Gdzie powinna znajdować się granica ramy w ponownie używalnej warstwie interfejsu użytkownika
Dowiedz się, w jaki sposób maszyny stanowe, komponenty Web oraz układ i ruch oparte na atrybutach pozwalają zachowaniom interfejsu użytkownika przetrwać poza ramami konkretnego frameworka, oraz kiedy ta przenośność nie jest warta zachodu.
Większość zespołów doskonale rozwiązuje problemy z interfejsem użytkownika tylko dla jednego frameworka. Starannie stworzony zestaw komponentów Angular, zbiór elementów bazowych React, system projektowy połączony z cyklem życia konkretnego renderera – to wszystko działa doskonale, dopóki projekt nie wymaga innego narzędzia, a wtedy niemal cała ta inwestycja staje się bezużyteczna. Podział ponownie używalnego interfejsu na warstwy (behawior, komponenty renderowane oraz proste funkcje HTML, takie jak układ i animacje) pozwala celowo zdecydować, które warstwy powinny być powiązane z frameworkiem, a które nie.
Problem rozwiązywania problemów interfejsu tylko dla jednego frameworka
Załóżmy zespół front-endu, który spędza większość czasu na pracy z Angular i jest zadowolony z rozwoju tego frameworka: sygnały, nowoczesne API oraz dużo struktury dla aplikacji, które tego potrzebują. Ich wewnętrzne narzędzia opierają się na modelu shadcn, w którym źródło komponentów jest kopiowane do każdego projektu, by stać się jego własnością, a do pracy na niskim poziomie wykorzystywane są specyficzne dla Angular bezgłowe prymitywy. W aplikacji Angular to doskonałe rozwiązanie.
Problemy zaczynają się wtedy, gdy następny projekt nie jest projektem Angular. Duże aplikacje z dużą ilością stanów i intensywnymi interakcjami mogą doskonale pasować do Angulara. Natomiast przeważnie statyczne strony marketingowe często lepiej funkcjonują przy użyciu Astro. Czasami sama platforma przeglądarki już zapewnia niemal wszystko, co jest potrzebne. Jeśli technologia ma podążać za wymaganiami każdego projektu, a nie odwrotnie, to pojawia się nieuniknione pytanie:
Ile z Twojej infrastruktury interfejsu użytkownika powinno przetrwać po zmianie frameworka?
Pogoni za tą kwestią prowadzi nas przez serię pomysłów: najpierw komponenty webowe, potem interfejsy bez głowy, następnie maszyny stanowe, a na końcu mniej konwencjonalny eksperyment, w którym układ i ruch otrzymują własne atrybuty HTML. Na pierwszy rzut oka wydają się niespowiązane. Gdy przyjrzymy im się razem, okazuje się, że są to różne rozwiązania tego samego problemu projektowego: jak daleko można dzielić się interfejsem, zanim framework aplikacji będzie musiał przeniknąć do każdej abstrakcji?
Bez głowy to nie to samo co niezależne od frameworka
Interfejsy bez głowy już rozwiązują dużą część tego problemu. Radix Primitives jest jasnym przykładem tego, dlaczego ten model się sprawdził. Zamiast dostarczać element Dialog z kolorem tła, odstępami, cieniem i promieniem obramowania należącymi do kogoś innego, dostarcza trudne elementy, a wygląd pozostawia do Twojej decyzji.
Te trudne aspekty to prawdziwa praca: zarządzanie uwagą, nawigacja klawiaturą, poprawne atrybuty ARIA, zachowanie przy zamknięciu oraz mnóstwo drobnych szczegółów, które łatwo zaniedbać, gdy modal wydaje się być po prostu jednym div umieszczonym na innym. Radix celowo dostarcza swoje elementy bazowe bez stylizacji i przedstawia je jako dostępne elementy React. Ty kontrolujesz prezentację, ale model komponentów pozostaje taki sam jak w React.
To może brzmieć oczywiste, ale ma swoje konsekwencje. Usunięcie zależności od stylizacji nie oznacza usunięcia zależności od frameworka. To samo dotyczy świata Angular: element bazowy może być całkowicie neutralny pod względem kolorów, odstępów i typografii, jednocześnie polegając w pełni na dyrektywach Angular, sygnałach, iniekcji zależności i hakenach cyklu życia.
To nie jest wada. Bardzo często jest to właśnie właściwy wybór. Prymitywa zaprojektowana dla Angulara może ściśle integrować się z Angularem, a prymitywa React może wykorzystywać model kompozycji Reacta. Głęboka integracja zazwyczaj zapewnia lepsze doświadczenie programisty niż udawanie, że framework nie istnieje. Chodzi tylko o to, że dwie różne koncepcje są często traktowane jako synonimy, mimo że takimi nie są:
headless
≠
framework-agnostic
Komponenty bez interfejsu wizualnego rezygnują z aspektów estetycznych. Abstrakcje niezależne od frameworka idą o jeden poziom głębiej i starają się unikać założeń dotyczących samego renderera. Są to dwa odrębne poziomy ponownego użycia, więc warto wiedzieć, który z nich faktycznie jest potrzebny.
Zachowanie może znajdować się poniżej komponentu
Weźmy menu spadkowe. Dwa takie menu mogą wyglądać zupełnie inaczej. Jedno znajduje się na stronie marketingowej z dużym tekstem, dużą ilością wolnego miejsca oraz animowanymi przejściami; drugie znajduje się na ciasnej pasku narzędzi IDE, gdzie każdy piksel ma znaczenie. Jedno może być renderowane przez Angular, drugie przez React, a trzecie przez komponent Web Component.
Poza różnicami wizualnymi ciągle pojawiają się te same pytania:
- Czy menu spadkowe jest obecnie otwarte?
- Który element jest aktywny?
- Czym służy klawisz Escape?
- Czy użytkownik może przemieszczać się między elementami za pomocą klawiszy strzałek?
- Jak radzi sobie z elementami wyłączonymi?
- Gdzie trafia uwaga po zamknięciu menu spadkowego?
Żadne z tych pytań nie zależy od tego, czy markup został stworzony przez Angular, czy React. To przede wszystkim problemy interakcji, a dopiero potem problemy renderowania.
Maszyny stanowe jako wspólny kontrakt
Tutaj interfejs użytkownika sterowany maszynami stanowymi staje się szczególnie atrakcyjny. Zag.js jest najbardziej znanym przykładem takiego podejścia. Zamiast traktować komponent React jako źródło prawdy, Zag modeluje interakcje jako maszyny niezależne od konkretnego frameworka. Odrębne adaptery frameworków łączą te maszyny z sposobem, w jaki React, Vue, Solid, Svelte i inne narzędzia radzą sobie z reaktywnością, życiowym cyklem oraz DOM-em, a dokumentacja wyjaśnia, jak napisać adapter dla frameworka, którego jeszcze nie obejmuje.
W tradycyjnym komponencie każda kwestia jest zawarta w jednej jednostce specyficznej dla danego frameworka:
React Dropdown
├─ state
├─ interactions
├─ accessibility
└─ rendering
Dzięki obecności maszyny pośredniczącej zachowanie jest definiowane raz, a każdy renderer je wykorzystuje:
Dropdown behavior
│
state machine
│
┌────────────┼────────────┐
↓ ↓ ↓
React Vue Svelte
Ostateczne komponenty pozostają oddzielnymi elementami, a każda platforma nadal może zachowywać się jak platforma. To, co zostało przeniesione, to umowa behawioralna – bardziej przydatna definicja pojęcia „niezależny od platformy” niż fantazja o jednym magicznym komponencie, który działa wszędzie bez żadnej integracji. Lepszy podsumowanie brzmi: napisz zachowanie raz, a następnie dostosuj je do tego renderera, który jest odpowiedni dla aplikacji.
Pomocnym modelem myślowym jest to, że maszyna stanowi czysty opis stanów, zdarzeń i przejść. Ponieważ nie przechowuje żadnych odniesień do DOM ani stanu platformy, łatwo ją również testować w izolacji: wysyłasz do niej zdarzenia i sprawdzasz uzyskany stan, bez konieczności montowania czegokolwiek.
Komponent może istnieć jako zachowanie, zanim stanie się elementem interfejsu
Ta sama zasada działa w bibliotekach komponentów webowych. Wyobraźmy sobie bibliotekę stworzoną za pomocą Stencil, która zawiera standardowe przyciski, okna dialogowe, spadające listy i karty. Te komponenty nie muszą być zastępowane maszynami stanowymi. Zamiast tego pod nimi może znajdować się oddzielna warstwa.
Fabryka przycisków wie o stanach wyłączonego i ładowania, obsłudze kliknięć oraz o tym, które właściwości powinny trafić do elementu interaktywnego. Fabryka spadających list wie o otwieraniu, zamykaniu, wyborze i nawigacji klawiaturą. Fabryka okien dialogowych zna ich cykl życia oraz reguły interakcji. Żadna z tych logik nie musi decydować o wyglądzie komponentu.
Koncepcyjnie coś takiego może istnieć jeszcze przed podjęciem jakiejkolwiek decyzji o renderowaniu:
const button = createButton({
disabled: false,
loading: false,
onClick(event) {
// application behavior
},
});
Zauważ, czego brakuje: nie ma Stencil, nie ma Angular, nie ma komponentu React, nawet CSS brakuje. Fabryka to jedynie opis zachowania, a renderer może zostać dołączony później. Na przykład przycisk Stencil z biblioteki wykorzystuje to zachowanie i udostępnia stylizowany Custom Element:
<and-button variant="destructive">
Delete
</and-button>
Inna aplikacja może otoczyć ten sam rdzeń behawioralny zupełnie innym przyciskiem. Weźmy na przykład IDE w stylu desktopowym, którego interfejs jest celowo znacznie bardziej gęsty niż typowa strona internetowa. Jego przyciski używają innych wymiarów, innych elementów projektowych oraz innego języka wizualnego, więc importowanie uniwersalnego przycisku Web Component byłby błędnym rozwiązaniem. IDE ma po prostu swój własny przycisk Angular, a ten przycisk nadal może wywołać tę samą fabrykę createButton().
To jest praktyczna korzyść. Celem nie jest to:
ONE BUTTON
↓
use everywhere
Cel jest następujący:
shared behavior
│
┌──────────┴──────────┐
↓ ↓
Web Component Angular component
↓ ↓
Web UI IDE UI
Prezentacja pozostaje lokalna dla każdego produktu, a nużąca logika interakcji nie musi być ponownie pisana dla każdego z nich.
Komponenty webowe sprawiają, że komponent renderowany jest przenośny
Maszyny stanowe sprawiają, że zachowanie jest przenośne. Nie wpływają one na renderowaną interfejs użytkownika, i to właśnie tutaj komponenty webowe zachowują swoją wartość. Element niestandardowy taki jak ten poniżej należy do platformy webowej, a nie do Angular, React czy Vue:
<and-button>
Save
</and-button>
Angular może go renderować, Astro może go emitować, React może go używać, a zwykła strona HTML może go zawrzeć za pomocą tagu script. Ramy programistyczne wokół niego mogą się zmieniać, podczas gdy sam element pozostaje dokładnie taki sam.
W praktyce wsparcie frameworków dla Custom Elements różni się pod względem szczegółów. Angular wymaga CUSTOM_ELEMENTS_SCHEMA (lub czegoś równoważnego), aby móc obsługiwać nieznane tagi, natomiast React historycznie przekazywał wartości jako atrybuty zamiast właściwości, co utrudniało obsługę złożonych danych i własnych zdarzeń, chociaż nowsze wersje poprawiły tę sytuację. Warto sprawdzić aktualne wsparcie swojego frameworka dla Custom Elements przed podjęciem decyzji o jego użyciu.
Otrzymujemy w ten sposób dwa różne rodzaje ponownego wykorzystania. Jeden to zachowanie przenośne:
State machine
↓
portable behavior
A drugi to komponent renderowany przenośny:
Web Component
↓
portable rendered component
Czasami chce się mieć cały komponent sieciowy. Jeśli przycisk w systemie projektowym musi wyglądać i zachowywać się identycznie we wszystkich aplikacjach, jego zapakowanie jako elementu niestandardowego jest rozsądnym wyborem. Innym razem w ogóle nie chce się mieć pełnego komponentu. Scenariusz IDE jest dokładnie taki: zachować logikę interakcji, ale pozwolić aplikacji w pełni kontrolować renderowanie i projekt.
To nie są konkurencyjne architektury; po prostu granica abstrakcji jest wyznaczona w różnych miejscach. Myślenie o granicach zamiast o bibliotekach wyjaśnia również coś innego: nie każdy element interfejsu użytkownika nadający się do ponownego użycia musi być komponentem.
Layout nie wymaga komponentu
Layout to najjaśniejszy przykład. Oto zwykły element Tailwind:
<div
class="
flex
flex-col
items-start
gap-6
p-6
rounded-xl
border
bg-card
shadow-sm
"
>
...
</div>
Nie ma z tym żadnego problemu. Jedną z prawdziwych zalet Tailwind jest to, że można zrozumieć większość elementu bez konieczności ciągłego przechodzenia między szablonem a plikiem stylów. Ale spójrz, ile zadań wykonuje ten jeden atrybut class. Niektóre z tych klas opisują identyfikator wizualny:
rounded-xl
border
bg-card
shadow-sm
Inne opisują relacje przestrzenne pomiędzy elementem a jego dziećmi:
flex
flex-col
items-start
gap-6
p-6
Przeglądarka nie robi między nimi żadnej różnicy. class to po prostu ogólny mechanizm do przypinania identyfikatorów, które mogą być wybierane przez CSS i JavaScript, a HTML nie rozróżnia „klasy układu” od „klasy projektowej”. Jednak jako decyzja projektowa API, rozdzielenie tych dwóch kategorii okazuje się korzystne. Eksperymentalny atrybut and-layout oddziela samą część przestrzenną:
<div
class="card"
and-layout="vertical align:start gap:lg p:lg"
>
...
</div>
Korzyścią nie jest zwięzłość; czasami ta wersja nie jest krótsza. Korzyścią jest to, że odpowiedzialności stają się widoczne: class opisuje, jak wygląda element, a and-layout opisuje, jak organizuje przestrzeń.
Jak implementowany jest atrybut
Implementacja nie wymaga ani frameworka, ani JavaScriptu. To czysty CSS: każdy element atrybutu jest dopasowywany za pomocą selektorów atrybutowych (selektor ~= dla słów oddzielonych spacjami jest idealnym rozwiązaniem) i mapowany na zasady Flexbox, Grid, zarządzania odstępami oraz responsywności. Deklaracja taka jak poniżej to w końcu po prostu CSS Grid z punktami przerwania.
<div
and-layout="grid cols:1 cols@md:2 cols@lg:3 gap:lg"
>
W tle nie kryje się żaden silnik układu, żadna instrukcja Angulara, żaden komponent Reacta ani żaden otoczak w tym stylu, chyba że faktycznie chcesz użyć Stacka:
<Stack direction="vertical" gap="lg">
To określenie ma znaczenie. Komponenty układu nie są złe. Abstrakcja <Stack> może być bardzo wygodna, szczególnie w systemach projektowych frameworków. Podejście oparte na atrybutach to po prostu kolejna opcja: gdy element, którego potrzebujesz, już istnieje, możesz nie musieć tworzyć dodatkowego komponentu tylko po to, by opisać, jak ułożone są jego dzieci.
Kompromis, który należy mieć na uwadze: atrybuty niestandardowe bez prefiksu data- nie są ważnym HTML zgodnie ze specyfikacją, mimo że wszystkie przeglądarki chętnie je stylizują. Narzędzia walidacyjne i niektóre lintery będą się skarżyć, a pisownia data-and-layout unika tego kosztem niewielkiej rozwlekłości.
class może zrobić wszystko, ale nie musi
Za tym kryje się szerszy wzorzec. Współczesne znaczniki mogą przenieść ogromną część obowiązków na class. Dodając CSS do celów pomocniczych oraz plugin animacji, zupełnie przeciętny element może przerodzić się w coś takiego:
<div
class="
card
flex
flex-col
items-center
gap-6
p-8
rounded-xl
border
bg-card
animate-in
fade-in
slide-in-from-bottom-4
duration-500
"
>
To działa, a czytelne klasy pomocnicze są o wiele lepsze niż udawanie, że każdy interfejs wymaga doskonale semantycznej hierarchii BEM. Prawdziwe pytanie nie brzmi, czy class może udźwignąć to wszystko; oczywiście, że tak. Pytanie brzmi, czy jeden atrybut powinien reprezentować każdą możliwość elementu. Rozdzielenie zadań daje nam:
<div
class="card"
and-layout="vertical align:center gap:lg p:xl"
and-motion="slide-in-up"
and-motion-duration="500ms"
>
Element teraz od razu pokazuje trzy odrębne aspekty:
class → visual identity
and-layout → spatial organization
and-motion → animation
Pomyśl o tym jako o niewielkim zastosowaniu zasady jednej odpowiedzialności w kontekście znaczników. HTML tego nie wymaga. Motywacją jest to, że taki wynik jest łatwiejszy do odczytania, przejrzenia i zmiany.
Ruch ma swój własny deklaratywny kanał
Część zajmująca się animacjami opiera się na wzorcu, który sprawdzał się dobrze przez lata. Animate.css, dostępny pod adresem animate.style, steruje animacjami wyłącznie za pomocą klas: dodaje się klasę podstawową wraz z nazwą animacji, a element się animuje.
<h1 class="animate__animated animate__bounce">
Hello
</h1>
Jest to proste, znajome i praktycznie pozbawione formalności. Plaginy do animacji opracowane z myślą o Tailwindzie podążają tą samą filozofią, przekształcając animacje w kolejną grupę komponowalnych narzędzi w formie klasy.
Ciekawe pytanie tutaj brzmi nie o to, jak stworzyć potężniejszy system animacji. Odzwierciedla to problem układu: jeśli animacje stanowią odrębną kwestię, mogą mieć osobne deklaratywne miejsce w kodzie. Zamiast umieszczać narzędzia do animacji obok wszystkiego innego:
<div
class="
card
animate-in
fade-in
slide-in-from-bottom-4
duration-500
"
>
opisuje się animację oddzielnie:
<div
class="card"
and-motion="slide-in-up"
and-motion-duration="500ms"
>
A gdy trigger ma znaczenie, należy to jasno określić wraz z opóźnieniem i czasem trwania:
<div
class="card"
and-motion="slide-in-up"
and-motion-trigger="enter"
and-motion-duration="800ms"
and-motion-delay="200ms"
>
Tagi określają, która animacja ma zostać uruchomiona, kiedy i w jaki sposób jest synchronizowana; implementacja zajmuje się resztą. W przypadku triggera enter oznacza to zazwyczaj użycie IntersectionObserver do monitorowania elementu. Triggerzy typu hover i tap wykorzystują własne słuchacze. Co istotne, zapytanie mediów prefers-reduced-motion można uwzględnić w jednym centralnym miejscu, zamiast polegać na tym, że każdy twórca komponentu o nim pamięta.
Atrybuty to nie magia. Oferują one sposób na informowanie o możliwościach elementu, bez konieczności włączania ich do drzewa komponentów frameworka.
Deklaratywne, dopóki deklaratywność jest pomocna
Ten podejście ma swoje ograniczenia. Prosta animacja wejścia doskonale pasuje do jednego atrybutu:
and-motion="fade-in"
Przejście wyjścia modalu to inny rodzaj problemu. Załóżmy, że modal musi zostać wyświetlony z animacją i usunięty z DOM-u dopiero po zakończeniu tej animacji. Wtedy kluczowe stają się cykl życia i kolejność działań, a krótka instrukcja imperatywna wyraża intencję jaśniej:
await player.play('fade-zoom-out');
closeModal();
Próba zakodowania każdej relacji z cyklu życia w coraz większym liczbie atrybutów HTML pogorszy deklaratywną API, zamiast ją ulepszyć. Podstawową zasadą jest wybór najprostszego rozwiązania, które dokładnie oddaje problem – czy to zwykły CSS, atrybut, maszyna stanów, standardowa usługa frameworku czy komponent Web. Styl deklaratywny nie powinien stać się dogmatem. Nikt nie próbuje pozbyć się JavaScriptu czy frameworków. Celem jest unikanie eskalacji każdego problemu do najbardziej złożonej dostępnej abstrakcji tylko dlatego, że istnieje.
Atrybuty jako małe API funkcjonalności
Gdy zarówno układ, jak i ruch podążają za tym wzorcem, atrybuty zaczynają przypominać nie tyle elementy konfiguracji, ile małe interfejsy API umożliwiające określanie funkcjonalności. Spójrzmy na ten fragment:
<section
class="feature-card"
and-layout="vertical gap:lg p:xl"
and-motion="fade-in"
and-motion-trigger="enter"
>
<h2>Framework-agnostic UI</h2>
<p>At least as much as possible.</p>
</section>
To nadal jest <section>, a jego semantyczne znaczenie pozostaje nienaruszone. Ustawienie odstępów, prezentacji oraz animacji wejścia nie wymagało użycia takiej warstwy otaczających elementów:
<and-stack>
<and-motion-container>
<and-card>
...
</and-card>
</and-motion-container>
</and-stack>
Ta wersja zagnieżdżona nie jest automatycznie błędna. Jeśli te elementy charakteryzują się sensownym zachowaniem i oferują przydatną API, z pewnością mogą być uznane za komponenty. Różnica jest węższa: możliwość wielokrotnego użycia nie musi automatycznie stać się kolejnym komponentem. Często węzeł DOM już istnieje i potrzebuje jedynie układu, animacji lub funkcji podpowiedzi. Framework nie zawsze musi o nim wiedzieć, a gdy abstrakcja znajduje się bezpośrednio w HTML, może być bezkosztowo wykorzystana w Angular, Astro, React, Vue lub na stronie bez żadnego frameworka.
Bezorientowanie na framework nie oznacza braku frameworka
Istnieje tu oczywiste ryzyko: pojęcie „bez względu na framework” może łatwo przerodzić się w kolejną rywalizację o „czystość”, co całkowicie pomija sedno sprawy. Frameworki zyskują swoje miejsce dzięki integracji różnych elementów. Angular oferuje sygnały, szablony, iniekcję zależności, formularze, routowanie oraz jasny model aplikacji. React posiada bardzo rozwinięty ekosystem kompozycji. Vue i Svelte wprowadzają inne kompromisy.
Behawior niezależny od frameworka musi nadal być powiązany z systemem reaktywnym danej aplikacji. Właśnie dlatego Zag dostarcza adaptery frameworków: adapter łączy daną aplikację z konwencjami reaktywności, cyklu życia oraz DOM frameworka, a aplikacja może pozostać niezależna tylko dlatego, że inna warstwa rozumie ten framework.
Podejście warstwowe opisane tutaj niesie ze sobą te same koszty:
- Komponent Angular zbudowany na ogólnej fabryce stanu może wymagać funkcji
effect(), aby dane wejściowe były zsynchronizowane ze stanem zewnętrznym. - Komponent Web musi połączyć warstwę stanu z własnymi funkcjami obsługi cyklu życia.
- Rozwiązanie oparte na atrybutach wprowadza prosty język specyfikacji, który każdy członek zespołu musi opanować.
- Atrybuty ruchu dodają kolejną warstwę interfejsu API.
- Dzielenie wszystkiego na oddzielne pakiety tworzy umowy, które muszą pozostać kompatybilne z upływem czasu.
Czasami prosty komponent specyficzny dla danego frameworka jest po prostu lepszym rozwiązaniem projektowym. Dlatego zestaw komponentów dostępny wyłącznie dla Angulara nadal ma sens obok tego wszystkiego. Nikt nie powinien przekazywać każdego przycisku w Angularze przez łańcuch czterech adapterów, parę mas stanu oraz komponent Web Component tylko dlatego, że termin „agnostyczny” brzmi bardziej zaawansowanie. To bardzo kosztowny sposób na uniknięcie pisania:
<volt-button>
Niezależność od frameworka opłaca się, gdy przenośność jest rzeczywistym wymogiem. Gdy tak nie jest, ścisła integracja z frameworkiem często stanowi lepsze rozwiązanie.
Interfejs użytkownika agnostyczny wobec frameworka jako stos, a nie biblioteka
Wyrażenie „biblioteka interfejsu niezależna od frameworka” zwykle kojarzy się z zestawem komponentów, które w jakiś sposób działają wszędzie. Komponenty Web Components dość blisko tego celu doprowadzają w przypadku renderowanych komponentów. Bardziej przydatnym modelem jest jednak nie jedna uniwersalna biblioteka, lecz szereg odpowiedzialności, z których każda staje się specyficzna dla danego frameworka w innym momencie:
Application
│
Angular / React / Vue / Astro
│
framework adapters
│
─────────────────────────────────────────
│
headless behavior / state machines
│
Web Components HTML capabilities
│ │
│ layout / motion attributes
│ │
─────────────────────────────────────────
│
Web Platform
Żadna aplikacja nie musi korzystać ze wszystkich warstw:
- Jeden projekt używa bezpośrednio komponentu Web Component i w ogóle nie dotyka leżącego pod nim stanu bezinterfejsowego.
- Inny wykorzystuje tylko maszynę stanów i buduje na jej bazie własny interfejs Angular.
- Statyczna strona Astro może nie potrzebować niczego poza atrybutami układu i ruchu.
- Aplikacja Angular może zignorować całą tę strukturę i użyć narzędzi typu Angular z jego własnymi elementami, ponieważ zapewnia to najlepsze doświadczenie programisty.
Główna idea polega na możliwości łączenia różnych elementów. Brak zależności od konkretnego frameworka nie powinien oznaczać jego zakazu. Oznacza to, że to ty decydujesz, gdzie znajduje się granica frameworka, zamiast pozwalać mu automatycznie rozprzestrzeniać się na każdą warstwę, którą można ponownie wykorzystać.
Zachowanie frameworka na warstwie aplikacji
To wszystko nie jest argumentem przeciwko Angularowi ani żadnemu innemu frameworkowi. To argument dotyczący tego, jak dużą odpowiedzialność framework otrzymuje domyślnie. Przyjrzyj się ponownie przykładom z tego punktu widzenia:
- Wizualny projekt spadającego menu może należeć do produktu, a jego renderowanie do Angulara, ale model interakcji nie musi tak być.
- Wspólny przycisk może być komponentem Web Components, gdy chcesz mieć identyczny przycisk we wszystkich projektach, albo może to być dostosowany komponent Angulara, który dzieli się tylko małym rdzeniem behawioralnym, gdy produkt wymaga własnego języka wizualnego.
and-layout.Gdy spojrzymy na to razem, wszystko pasuje do siebie:
- Headless UI eliminuje elementy wizualne.
- Maszyny stanowe uwalniają zachowanie od konkretnego renderera.
- Komponenty webowe umożliwiają przemieszczanie gotowych, zrenderowanych komponentów pomiędzy różnymi systemami.
- Dedykowane atrybuty umożliwiają przypisanie lekkich funkcji, takich jak układ i ruch, bezpośrednio do samego HTML, a nie do frameworka.
Żadna z tych koncepcji nie jest nowa, a nie każdy system interfejsu użytkownika powinien być budowany w ten sposób. Połączone jednak stanowią wyzwanie dla długo utrzymywanego standardu: że framework musi znajdować się również pod każdą ponownie używalną abstrakcją interfejsu, jaką posiada aplikacja.
Główne wnioski
- Rozróżniaj „bez stylizacji” od „niezależnego od renderera”; większość bibliotek typu headless to tylko te pierwsze.
- Logikę interakcji, którą dzielą się różne produkty, umieść w maszynach stanowych lub fabrykach niezależnych od frameworka, świadomie akceptując koszt związany z adapterami.
- Używaj Web Components, gdy sam komponent renderowany, a nie tylko jego zachowanie, musi być identyczny we wszystkich implementacjach.
- Dla układu i prostych animacji korzystaj z możliwości dostarczanych wyłącznie przez CSS, a przejdź na kod imperatywny, gdy ważny stanie się porządek cyklu życia komponentu.
Literatura pokrewna
- Kiedy użyteczne komponenty React mają negatywne skutki: eksplozja propów i rozwiązanie — Dowiedz się, jak przedwczesne ponowne użycie przekształca prosty komponent React w obciążenie spowodowane dużą ilością propów, oraz jak duplikacja, złożone komponenty i zasada trzech zapobiegają temu.
- Przemyślenie o stanie w React: Gdzie naprawdę powinny znajdować się twoje dane — Ten artykuł wyjaśnia, jak zmniejszyć błędy w React poprzez przenoszenie stanu do URL, DOM lub wartości pochodnych zamiast nadmiernego używania useState.