Web Components – Z czym to się je i czy nadeszła ich pora?
Świat technologii webowych idzie do przodu i coś co kilka lat temu było pewnym konceptem o którym było wiadomo tylko tyle, że może się przyjmie, a może nie, nagle zaczyna wyglądać naprawdę dojrzale. Jednym z takich konceptów webowych, co do których byłem sceptycznie nastawiony kilka lat temu są Web Componenty.
To co wówczas mnie nie przekonywało do Web Componentów, jako sposobu na budowanie interfejsu aplikacji internetowych było ich słabe, natywne wsparcie w przeglądarkach (pisał o tym Comandeer swego czasu). Posiłkowanie się różnego rodzaju polyfillami czy też frameworkami dla Web Components pokroju Polymer było dla mnie niczym innym jak używaniem frameworków JavaScript w stylu Angular czy też bibliotek JavaScript w stylu ReactJS. To co mnie dodatkowo nie przekonywało, w kontekście budowania UI dla aplikacji CMS była słaba znajomość technologii wśród członków społeczności aplikacji, którą wówczas budowałem. Wtedy też nie było dużej szansy, aby byli oni w stanie szybko przyswoić wiedzę na temat mimo wszystko raczkującej technologię na tyle, aby mogli z niej korzystać efektywnie w swoich projektach. Tym sposobem postawiliśmy na ReactJS. Czy dzisiaj postawiłbym na Web Components? To zależy ..., ale o tym w dalszej części tekstu.
Taka mała dygresja, w dalszej części tekstu będę zamiennie używał określenia Web Components i Custom Elements.
Web Components - co to jest?
W skrócie, Web Components to po prostu niestandardowe tagi HTML, które można
wykorzystywać w dowolnym miejscu w kodzie na podobnej zasadzie jak elementy
typu <div></div>
czy też
<input />
.
Rozszerzając nieco definicję tego konceptu musielibyśmy dopowiedzieć kilka rzeczy. Mianowicie, Web Components jest to zestaw API przeglądarki, pozwalających na tworzenie niestandardowych elementów HTML, które zapewniają:
- Reużywalność komponentu w kodzie dowolnej strony internetowej/aplikacji internetowej,
- Enkapsulację wewnętrznego kodu HTML, co pozwala na utrzymanie tej samej funkcjonalności elementu oraz na utrzymanie takiego samego wyglądu danego elementu, niezależnie od kodu CSS na stronie docelowej.
Kluczowe funkcjonalności Web Components
Jak wspomniałem wcześniej, Web Components jest to zestaw różnego rodzaju API udostępnionych przeglądarce. Składają się na nie:
- Custom Elements
-
Możliwość tworzenia niestandardowych tagów HTML wraz ze zintegrowanymi z danym tagiem funkcjonalnościami, np:
<piotr-nalepa-menu></piotr-nalepa-menu>>
, które może być swego rodzajem menu. Takie tagi mogą mieć własne nazwy oraz można je wykorzystywać w wielu miejscach kodu HTML tak samo jak np.<div></div>
. - Shadow DOM
-
Tworzenie ukrytego drzewa DOM, na które nie można wpłynąć poprzez zewnętrzny kod JavaScript czy CSS. Ma wtedy miejsce enkapsulacja, czyli ograniczenie możliwości dostępu do wnętrza kodu komponentu. Jednakże istnieją rozwiązania aby w sposób kontrolowany manipulować wyglądem z zewnątrz komponentu.
W moim mniemaniu jest to kluczowa funkcjonalność Web Components.
- HTML Template
-
Jak sama nazwa wskazuje mamy do czynienia ze swoistego rodzaju szablonem. W tym przypadku, szablon dla Web Componentu jest ukryty za znacznikiem
<template></template>
. Nie należy jednak oczekiwać, że to API udostępnia nam funkcjonalności znane z różnych języków szablonów, takich jak Handlebars czy Twig.Tenże znacznik zapewnia jednak funkcjonalność polegającą na udostępnieniu szablonu elementu poza wyrenderowanym drzewem DOM. Dzięki temu, nie musimy śmiecić kodu HTML elementami imitującymi szablony.
Wsparcie przegląderek dla Web Components
Rozważając wykorzystanie Web Components trzeba zawsze brać pod uwagę wsparcie przeglądarek, których obsługa jest wymagana w danym projekcie nad którym pracujesz. W przypadku Web Components sytuacja implementacji API w przeglądarkach wygląda nienajgorzej jeśli chodzi o najnowsze wersje przeglądarek. W przypadku gdy trzeba wspierać starsze wersje przeglądarek, chociażby Internet Explorer 11 lub Microsoft Edge (sprzed przejścia na silnik Chromium) to sytuacja nie wygląda za różowo. Nie ma pratycznie żadnego wsparcia dla technologii Web Components, dlatego bez wsparcia tzw. polyfillami, czyli bibliotekami emulującymi brakujące funkcjonalności, się nie obejdzie.
Cykle życia Custom Elementu
Podobnie jak w React czy też w przypadku standardowej strony internetowej, każdy element przez nas utworzony ma swoje mechanizmy do obsługi cykli życia elementu.
W przypadku Custom Elementów możemy rozróżnić:
- constructor()
- connectedCallback()
- disconnectedCallback()
- attributeChangedCallback(name, oldValue, newValue)
- adoptedCallback()
Każdy z tych mechanizmów ma swój konkretny cel i obowiązki. Ich zastosowanie opisałem poniżej:
constructor()
Kiedy tworzymy klasy JS to constructor()
może być domyślnie
pominięty. W przypadku Web Components, jest on bardzo pożyteczny ze względu na
fakt, że to w nim możemy aktywować ShadowDOM, podpiąć
event listenery czy też zainicjalizować wewnętrzny stan elementu.
W przypadku implementacji kodu w constructor()
należy się
kierować kilkoma zasadami:
-
Pierwsza linijka kodu konstruktora powinna zawierać tylko i
wyłącznie kod:
super()
, aby być pewnym że element odziedziczy poprawny łańcuch dziedziczenia (prototype chain) oraz wszystkie własności i metody z klasy po której element dziedziczy. -
nie można zwracać za pomocą
return
żadnych wartości pozathis
lub gdy chcemy wykonać tzw. wczesne wyjście (early return) z funkcji. -
Nie można używać
document.write()
anidocument.open()
; - Atrybuty i wewnętrzny kod HTML elementu nie powinny być używane, ponieważ jeszcze nie zostały zaimplementowane w drzewie DOM dokumentu strony.
connectedCallback()
Metoda ta obsługuje moment wstawienia elementu do drzewa DOM strony. To jest dobry moment, aby uruchomić kod pobierający dane z zewnętrznych serwerów czy też kod odpowiedzialny za wyszukiwanie elementów wewnątrz szablonu Custom Elementu.
disconnectedCallback()
Tą metodą obsługujemy moment w którym element zostanie usunięty z drzewa DOM.
Ważne jest to, aby pamiętać o usunięciu obsługi zdarzeń jeśli zostały
zaimplementowane na elementach znajdujących się poza strukturą Custom
Elementu, na przykład na elemencie <body>
.
attributeChangedCallback(name, oldValue, newValue)
Jeśli chcemy reagować na zmiany wartości atrybutów zdefiniowanych w Custom Elemencie to w tej metodzie możemy zaimplementować odpowiedni kod. To co jest ważne, to fakt, że zmiany będą nasłuchiwane tylko w atrybutach określonych za pomocą statycznej implementacji metody:
1 2 3 | static get observedAttributes() { return ['hidden']; } |
Zabezpiecza to przed zbyt częstym uruchamianiem metody
attributeChangedCallback(...)
, co poprawia wydajność kodu.
Reagujemy na zmiany tylko i wyłącznie wybranych atrybutów elementu.
adoptedCallback()
Jest to specyficzna metoda obsługująca przypadek gdy dany element zostanie
niejako
przeniesiony między różnymi dokumentami, np. przeniesiony ze strony pierwotnej do strony wyświetlonej w
<iframe>
. Dotychczas nie miałem okazji z tej metody
korzystać.
Tworzymy pierwszy Custom Element
Mając teorię za sobą możemy przejść do tworzenia własnego
Web Componentu. W tym celu utworzymy nowy plik o nazwie:
nowy-element.js
i wewnątrz niego wstawimy następujący kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | (function() { const template = document.createElement('template'); // Definiujemy szablon Web Componentu. // Wewnątrz szablonu znajdują się również style dla komponentu // Tym samym style nie są dziedziczone przez elementy // i zapewniamy ich nienaruszalność. template.innerHTML = ` <style> :host { display: block; } *, *:after, *:before { box-sizing: border-box; } p { padding: 8px 16px; color: #fff; background: #b30; font-weight: bold; } </style> <p> <slot>To jest nowy element typu: nowy-element</slot> </p>`; class NowyElement extends HTMLElement { constructor() { // dziedziczymy wszystkie metody // i własności po HTMLElement super(); // inicjalizujemy ShadowDOM this.attachShadow({ mode: 'open' }); // wstawiamy kod HTML zdefiniowany w szablonie do wnętrza komponentu this.shadowRoot.appendChild(template.content.cloneNode(true)); } static get observedAttributes() { return ['hidden']; } connectedCallback() { this.shadowRoot.addEventListener('click', this.hide, false); this._refContainer = this.shadowRoot.querySelector('p'); document.body.addEventListener( 'click', this.detectClickOutside, false ); if (this.hasAttribute('hidden')) { this._refContainer.setAttribute('hidden', true); } } disconnectedCallback() { document.body.removeEventListener('click', this.detectClickOutside); } attributeChangedCallback(name, oldVal, newVal) { if (name === 'hidden' && newVal === 'true') { this._refContainer.setAttribute('hidden', true); } else if (name === 'hidden' && newVal === 'false') { this._refContainer.removeAttribute('hidden'); } } // wykrywamy kliknięcie za naszym elementem // jeśli nie kliknięto w nasz element // to pokazujemy element ponownie detectClickOutside = event => { if (event.target !== this) { this._refContainer.removeAttribute('hidden'); } }; // ukrywamy element // oraz wywołujemy własny event o nazwie `hidden` hide = () => { this._refContainer.setAttribute('hidden', true); this.dispatchEvent( new CustomEvent('hidden', { bubbles: true, cancelable: false, // własność `composed` jest bardzo ważna // bez niej zdarzenie nie przedostanie się // poza wnętrze elementu composed: true, detail: {} }) ); }; } window.customElements.define('nowy-element', NowyElement); })(); |
Tak utworzony kod możemy załadować w dowolnej stronie HTML poprzez standardowe
ładowanie skryptów JavaScript:
<script src="nowy-element.js" async defer></script>
a
następnie poprzez wstawienie odpowiedniego znacznika HTML w kodzie strony:
<nowy-element></nowy-element>
.
Jeśli chcemy to możemy dodać atrybut hidden
do elementu:
<nowy-element hidden="true"></nowy-element>
tym samym
będzie on domyślnie ukryty. Lecz jeśli kliknięmy w dowolny obszar poza
obszarem elementu to nasz <nowy-element />
ujawni swoje
oblicze.
Wstawianie dodatkowej treści wewnątrz elementu
Jak pewnie udało Ci się zauważyć, w szablonie
<nowy-element />
wstawiłem znacznik
<slot></slot>
, tym samym określiłem miejsce w którym
pojawi się dowolny kod HTML wstawiony wewnątrz przez nas zdefiniowanego
znacznika. Przeanalizujmy następujący kawałek kodu:
<nowy-element><p>Jakiś zupełnie inny
tekst</p></nowy-element>
.
Utworzyliśmy nowy element typu nowy-element a następnie wewnątrz
niego wstawiliśmy paragraf z tekstem. To co się teraz stanie, to zastąpi on
zawartość znacznika <slot />
zdefiniowanego w szablonie Web
Componentu. Czyli zamiast
To jest nowy element typu: nowy-element
w slocie pojawi się
<p>Jakiś zupełnie inny tekst</p>
.
To co jest istotne, to fakt że możemy posiadać wiele slotów wewnątrz szablonu Web Componentu. To co jednak musimy zrobić to zdefiniować identyfikatory slotów, dzięki czemu programiście korzystającemu z naszego kodu będzie łatwo określić co ma się gdzie pojawić wewnątrz Custom Elementu, bez potrzeby ingerencji w jego szablon.
W celu udostępnienia wielu slotów na dowolny kod HTML wewnątrz elementu musimy
nadać slotom identyfikator:
<slot name="title"></slot>
a następnie wykorzystać go
podczas implementacji znacznika nowy-element w następujący sposób:
<nowy-element><p slot="title">Dowolny
tytuł</p></nowy-element>
. W ten sposób kod HTML będzie wyrenderowany w odpowiednim miejscu.
Zatem czy już nadeszła pora dla wykorzystania Web Components?
Bazując na tych wszystkich informacjach, które wymieniłem wcześniej można już w jakimś stopniu pokusić się o odpowiedź. Moja odpowiedź to: To zależy.
Zależy to od wielu czynników i celu biznesowego danego projektu. Jeśli zależy nam na wsparciu społeczności, która nie ma silnie rozwiniętych umiejętności JavaScript to pójście w Web Components będzie problematycznym rozwiązaniem. Jako twórcy projektów Open Source nie możemy mieć pewności jakie rozwiązania i przeglądarki są wspierane przez ich projekty bazujące na naszym. Dodatkowo, brak polecanych rozwiązań, dobrych praktyk tak jak to ma miejsce w Angular czy React może być dodatkową barierą wejścia w technologię.
W przypadku, gdy tworzymy projekt w którym będziemy się skupiać tylko i wyłącznie na najnowszych wersjach przeglądarek oraz nie będziemy mieć do czynienia ze społecznością Open Source, która chce wykorzystywać nasz projekt jako część ich różnych projektów, to nie widzę przeciwskazań aby pójść w Web Components natywnie, bądź przy pomocy frameworków stworzonych specjalnie dla Web Components takich jak: Polymer, Stencil czy innych.
To co jest ważne, to tak dobierać technologie, aby w przyszłości nie spotkać się z granicą polegającą na przykład na braku wykwalifikowanych rąk do pracy w danym projekcie.
Ja ze swojej strony poszedłbym teraz w integrację Web Components z różnymi innymi technologiami. Nie zamykałbym się na jeden wybrany sposób kodowania, tylko elastycznie podchodziłbym do wymagań projektowych bazując na swojej wiedzy oraz obecnej sytuacji kadrowej w projekcie.
Podsumowanie
Mam nadzieję, że udało mi się Ciebie zaciekawić temat Web Components. Jest to fascynująca technologia, która jeszcze cierpi na brak większej popularności oraz brak większego wsparcia w szerszym spektrum przeglądarek. Niemniej jednak, jest to technologia przyszłości. Jestem w stanie sobie wyobrazić, że za kilka lat coraz więcej projektów będzie z nich korzystać. Póki co, wszyscy się uczymy stosowania Web Components, a niektórzy nawet już zdecydowali wypuścić nowe wersje swoich projektów bazujących na Web Components.