Wakacje! Słońce, wysoka temperatura, półnagie kobiety na plaży… Meh. To najlepszy czas, żeby zaszyć się w swojej jaskinii programisty i wyhodować jakiegoś kodowego potworka. BUHAHAHAHA!
A ci z nas, którzy nie cierpią na ostre postacie agorafobii, z chęcią lubią prezentować innym efekty swej syzyfowej pracy. Ja również należę do tych ekshibicjonistów, dlatego też postanowiłem pochwalić się ideą (ok, istnieje coś w wersji mocno prealpha, aczkolwiek używalnej ;)) rodzącą się w bólach w mym umyśle. Ale od początku!
Na początku było Słowo…
…a kilkaset milionów lat później inteligentny zlepek komórek stworzył coś, co buńczucznie nazwał HTML-em. Dzięki temu wynalazkowi dzisiaj możemy oglądać słodkie kotki! Jest on także sponsorem dzisiejszego artykułu.
Któż nie zna tego wręcz prymitywnego w swej budowie języka znaczników? Wszyscy mamy z nim do czynienia na co dzień – wszak to język, w którym do nas przemawia Sieć. Lecz czy kiedykolwiek zastanawialiście się jaka to magia kryje się za tymi niepozornymi <tagami>. Na pewno nie! No bo cóż w tym takiego magicznego?
A jeśli powiem, że w tym prostym języku tkwi niesamowity potencjał, który może odmienić oblicze Sieci, naszej Sieci? „Oszalał, chce powrotu XHTML 2!” – zakrzyknie co bardziej rozgarnięty czytelnik. Sam bym za takie myśli dał sobie w pysk jakieś 2 lata temu… Ale o tamtego czasu zmieniło się bardzo dużo, by nie powiedzieć: wszystko!
Declaratives, declaratives everywhere!
Nie oszukujmy się: HTML nie powstał jako język dla zaawansowanych aplikacji. To język dla dokumentów – header nie ma nic wspólnego z titlebarem aplikacji a [data-action] nie są w stanie zastąpić sensownego data binding. Nic zatem dziwnego, że powstały całe potężne frameworki JS mające te braki naprawić: poczynając od standardowego, tradycyjnego i lekko trącego dziś myszką jQuery UI, przechodząc obok potężnego i nielubianego przeze mnie, przerośniętego ext.js na najnowszym dziecku Google, Angularze ze swoim sytemem modułów i nie do końca walidującym się HTML-em, kończąc. Żeby zrobić proste okienko z komunikatem (ok, w HTML 5 mamy dialog, który działa aż w Chrome 37+) ładujemy tonę JS-a, zamiast… umieścić odpowiedni kod HTML na stronie. Powiedzcie sami – czy nie ładniej zamiast
1 2 3 4 | new Dialog({ content: 'whatever' ,modal: true }); |
użyć po prostu:
1 | <dialog modal>Whatever</dialog |
Nie jest to jakiś nowy pomysł – w HTML de facto od zarania dziejów (tzn od powstania JS ;)) takie rzeczy istnieją. Najlepszym przykładem tego jest new Image, który równocześnie jest konstruktorem dla elementu img (a raczej – każdy tag img na stronie jest równocześnie obiektem Image w JS) czy też new Audio i tag audio. Oto mamy mały i poręczny element HTML, pod którym kryje się potężne JS-owe API. Jak bardzo jest to myślenie naturalne, wskazują dzieje Event Source API (aka Server Sent Events): jego pierwszą wersją był… tag eventsource, który nawet doczekał się implementacji w jednej z wersji Opery.
Ci, którzy znają XML, są całkowicie przyzwyczajeni do własnych tagów. W HTML jednak przez długie lata taka rzecz była niedostępna. Owszem, na upartego można było stworzyć sobie nowy tag, typu comandeertoboss, ale na tym nasze możliwości się kończyły. Nie było żadnego sensownego sposobu na stworzenie zależności, jak w parze new Image ↔ img. Co więcej, niektóre przeglądarki (tak, na ciebie patrzę, IE; na ciebie również, stary lisku) miały dziwne problemy ze stylowaniem tych elementów (IE potrzebował czegoś na kształt HTML5 shiv a lisek… serwowania jako XHTML). O problemach w DOM już nawet nie wspominam. Nie dało się i już. „Semantyczny” kod (tzn. taki, który po prostu dałoby się czytać) trzeba było osiągać na inne sposoby – tutaj ważne role pełniły (i wciąż pełnią!) mikroformaty oraz architektura BEM. XML-owy raj (gosh…) własnych znaczników był poza zasięgiem webdeveloperów. Do czasu…
Web Components are coming!
Choć aplikacje internetowe różnią się diametralnie od siebie (FB nijak się ma do Muro), pod maską tak naprawdę niewiele się różnią. Wszystkie stosują potężne frameworki JS do tworzenia interfejsów użytkownika. Takie rozwiązanie musi:
- Udostępniać sensowny sposób tworzenia nowych elementów GUI
- Posiadać system szablonów
- Umożliwiać całkowitą enkapsulację (interfejs ma brać dane i tyle – co leży pod spodem, powinno być ukryte)
- Być reużywalne (czyli można je zaimportować na każdej stronie WWW, która tego potrzebuje)
- Posiadać system 2-way data binding lub posiadać sensowny sposób informowania o zmianach (system eventów/pub-sub)
Tylko patrząc na tą listę, każdy logicznie myślący człowiek widzi, że problemy te można rozwiązać na wiele różnych sposobów (stąd tak niesamowita liczba dostępnych frameworków JS). A jeśli powiem, że obecnie istnieje natywny mechanizm, który rozwiązuje te wszystkie problemy?
Uśmiechasz się ironicznie, prawda? Nie wierzysz mi a to już działa (tzn nie do końca, ale o tym później) i nazywa się „Web Components”. Jak sama nazwa wskazuje, składa się to z kilku elementów:
- Custom elements – czyli sensowny sposób tworzenia nowych elementów GUI
- Tag template – czyli sensowny system szablonów
- Shadow DOM – czyli całkowita enkapsulacja
- link[rel=import] – czyli całkowita reużywalność
- Custom events – czyli sensowny sposób informowania o zmianach
- Object.observe/DOM Mutation Observer – czyli 2-way data binding
Brzmi tak pięknie, że aż musi być haczyk, prawda? Otóż… jest, taki jak zawsze w naszym dobrym, starym webdevelopingu: kompatybilność, którą na bieżąco można sprawdzić na stronie Are We Componentized Yet?. Ale jak to zwykle bywa, istnieją polyfille. Najpopularniejsze to Polymer (którego używam i który jest o wiele bardziej rozbudowany i oprócz wspomnianych wyżej rzeczy dostajemy w prezencie Pointer Events; „minusem” jest brak wsparcia dla IE9-, ale patrząc na ranking.pl to wcale nie jest minus ;)) i X-Tags (mniej rozbudowany, skupiający się przede wszystkim na custom elements, ale za to dostajemy obsługę dla IE9+). Natywnie wszystkie te rzeczy, jak widać, działają sensownie jedynie w Chrome 36+ (no i w Operze ;)), natomiast Firefox zostaje ciut z tyłu; Safari i IE przemilczę.
Skoro już wiemy co i gdzie (nie) działa, przyjrzyjmy się bliżej wszystkim tym elementom po kolei (nie liczcie na duży tutorial – nie chce mi się ;) poza tym napisano już doskonałe – oczywiście po angielsku – do których linki podam na końcu; ten artykuł potraktujcie jako dobry overview całej technologii).
Custom elements
To najczęściej wykorzystywany element (pun intented) Web Components i ich znak rozpoznawczy. Bez tego elementu (pun not intented) cała reszta nie ma żadnego sensu. Zatem w skrócie: tak, możesz tworzyć własne elementy HTML!
I zanim polecisz tworzyć comandeertoboss, wiadro zimnej wody na ochłodę: to nie jest aż tak proste! Custom element wygląda bowiem ciut inaczej niż zwykły element:
1 | <tag-name></tag-name> |
Jak widać, nazwa składa się z dwóch części, przedzielonych myślnikiem. Ten myślnik jest konieczny. Dlaczego? Gdyż przeglądarka inaczej traktuje elementy bez niego i z nim. Bez myślnika przeglądarka po prostu uznaje to za nieznany, niepoprawny element… i na tym się kończy. Natomiast gdy w nazwie myślnik występuje, przeglądarka zaznacza sobie ten element jako „prawdopodobnie custom element”.
„Do rzeczy – my chcemy pełnoprawnego custom elementa!” – zakrzykną webdeveloperzy, nielegalnie czytający ten artykuł w pracy. A więc – żeby przeglądarka rozpoznawała nasz tag jako… nasz tag ;) należy użyć funkcji document.registerElement:
1 2 3 | var TagName = document.registerElement('tag-name', { prototype: TagNamePrototype }); |
Pozwoliłem sobie zapisać wynik funkcji do globalnego konstruktora TagName (dzięki temu mamy swój własny new Image – yay!). Jak widać, document.registerElement bierze jako 2. paremetr obiekt opcji, a w nim – prototyp naszego elementu. Tutaj ważne jest to, żeby obiekt TagNamePrototype dziedziczył po HTMLElement (albo jego pochodnych). Drugą opcją, obok prototype, jest extends – dzięki temu można rozszerzać już istniejące elementy; używa się ich jednak ciut inaczej:
1 | <button is="mega-button">Nasz mega button</button> |
Bardziej doświadczeni czytelnicy na pewno od razu zapytają czemu jest bezsensowny myślnik, skoro można to było rozwiązać za pomocą XML namespaces? Otóż: XML jest martwy ;) XHTML się nigdy nie przyjął, zatem naturalnym ruchem było wykorzystanie o wiele prostszego sposobu i to kompatybilnego ze starym HTML (tzw. bożek Backwards Compatibility).
W Sieci można znaleźć pełno custom elements, od bezsensownych po całkiem użyteczne.
template
Każda większa aplikacja webowa korzysta z systemu szablonów – nic w tym dziwnego. Jeśli kilka różnych części GUI korzysta z tego samego rozwiązania, to można to wyrzucić do szablonu i jedynie podmieniać odpowiednie dane. Dotąd, z powodu ograniczeń JS, przeważająca część systemów szablonów client-side operowała na stringach. template przenosi szablony na poziom DOM. To tak naprawdę nic innego, jak osobne drzewko DOM na naszej stronie. Co więcej – całkowicie nieaktywne. Nie musimy się martwić o to, że obrazki doczytają się bez potrzeby – zawartość template uaktywnia się dopiero po umieszczeniu wewnątrz strony. Do tego czasu wszystko w nim pozostaje nienaruszone. A jak szablonów używać?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <template id="image"> <img src="" alt=""> </template> <div id="image-container"></div> <script>var template = document.getElementById('image').content //pobieramy drzewko DOM szablonu (można zrobić jego klona, żeby zostawić oryginał w spokoju) ,img = template.querySelector('img'); //z tego drzewka pobieramy obrazek //ustalamy atrybuty obrazka img.src = '/cos.png'; img.alt = 'Coś tam'; var parsed = document.importNode(template, true); //importujemy do dokumentu document.getElementById('image-container').appendChild(parsed); //i używamy na naszym elemencie</script> |
Mogłoby być prostsze, ale i tak nie jest źle. A mamy za to natywny system szablonów!
Shadow DOM
Kto wie jak wygląda drzewko DOM dla elementu input? Pewnie większość z Was popuka się w czoło i stwierdzi, że zwariowałem – jakie drzewko? A to już Wam pokazuję jakie (wyciągnięte z Chrome):
1 2 3 4 5 6 7 | <input id="input"> | | #shadow-root (user agent) | | <div id="innereditor">tekst z pola</div> |
Zszokowani? I właśnie o to chodzi! My, jako userzy, widzimy tylko efekt końcowy – pole formularza. Cała reszta siedzi pod spodem i jest obsługiwana przez przeglądarkę. Polecam spojrzeć jak bardzo złożony pod spodem jest np tag video (na pewno ma więcej kodu niż aplikacje niejednego z nas ;)). I to jest właśnie Shadow DOM – magiczny worek, ukrywający całą implementację pod ładnie wyglądającym tagiem. Do niedawna taką moc mieli jedynie twórcy przeglądarek. Jednak Shadow DOM zostało ustandaryzowane (prawie – to najczęściej zmieniająca się specyfikacja W3C ;)) i obecnie każdy z nas może tworzyć rzeczy podobne do pola input. Jak?
1 2 3 4 | <p></p> <script>var elem = document.querySelector('p') ,shadow = elem.createShadowRoot(); shadow.textContent = 'Tekścik';</script> |
Tym sposobem stworzyliśmy akapit z niewidzialnym tekstem (sympatyczny kod? ;)) – nie ma go w źródle strony a inspektor elementów też niekoniecznie go pokazuje (dopóki nie zaznaczym odpowiedniej opcji w dev-tools). Oczywiście nic nie stoi na przeszkodzie, żeby włożyć tam coś bardziej skomplikowanego (Shadow DOM bardzo lubi tag template ;)) czy nawet… kolejne cieniste drzewko (because we always must be able to go deeper…).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <template id="image"> <img src="" alt=""> </template> <div id="image-container"></div> <script>var template = document.getElementById('image').content ,img = template.querySelector('img') ,container = document.getElementById('image-container') ,shadow = container.createShadowRoot(); img.src = '/cos.png'; img.alt = 'Coś tam'; var parsed = document.importNode(template, true); shadow.appendChild(parsed);</script> |
Zalety? Całkowita enkapsulacja! Co więcej – Shadow DOM można podpiąć pod accessibility API (czyli czytniki ekranowe to widzą) i wykorzystywać na nim ARIA. Niemniej jest to sposób na rozdzielenie prezentacji naszego custom elementu od jego treści – treść podajemy normalnie w tagu (jak zawsze), natomiast cały sposób prezentacji zamykamy w Shadow DOM (a obsługa treści w Shadow DOM jest banalna, bo wystarczy w odpowiednim miejscu wstawić tag content). Proste i skuteczne.
link[rel=import]
Największa przewaga GUI w JS nad GUI w HTML? Możliwość importu tylko tych rzeczy, których potrzebujemy, w prosty, asynchroniczny i przyjemny sposób (patrz: AMD, CJS, UMD, ES6 Modules). W HTML takiej możliwości nie było… do niedawna. Od niedawna „moduły HTML” są jeszcze prostsze do wykorzystania niźli moduły JS (i mówi Wam to wielki fanatyk JS-a!). Wyobraźmy sobie, że stworzyliśmy super wypasiony custom element, z Shadow DOM, szablonami i całym potężnym API… I mamy problem: jak go załączać na wszystkich naszych stronach? 1. myśl – inline’ujemy – odpada w przedbiegach: 1000 linijek HTML-a to jednak nie to, co tygryski lubią najbardziej. 2. myśl – AJAX i wkładanie odpowiednich zasobów w odpowiednie miejsca. Działa a my mamy asynchroniczny lazy loading… Niemniej wymaga to ciut pracy i nie zawsze musi działać.
Zatem jesteśmy pokonani? Oczywiście, że nie! Z pomocą nadchodzi link[rel=import]! Pozwala on wydzielić z naszego kodu „moduł HTML” (zatem custom element oraz to wszystko, czego potrzebuje do działania) i dołączać go do wszystkich innych dokumentów (skoro nasze API przepisaliśmy na tag, to czemu nie mamy załączać go dzięki tagowi?). Wyobraźmy sobie kod naszego wypasionego custom elementu:
1 2 3 | <link rel="stylesheet" href="whatever.css"> <template>[…]</template> <script src="/jquery/of/course.js"></script> |
Po pewnym czasie może się to bardzo rozrosnąć… Dlatego wystarczy cały ten kod przerzucić do osobnego pliku .html i importować:
1 | <link rel="import" href="naszwypasionycustomelement.html"> |
I tyle! Do całej zawartości zaimportowanego pliku dostaniemy się przez własność import owego link[rel=import] (tak, jak w przypadku template, tutaj również otrzymamy drzewko DOM). Tym prostym sposobem stworzyliśmy nasz 1. „moduł HTML”. Czekam z niecierpliwością kiedy Facebook i inne portale będą tak udostępniać swoje widgety.
Custom events
Każdy szanujący się custom element powinien rzucać jakieś zdarzenia. A czy jest jakiś lepszy sposób od użycia tutaj custom events?
1 2 3 4 5 6 7 8 9 10 11 12 | document.addEventListener('naszEvent', function(e) { console.log(e.detail.custom); }); var event = new CustomEvent('naszEvent', { detail: { custom: 'dodatkowe dane' } }); document.dispatchEvent(event); |
Jak widać, można nadać zdarzeniu dowolną nazwę. Jedynym ograniczeniem jest to, że wszystkie dodatkowe dane zdarzenia są przekazywane jako event.detail. Niemniej jest to potężny DOM-owy element, pozwalający kontrolować działanie elementu (no przecież każdy z nas doskonale to wie! :D).
Object.observe/DOM Mutation Observer
Data binding w JS? Object.observe – służy do obserwowania zmian zachodzących w danym obiekcie JS i w chwili jakiejkolwiek zmiany, wykonuje daną czynność.
1 2 3 4 5 6 7 8 9 10 11 12 13 | var o = { name: 'costam' }; Object.observe(o, function(changes) { changes.forEach(function(change) { console.log(change); }); }); o.name = 'inne coś'; |
Coś pięknego prawda? Jeśli chodzi o same DOM, tutaj tę rolę pełni Mutation Observer, którego używanie już tak przyjemne nie jest, dlatego odeślę zainteresowanych do MDN ;)
Co prawda same te mechanizmy nie tworzą nam 2-way data binding out of box, ale wystarczy kilka minut, żeby takie rozwiązanie na ich podstawie przygotować. Jeśli chodzi o ES5 – tutaj można się ładnie pobawić przy pomocy getterów/setterów i eventów, by uzyskać podobne rozwiązanie.
Co dalej?
Jaki obecnie jest najlepszy sposób na tworzenie własnych custom elements? Użycie custom elementu polymer-element!
Wszystkich zainteresowanych tym rozwiązaniem zapraszam do zapoznania się z projektem untitled-element, który jest bardzo ciekawym wrapperem dla polymer-element, generującym automatycznie dokumentację dla naszego custom elementu oraz używającym Bowera jako systemu rozprowadzania (Polymer stworzył niebotyczny ekosystem i de facto „czyste” custom elements są w zdecydowanej mniejszości).
Osobiście sam używam untitled-element, który jest podstawą mojego projektu dGUI (declarative GUI) – na razie są dwa nieskończone elementy, ale na dysku już mam zalążki kolejnych custom elementów, które w ostateczności złożą się na rozbudowane GUI aplikacji a’la desktopowej i nie tylko (node-webkit, yay!).
Większość devów oszalała na punkcie Web Components i nic dziwnego. Przepisano już połowę Sieci na nie: routery, asynchroniczne formularze, przycisk FB a nawet… console.log. Czy Web Components się przyjmą? Już to zrobiły. Czy ludzie je polubią? Już z nimi sypiają! Czy to dobrze? Tak i nie… Możliwości są bardzo duże, a wraz z nimi przyszła wielka odpowiedzialność (ale o niej to może innym razem!). Niemniej dobrze użyte WebComponents są przyszłością aplikacji webowych!
Linki i inspiracja:
- http://www.html5rocks.com/en/tutorials/webcomponents/customelements/
- http://www.html5rocks.com/en/tutorials/webcomponents/template/
- http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom/
- http://www.html5rocks.com/en/tutorials/webcomponents/imports/
Image courtesy of twobee at FreeDigitalPhotos.net
Tagi: JavaScript • promowany • Web Components
Robert
szukałem przewijania w dół strony " jak to sie nazywa", ale Progress bar takze dodam do siebie.2023-07-15 23:55:00