Web Components to rozwiązanie, które pozwala tworzyć własne elementy HTML wraz z ich logiką i wyglądem. Przypomina to nieco rozwiązania znane z większości frontendowych frameworków, takich jak React, Vue czy Angular.
Na wstępie warto jednak zaznaczyć, że nie jest to jedna technologia, a zbiór kilku różnych API, które udostępniają nam przeglądarki. W praktyce oznacza to, że nie jest to część języka JavaScript i możemy z nich korzystać jedynie podczas pisania kodu dedykowanego dla klienta.
Czy to w ogóle potrzebne?
Odpowiedź na to pytanie nie jest oczywista. W dzisiejszym świecie większość stron internetowych korzysta z jakiegoś frameworka, a jeśli tego nie robi, to prawdopodobnie jest oparta o Wordpress lub inny CMS. Czy w takim razie jest jeszcze miejsce na kolejną technologię i przede wszystkim, czy ta technologia jest w czymkolwiek lepsza, od tych, które wymieniłem przed chwilą?
Zacznijmy od minusów:
Web Components nie zastąpią frameworka - owszem, pozwalają na tworzenie modularnych, reużywalnych komponentów, ale w żaden sposób nie pomagają nam w zarządzaniu widokiem, routingiem ani stanem globalnym aplikacji, a tego (i wielu innych rzeczy) oczekujemy od frameworka.
Są oparte o klasy - choć nie musi to być minus, to nie bez powodu twórcy Reacta zdecydowali się na odejście od stosowania komponentów klasowych. Czytelność tego podejścia i konieczność stosowania słowa kluczowego
this
(artykuł na jego temat znajdziesz tutaj) może być dla wielu, zwłaszcza początkujących programistów, zniechęcająca.Stosują mniejszą warstwę abstrakcji - jeżeli napisałeś/aś choć jeden komponent Reacta, to z pewnością wiesz, że twórcy postarali się o to, żeby programista używający go, nie musiał pisać zbyt wiele niskopoziomowego (jak na JS 😉) kodu. W przypadku Web Components trzeba będzie napisać go nieco więcej, a niektóre rozwiązania trzeba będzie utworzyć od zera, lub wykorzystać zewnętrzne biblioteki.
Lista minusów, choć niedługa, to niestety przedstawia dość poważne wady. Przede wszystkim nie jest to kompletne rozwiązanie do tworzenia stron internetowych. Jeżeli jednak podejdziemy do tematu w inny sposób to może się okazać, że jest to niezwykle przydatna opcja. Jakie są w takim razie zalety?
Reużywalność - elementy utworzone w ten sposób mogą być wykorzystane w aplikacjach napisanych przy użyciu nowoczesnych frameworków, takich jak Angular, Solid i React. Ta lista jest oczywiście znacznie dłuższa, jednak nie jest to jedyna opcja w przypadku Web Components. Nic nie stoi na przeszkodzie, by zastosować je wraz z czystym JavaScriptem lub TypeScriptem. Reużywalność to prawdopodobnie największy plus tej technologii.
Niezależność - ten punkt wynika bezpośrednio z poprzedniego. Web Components nie są powiązane z żadnym z frameworków, dzięki czemu unikamy przywiązania do jednego z nich. Najczęściej spotkasz się z pojęciem vendor-lock, czyli właśnie przywiązaniem do jednego dostawcy usług lub technologii. Jeżeli zdecydujemy się na wykorzystanie na przykład Reacta, to jesteśmy uzależnieni od rozwoju tej technologii i jednocześnie musimy podążać w tym samym kierunku co jej twórcy. W przypadku mniej popularnych z nich, może się nawet okazać, że przestanie ona być wspierana.
Enkapsulacja - cała logika oraz wygląd danego komponentu są zawarte w jego wnętrzu, dzięki czemu unikamy zanieczyszczenia globalnej przestrzeni i uniemożliwiamy ingerencję w wewnętrzne działanie elementu, udostępniając przy tym jedynie niewielki interface, za pomocą którego użytkownik jest w stanie dostosować komponent do własnych potrzeb. Rozwiązanie to jest doskonalone znane użytkownikom frameworków i zazwyczaj można je spotkać pod nazwą "props" lub po prostu "propsy".
Zamiast zanudzać cię kolejnymi teoretycznymi rozważaniami i opisywać, czym dokładnie jest Shadow DOM, Shadow Host, metody cyklu życia, przejdźmy od razu do kodu, a wszystkie potrzebne pojęcia poznasz w trakcie jego pisania.
Pierwszy komponent
Na początek coś bardzo łatwego. Stworzymy komponent, który będzie odpowiedzialny za wyświetlanie nagłówka. Nie będzie w nim zbyt wiele logiki (na to przyjdzie czas później), skupimy się na wyglądzie i składni.
Wspomniałem, że Web Components oparte są na klasach. Zacznijmy zatem od utworzenia nowej klasy o nazwie SectionTitle
(nazwa jest dowolna, powinna jednak oddawać charakter elementu). Każdy komponent musi również rozszerzać istniejącą klasę HTMLElement.
Pamiętaj także o wywołaniu konstruktora klasy rozszerzanej za pomocą super().
Ostatni element wymagający wyjaśnienia, to wywołanie metody attachShadow({ mode: "open" })
. Odpowiada ona za dołączenie Shadow DOMu do tworzonego elementu, co w praktyce czyni go Shadow Rootem. Właściwość open
oznacza, że struktura HTML Shadow DOMu będzie widoczna "na zewnątrz", a więc z poziomu DOM.
Zanim przejdziemy dalej, wyjaśnijmy sobie te pojęcia:
Shadow DOM - to wirtualne drzewo DOM, które jest dołączane do wybranego elementu HTML (najczęściej będzie nim właśnie Web Component). Z jego pomocą możemy tworzyć poddrzewa (ang. subtree) wewnątrz light DOMu (czyli po prostu DOMu).
Shadow Host - element w drzewie dokumentu, do którego przyłączone zostało Shadow Tree.
Shadow Root - to element będący rodzicem pozostałych elementów w Shadow Tree.
Przedstawienie tego na diagramie powinno pomóc rozjaśnić nieco sytuację:
Shadow DOM wewnątrz Light DOM lub po prostu DOM
W tym momencie nasz komponent nie posiada jeszcze żadnej struktury ponieważ, nie utworzyliśmy wewnątrz żadnego tagu HTML. Zatem kolejnym krokiem będzie dołączenie do istniejącej już właściwości shadowRoot
(która jest dostępna dzięki rozszerzeniu klasy HTMLElement
) tagów HTML.
Można również stosować metody createElement
w celu tworzenia elementów HTML, a następnie appendChild
, żeby łączyć je z shadowRoot
ale takie podejście będzie wymagać od nas znacznie większej ilości kodu, a przy okazji obniży jego czytelność.
Na tym etapie warto także wyjaśnić, czym dokładnie jest tag <slot>
, choć dla użytkowników Vue z pewnością nie jest on niczym nowym. Stosuje się go najczęściej w połączeniu z tagiem <template>
lub, tak jak w naszym przypadku, podczas tworzenia własnych elementów HTML. Pełni on rolę placeholdera i mówiąc wprost "trzyma" wolne miejsce w które w przyszłości trafi tekst albo inne tagi. Wrócimy do tego tematu, kiedy przyjdzie czas na wykorzystanie slotów.
Struktura HTML jest prawie gotowa, brakuje nam jedynie styli CSS. Podobnie jak poprzednio, najłatwiej (i najczytelniej) będzie skorzystać z innerHTML
i zastosować tag <style>
. Edytujmy zatem kod utworzony wcześniej:
Zwróć uwagę, że nie korzystamy tutaj z klas CSS, które mogą być już dostępne w danym projekcie. Gdybyśmy wykorzystali jakąkolwiek właściwość (klasę CSS, zmienną, itd.) pochodzącą z zewnątrz, czyli spoza klasy SectionTitle
, to nasz komponent przestałby być reużywalny i stałby się ściśle powiązany z konkretnym kodem.
Zastosowanie tagu <style>
sprawia, że style w nim utworzone nie będą widoczne na zewnątrz, a więc poza klasą SectionTitle
, dzięki czemu zachowujemy pełną enkapsulację i jednocześnie nie wpływamy na reużywalność.
Pozostał jeszcze jeden krok - zdefiniowanie nowego elementu. Bez tego nie będziemy mogli wykorzystać utworzonego właśnie komponentu. Jedyne co musimy zrobić to wykorzystać metodę define
dostępną w obiekcie customElements
. Zamiast jednak zanieczyszczać globalny zakres, wykorzystam natychmiast wywoływane wyrażenie funkcyjne (ang. immediately invoked function expression lub IIFE).
Gotowy kod wygląda zatem następująco:
Metoda define
przyjmuje dwa argumenty, z których pierwszy, to nazwa elementu, za pomocą której później będziemy się do niego odwoływać, a drugi, to utworzony przed chwilą komponent. Zauważ, że w przypadku nazwy, zastosowałem Kebab case (poszczególne słowa pisane są małymi literami i połączone za pomocą -
), ponieważ jest to wymagane przez specyfikację. Musimy również wprowadzić co najmniej dwa wyrazy.
To wszystko! Od teraz możesz zacząć wykorzystywać utworzony element w projekcie:
Metody cyklu życia
Prawdopodobnie spotkałeś lub spotkałaś się już z tym pojęciem, ponieważ lifecycle methods to bardzo popularna praktyka w niemal każdym frontendowym frameworku.
Gdyby jednak była to dla Ciebie nowość: metody cyklu życia to specjalne funkcje, dostarczane przez twórców frameworka lub języka, które wywoływane są w konkretnych stadiach istnienia komponentu. Za przykład może posłużyć nam connectedCallback
, który wywoływany jest tuż po dodaniu danego komponentu do DOM.
Najłatwiej jest je zrozumieć i poznać przez praktykę, dlatego zamiast opisywać wszystkie metody w formie listy, zastosujemy je w kodzie, a przy okazji stworzymy nowy, znacznie bardziej przydatny w życiu codziennym, komponent.
Tym razem nie będziemy skupiać się na strukturze HTML ani na stylach CSS, a nasz komponent będzie zawierać jedynie tekst. Nie wykorzystamy również tagu slot
, ponieważ nie chcemy dać użytkownikowi możliwości przekazania jakiejkolwiek wartości do środka. Dla uproszczenia kodu pominę także IIFE.
Zauważ, że w powyższej klasie nie ma jawnie utworzonego konstruktora (w takim przypadku silnik języka JavaScript utworzy za nas domyślny konstruktor), a cała logika zawarta jest w metodzie connectedCallback
wywoływanej tuż po dodaniu elementu do drzewa DOM.
Jeżeli użyjemy tego komponentu, to zobaczymy następujący rezultat:
Vue i komponenty sieciowe
Wspomniałem wcześniej, że WC mogą być używane z niemal każdym frameworkiem frontendowym. Nie sposób jednak przedstawić wszystkich możliwości w jednym artykule, dlatego musimy wybrać jedną z nich.
Zdecydowałem się na Vue ponieważ integracja z nim jest niemal niezauważalna i wymaga jedynie kilku zmian w konfiguracji projektu (w przypadku innych frameworków jest niemal równie łatwo).
Jeżeli jednak decydujemy się na wykorzystanie komponentów sieciowych w projekcie, który używa frameworka, to najczęściej robimy to, by móc je zastosować także w innych projektach.
Duże firmy nierzadko stosują architekturę projektu opartą o mikro frontendy (ang. micro frontends), co w praktyce oznacza, że aplikacja zostaje podzielona na wiele małych, niezależnych od siebie projektów, a każdy z nich może korzystać z innych bibliotek, w tym także frameworków.
W takim przypadku WC są idealnym rozwiązaniem i często stosuje się je w celu zachowania spójnego wyglądu aplikacji. Warunkiem koniecznym, by takie rozwiązanie zadziałało, jest utworzenie własnej biblioteki z komponentami (prywatnej lub publicznej, w zależności od projektu), a następnie wykorzystanie jej w miejscu docelowym. Dzięki temu wszystkie nasze aplikacje korzystają z jednego źródła.
Podsumowanie
Czy w takim razie powinniśmy przestać używać frameworków i przesiąść się na Web Components?
Zdecydowanie nie. WC nie zastępują w pełni żadnego z popularnych frameworków, ale mają swoje miejsce w świecie frontendu. Aktualnie wykorzystuje się je przede wszystkim w dużych firmach, które korzystają z wielu technologii, a komponenty sieciowe, dzięki swojej modularności i reużywalności pozwalają im na pokonywanie barier pomiędzy nimi.
Technologia ta jest stale rozwijana i może się okazać, że w przyszłości, wraz ze wzrostem popularności mikro frontendów, zyska znacznie większą popularność.