Symbiote.js: zaawansowana kompozycja komponentów webowych bez wirtualnego DOM
Symbiote.js — lekka (~6 kB Brotli) biblioteka do tworzenia komponentów webowych, skupiająca się na zaawansowanej kompozycji poprzez znaczniki HTML. Rozwiązuje problem strukturalnej organizacji interfejsów, eliminując zależność od wirtualnego DOM i oferując elastyczne mechanizmy pracy z kontekstami danych. W odróżnieniu od tradycyjnych frameworków, Symbiote.js działa bezpośrednio z DOM API, minimalizując boilerplate i umożliwiając łączenie komponentów za pomocą deklaratywnej składni.
Podstawy kompozycji strukturalnej
Tworzenie interfejsów składa się z dwóch aspektów: logicznego modelu (komponenty, dane) i kompozycji strukturalnej (związki komponentów, przepływ danych). Symbiote.js proponuje unikalne podejście do tej drugiej, używając HTML jako pierwszorzędnego narzędzia do definiowania zależności. Kluczowe zasady:
- Szablony komponentów — niezależne ciągi HTML
- Zewnętrzna składnia definiuje strukturalne powiązania
- Brak wiązania z runtime'em JS
- Obsługa podejść hybrydowych (renderowanie po stronie serwera/klienta)
To pozwala ożywić wcześniej wygenerowaną składnię, tworzyć „głupie” komponenty bez własnej logiki lub korzystać z dostawców kontekstu. Przykładem jest implementacja zakładek za pomocą dwóch powiązanych komponentów:
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');
Drugi komponent obsługuje wyświetlanie treści:
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');
Kluczowe cechy techniczne
Przykład ilustruje pięć kluczowych mechanizmów:
- rootStyles — style są aplikowane poprawnie niezależnie od obecności Shadow DOM w elementach rodzicielskich. Zapewnia to kompatybilność z Tailwind, Bootstrap i globalnymi stylami.
- renderCallback — cykl życia z dostępem do węzłów potomnych (w przeciwieństwie do standardowego connectedCallback).
- init$ — inicjalizacja reaktywnych właściwości z wartościami domyślnymi.
- sub() — subskrypcja zmian właściwości poprzez Pub/Sub bez wirtualnego DOM.
- *Właściwości poprzez (gwiazdka)** — deklaracja współdzielonego kontekstu (analogicznie do atrybutu name w input[type=radio]).
Cechą architektury jest bezpośrednia praca z DOM API bez abstrakcji. Nie jest to antypattern, lecz świadomy wybór eliminujący koszty wirtualnego DOM. Biblioteka wspiera jednocześnie typizację, reaktywność i debugowanie w runtime.
Paradigma low-code w działaniu
Zalety Symbiote.js ujawniają się w scenariuszach, w których interfejsy są składane za pomocą atrybutów HTML bez JavaScriptu. Rozważmy komponent wyświetlający bieżącą zakładkę:
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');
Użycie:
<super-current ctx="section-select"></super-current>
Taki model otwiera możliwości dla:
- Projektantów: prototypowanie interfejsów bezpośrednio w HTML
- Menedżerów treści: konfiguracja wyświetlania poprzez atrybuty bez kodu
- Rozwiązań embedowanych: konfiguracja widżetów poprzez składnię
- Systemów AI: generowanie interfejsów z przewidywalnym powiązaniem składni i zachowania
Im więcej komponentów w bibliotece, tym łatwiej składać złożone interfejsy za pomocą HTML. Każda grupa komponentów jest izolowana poprzez atrybut ctx, co umożliwia umieszczanie na stronie wielu niezależnych instancji.
Co jest ważne
- Zerowa zależność od bundlerów: komponenty działają bez Webpack/Vite
- Kontekst poprzez DOM: powiązania między komponentami definiowane przez strukturę składni
- 6 kB rozmiaru: minimalny wpływ na wydajność
- Kompatybilność z dowolnym stackiem: integracja do istniejących projektów bez migracji
- Typy „out of the box”: obsługa TypeScript bez dodatkowej konfiguracji
Symbiote.js nie zastępuje frameworków, lecz uzupełnia je w obszarze kompozycji strukturalnej. Dla zadań logicznych oferuje minimalistyczne API, koncentrując się na tym, co inne rozwiązania realizują za pomocą zewnętrznych bibliotek (zarządzanie stanem, routing). Zmniejsza to obciążenie poznawcze przy projektowaniu interfejsów, szczególnie w zespołach o zróżnicowanym składzie.
— Editorial Team
Brak komentarzy.