Symbiote.js: суперспособности для веб-компонентов
- понедельник, 20 апреля 2026 г. в 00:00:07
Смысл создания и использования новых библиотек и фреймворков в том, чтобы решить задачи, которые не были решены ранее. Либо, в том, чтобы решить какую-либо задачу более эффективно, чем это уже было сделано кем-то.
Сегодня мы начнем разговор о задачах, которые можно решать с помощью Symbiote.js, и делать это гораздо проще и элегантнее, чем с другими фреймворками.
Symbiote.js - это легкая (~6 кб brotli), но очень мощная библиотека, основанная на веб-компонентах. Ее основным отличием от конкурентов является фокус на продвинутых композиционных возможностях в рамках HTML-разметки (как общей, так и в шаблонах) и более гибкой работе с контекстами данных.
Symbiote.js - это самодостаточное решение для создания сложных современных интерфейсов. С ним вам не нужно собирать классический бутерброд из глобального стейт-менеджера, роутера или SSR - все это есть из коробки. При этом, вы получаете максимальную творческую свободу, без обязательной привязки к компиляторам, сборщикам, каким-то закрытым экосистемам. И все это с минимумом бойлерплейта, типами, реактивностью, рантайм дебаггером и всем тем полезным и современным, к чему мы давно привыкли.
Работу с интерфейсами можно, условно, разделить на 2 составляющие:
Логическая - компонентная модель и логика + абстракции данных
Структурная - композиция компонентов и потоков данных
И в первом и во втором случае, Symbiote.js имеет свои уникальные фишки. Но сейчас я предлагаю сосредоточиться именно на второй части.
Библиотека заточена на работу с HTML. Собственные шаблоны компонентов в ней - это независимые HTML-строки. Внешний HTML - это полноценный каркас и определение структурных зависимостей. Принципиально отсутствует жесткая привязка к JS-рантайму. Это позволяет очень гибко оперировать элементами вашего интерфейса на композиционном и декларативном уровне, причем, как на клиенте так и на сервере.
Вы можете оживлять предварительно созданную разметку, рендерить шаблоны с нуля или использовать любые гибридные подходы. Вы можете создавать “чистые” компоненты-провайдеры контекста и “глупые” компоненты без своей логики, которые автоматически подключаются к, основанному на DOM-структуре, или полностью абстрактному контексту данных. Вы можете использовать один компонент с совершенно разными кастомными шаблонами без единой дополнительной строчки в JS, что особенно полезно для встраиваемых решений, где декларативный подход к конфигурированию особенно удобен.
Перейдем к примерам.
Давайте создадим нечто предельно понятное и полезное - универсальные табы для нашего интерфейса.
Наше решение будет состоять из двух частей: переключателя табов и view-контейнера для отображения выбранного контента.
Шаг первый:
import Symbiote, { css } from '@symbiotejs/symbiote'; class SuperTabs extends Symbiote { init$ = { '*currentTabName': 'first', }; renderCallback() { this.tabEls = [...this.querySelectorAll('[tab]')]; this.tabEls.forEach((/** @type {HTMLElement} */ el) => { let tab = el.getAttribute('tab'); if (el.hasAttribute('current')) { this.$['*currentTabName'] = tab; } el.onclick = () => { this.$['*currentTabName'] = tab; }; }); this.sub('*currentTabName', (val) => { this.tabEls.forEach((/** @type {HTMLElement} */ el) => { if (el.getAttribute('tab') === val) { el.setAttribute('current', ''); } else { el.removeAttribute('current'); } }); }); } } SuperTabs.rootStyles = css` super-tabs { display: inline-flex; gap: 2px; [tab] { cursor: pointer; &[current] { background-color: transparent; pointer-events: none; } } } `; SuperTabs.reg('super-tabs');
Шаг второй:
class SuperTabsView extends Symbiote { renderCallback() { this.tabCtxEls = [...this.querySelectorAll('[tab-ctx]')]; this.sub('*currentTabName', (val) => { this.tabCtxEls.forEach((/** @type {HTMLElement} */ el) => { if (el.getAttribute('tab-ctx') === val) { el.setAttribute('active', ''); } else { el.removeAttribute('active'); } }); }); } } SuperTabsView.rootStyles = css` super-tabs-view { display: block; [tab-ctx] { display: none; &[active] { display: contents; } } } `; SuperTabsView.reg('super-tabs-view');
Наши табы готовы и будут прекрасно работать в сочетании с любым другим фреймворком или полностью самостоятельно:
<super-tabs ctx="section-select"> <button tab="first">First</button> <button tab="second">Second</button> <button tab="third">Third</button> </super-tabs> <super-tabs-view ctx="section-select"> <div tab-ctx="first">First content</div> <div tab-ctx="second">Second content</div> <div tab-ctx="third">Third content</div> </super-tabs-view>
На что обратить внимание в этих примерах кода?
Интерфейс rootStyles - в данном случае, компоненты создаются без собственного Shadow DOM по умолчанию. Но они могут быть “в тени” внешнего Shadow DOM, который может быть где-то вверх по дереву. А может и не быть. И в том и в другом случае, стили будут применены корректно. Любые ваши тэйлвинды, бутстрапы и общие стили документа, также, тут работают штатно.
Коллбек жизненного цикла renderCallback - этот хук гарантирует нам доступ к дочерним DOM-узлам компонента (стандартный connectedCallback такой гарантии не дает).
Инициализация свойств init$ - тут мы инициализируем свойство и задаем значение по умолчанию (имя активного таба).
Подписка на свойство с помощью метода sub() - базовый паттерн для работы с реактивными свойствами - простой и понятный Pub/Sub. Подписки (отписки) и публикации значений могут происходить как полностью автоматически, так и явно, как в примере.
Определение свойства через *propName - пример объявления свойства для Shared Context. Это уже не собственное свойство компонента, оно общее для всех, у кого явно задан атрибут ctx. Похожим образом, к примеру, работает нативный браузерный элемент <input type="radio"> и его атрибут name.
Кроме того, как видите, Symbiote.js отлично сочетается со стандартными методами DOM API. И, в отличие от многих других фреймворков, тут это совсем НЕ антипаттерн, так как нет никакого Virtual DOM.
Работать с другим подходом, когда вы НЕ взаимодействуете с DOM напрямую - также, легко и удобно. Для примера давайте создадим “глупый” компонент, который будет просто отображать текущее значение контекста табов:
import Symbiote, { html, css } from '@symbiotejs/symbiote'; class SuperCurrent extends Symbiote {} SuperCurrent.rootStyles = css` super-current { display: block; h2 { text-transform: capitalize; } } `; SuperCurrent.template = html`<h2>{{*currentTabName}}</h2>`; SuperCurrent.reg('super-current');
Использование в разметке:
<super-current ctx="section-select"></super-current>
Вот и все, минимум кода - и вы связали независимые компоненты в одну взаимодействующую группу. И этих компонентов и групп на странице может быть сколько угодно. Естественно, аналогичным образом вы можете привязывать обработчики событий, и любые более сложные структуры данных.
Где все это особенно полезно?
Представьте, что вы создали библиотеку таких атомарных компонентов. Дальше, человек без глубоких знаний JavaScript может собирать из них интерфейсы, оперируя исключительно HTML-разметкой. Подключил скрипт, расставил теги с нужными атрибутами - готово. Никакого сборщика, никакого фреймворка в голове - только структура и смысл.
Команды часто состоят из разного рода специалистов: дизайнеров, аналитиков, маркетологов, SEO-шников… Таким образом, мы создаем общедоступный технический протокол общения, и позволяем огромному количеству людей вносить свой вклад без необходимости погружаться в дебри настоящего программирования или дергать разработчиков ради любой мелочи.
Это открывает двери для:
Дизайнеров - прототипирование интерфейсов прямо в HTML
Контент-менеджеров - настройка отображения контента через атрибуты, без написания кода
Встраиваемых решений - сложные виджеты, которые любой может настроить через HTML, не погружаясь в код
AI-ассистентов - генерация и модификация интерфейсов на лету, где простая и предсказуемая связь между разметкой и поведением критически важна
Описанные в статье механики - это ДАЛЕКО не все, что умеет Symbiote.js. Я сознательно не стал перегружать материал и планирую целый цикл подобных публикаций с примерами и реальными кейсами. Вы же, со своей стороны, можете провести интересный эксперимент: попросить ИИ привести пример решения подобной задачи в любом другом, интересующем вас, фреймворке и сравнить объем и сложность полученного кода, размер бандла, количество зависимостей и т.д. Уверяю вас, результат заставит вас посмотреть на Symbiote.js более внимательно.