chevron-left chevron-right

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.

Web Components - wsparcie przeglądarek na dzień 29.09.2019

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ć:

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 poza this lub gdy chcemy wykonać tzw. wczesne wyjście (early return) z funkcji.
  • Nie można używać document.write() ani document.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.