Komponenty - podstawowa składowa współczesnych interfejsów użytkownika

Komponenty - podstawowa składowa współczesnych interfejsów użytkownika

Jeśli programista zajmujący się w dzisiejszych czasach front-endem zostałby zapytany o to co łączy popularne obecnie biblioteki i frameworki służące do budowania interfejsu użytkownika, to wskazałby zapewne na komponenty. Nie wszyscy wiedzą jednak czym właściwie komponenty są oraz co dobrego wnoszą do życia twórców aplikacji webowych. Aby poznać odpowiedzi na tego typu pytania przyjrzyjmy się frameworkowi Angular, którego rozwój daje nam pogląd na to w którą stronę zmierza współczesny front-end.

Zaczęło się niepozornie

Jeszcze kilka lat temu światem web developmentu rządziła prosta, niepodważalna reguła – część kliencka aplikacji webowej powinna być tak prosta jak to możliwe, a każda bardziej skomplikowana operacja powinna być delegowana do serwera. Taki stan rzeczy był spowodowany m.in. ówczesnymi ograniczeniami sprzętowymi po stronie klienta oraz ubogimi możliwościami przeglądarek internetowych tamtych czasów.

Kiedy w 2010. jeden z inżynierów firmy Google, niejaki Misko Hevery, prezentował światu pierwsze wersje narzędzia przedstawianego wtedy jako <angular />, świat odpowiedział umiarkowanym entuzjazmem. Słuchacze jego prezentacji martwili się raczej o kompatybilność rozwiązania z istniejącymi na rynku przeglądarkami, a nie o możliwość budowania zaawansowanych interfejsów z ładowanymi asynchronicznie komponentami. Czas jednak mijał, a obietnica łatwego budowania dynamicznych aplikacji webowych zyskiwała kolejnych zwolenników.

Czym w tamtym okresie wyróżniał się Angular? Kiedy większość dostępnych na rynku bibliotek skupiała się na wyciskaniu maksimum z imperatywnych manipulacji przeglądarkowym DOMem (jQuery), albo ukrywania znanego dla programistów środowiska opartego o HTML (ExtJS), Angular starał się po prostu… rozszerzać możliwości przeglądarki. „Tak wyglądałby HTML, gdyby został zaprojektowany do tworzenia aplikacji webowych” – brzmiało jedno z pierwszych haseł mających przekonać programistów do Angulara.

Programista chcący stworzyć dynamiczną aplikację nie potrzebował zbyt wiele. W powyższym fragmencie kodu widać, że poza dwoma atrybutami z prefixem ng- oraz wąsatymi nawiasami w tagu p, reszta to najzwyklejszy HTML. W ten sposób można podsumować całą filozofię pierwszych wersji Angulara – pozostać tak blisko znanych sposobów na pisanie strony WWW jak to możliwe, przy jednoczesnym dostarczeniu szerokiej palety gotowych do wykorzystania funkcjonalności.

Jak budować interfejs użytkownika?

Interfejs użytkownika oparty o losowo porozrzucane po stronie atrybuty nie wytrzyma próby czasu – kod z biegiem czasu stanie się po prostu niemożliwy do utrzymania. Ale czy potrzebujemy czegoś więcej? Zdecydowanie tak – budowanie interfejsu użytkownika nie różni się zbyt wiele od tworzenia kodu w innej warstwie aplikacji. Tak samo jak na back-endzie, front-end powinien wykorzystywać moduły o małej, ściśle określonej odpowiedzialności, które dodatkowo mogłyby być reużywalne i możliwe do wymiany bez wpływania na cały projekt. Co w tej kwestii miał do zaoferowania Angular?

Przez kilka pierwszych wydań tego frameworka (Angular.JS, czyli 1.x) sztandarowym podejściem do budowania aplikacji było jej pisanie zgodnie z czymś co przypominało wzorzec MVC. Modelem mógł być dowolny obiekt zawierający dane, natomiast na styku „V” oraz „C” rządził kontroler sterujący widokiem.

Implementacja przykładowego kontrolera mogła wyglądać następująco:

Mając zadeklarowany kontroler, mogliśmy korzystać z jego właściwości w widokach:

Kontrolery enkapsulowały funkcjonalność danego fragmentu strony, a tym, co łączyło świat JavaScriptu z widokiem w HTMLu był obiekt $scope który spełniał rolę czegoś na wzór view modelu. Był to model zawierający wszystkie niezbędne dane wymagane do wyrenderowania określonego fragmentu naszej aplikacji.

Dwa powyższe fragmenty kodu nie wydają się być czymś szczególnie skomplikowanym – tworzymy kontroler, do którego wstrzykujemy obiekt $scope, ten z kolei wypełniamy danymi, a danych finalnie używamy w widoku. Przynajmniej tak mówiła teoria. W praktyce nie wyglądało to tak kolorowo – głównym problemem takiego podejścia był fakt, że często w miarę wzrostu rozmiaru projektu kontrolery rozrastały się do rozmiarów, których utrzymanie było bardzo trudne.

Co prawda dokumentacja na stronie Angulara niemal od samego początku mówiła, żeby logikę biznesową naszej aplikacji delegować do zewnętrznych modułów i serwisów, jednak nic nie stało na przeszkodzie, żeby robić „po swojemu”. Kontrolery miały jeszcze jedną wadę, którą był (aż do wprowadzenia wersji 1.5) brak jasno zdefiniowanego cyklu ich życia. W trakcie swojej pracy w projektach opierających się o Angular.JS widziałem kilkanaście różnych implementacji funkcji init, initialize czy jakichkolwiek innych, które miały być miejscem, w którym zainicjalizujemy nasz model czy pobierzemy niezbędne dane z zewnętrznych serwisów. Swoboda, którą Angular zostawił programistom okazywała się po czasie przynosić więcej szkód niż korzyści. Koniec końców na kontrolerach świat się jednak nie kończył.

Angular.JS wprowadził jeszcze jeden sposób na tworzenie dynamicznych interfejsów użytkownika – tym sposobem są dyrektywy. W dokumentacji czytamy, że „dyrektywy są sposobem na nauczenie HTMLa nowych tricków” ale dla nas to po prostu pewnego rodzaju znaczniki dzięki którym najpierw w kodzie HTML możemy oznaczyć element, którego wygląd lub zachowanie chcemy modyfikować, a potem w kodzie JS określić czym ta nowa funkcjonalność ma być.

Dzięki dyrektywom możemy np. wchodzić w bezpośrednią interakcję z DOMem, modyfikować i rozszerzać działanie istniejących elementów na stronie a także… budować reużywalne komponenty. W kontekście dzisiejszego tematu to właśnie ta ostatnia możliwość interesuje nas szczególnie. Jak to wygląda w praktyce? Zaczyna się od JavaScriptu…

…a kończy na widoku:

W pierwszym z dwóch fragmentów kodu widzimy przykład tzw. directive definition object, czyli obiektu konfiguracyjnego dla nowej dyrektywy. Pole restrict dotyczy sposobu deklaracji dyrektywy (w naszym przypadku E, czyli element), scope dotyczy parametrów z jakimi możemy ją zainicjalizować, a template to szablon (widok). W kolejnym fragmencie widzimy z kolei, jak posługiwać się dyrektywą z poziomu HTMLa.

Dzięki dyrektywom zyskujemy praktycznie nieograniczone możliwości związane z tworzeniem elementów nieistniejących w standardzie HTML, co koniec końców przekłada się na tworzenie bardziej deklaratywnego i wysokopoziomowego kodu interfejsu użytkownika (<avatar /> zamiast <img />, <dashboard /> zamiast <div />, itd.). Czy tak użyteczne rozwiązanie jest więc tym, na czym Angular postanowił się zatrzymać?

Nowe czasy, nowe rozwiązania

Dyrektywy, pomimo bycia jedną z najważniejszych funkcjonalności frameworka Angular.JS, nie są rozwiązaniem bez wad. Krytykuje się je m.in. za skomplikowaną konfigurację oraz stosunkowo wysoki próg wejścia dla osób początkujących. W zasadzie nie ma się czemu dziwić – istnieją przykładowo aż cztery możliwe sposoby na zadeklarowanie dyrektywy po stronie kodu HTML (jako tag, klasa, atrybut i… komentarz) – które rozwiązanie jest w takim razie tym właściwym? A może wszystkie są tak samo dobre? No i co z kontrolerami o których pisałem kilka akapitów wcześniej?

Zespół odpowiedzialny za Angulara 2.0 (teraz nazywanego po prostu Angularem) był świadomy problemów które wymieniłem, dlatego w 2014r. zdecydował się na dość radykalny krok – zrywamy ze starymi przyzwyczajeniami na rzecz podejścia component-first. Rezygnujemy z kontrolerów, rezygnujemy z obiektu $scope, rezygnujemy ze skomplikowanych obiektów konfiguracyjnych (DDO). W nowym podejściu stawiamy przede wszystkim na komponenty. Ale czym te komponenty właściwie są?

Komponent to podstawowa składowa interfejsu użytkownika. W Angularze reprezentowana jest ona przez klasę posiadającą kilka charakterystycznych elementów:

  • Stan – stan komponentu w najprostszej formie przechowujemy tak jak stan instancji obiektu danej klasy, czyli po prostu we właściwościach (properties)
  • Cykl życia – funkcjonalność komponentu w Angularze opiera się na konkretnie zdefiniowanych hookach, czyli funkcjach które Angular wywołuje na konkretnym etapie życia komponentu (np. ngOnInit(), ngOnChanges(), etc.).
  • Kompozycja i komunikacja – interfejs użytkownika skomponowany jest z wielu komponentów, które mogą się ze sobą komunikować. Wejściem dla komponentu są pola oznaczone dekoratorem @Input, natomiast sam komponent może powiadamiać jego otoczenie o zdarzeniach wewnątrz poprzez tzw. EventEmittery.
  • Metadane – dzięki dekoratorowi @Component możemy określić m.in selektor, który będzie wykorzystywany do inicjalizowania komponentu, albo też jakie style mają być z danym komponentem powiązane

Komponenty w nowym Angularze nie przypominają tego do czego przyzwyczajono programistów posługujących się na co dzień dyrektywami. Zmiany można zauważyć już na samym poziomie języka (TypeScript) – widać jak dużą rolę odgrywają teraz klasy, dekoratory czy typy. O ile w kwestii migracji starszych projektów nie obejdzie się bez dokumentacji, tak z punktu widzenia nowych aplikacji Angular zrobił ogromny krok w przyszłość.

Jeśli chodzi o widok aplikacji, to najbardziej istotną nowością jest teraz rola komponentu – o ile poprzednio aplikacja mogła opierać się o klasyczne widoki uzupełniane dyrektywami, tak teraz rozwiązanie które budujemy składamy w całości z komponentów. Wiąże się to z faktem, że Angular obrał kierunek platformy, na której opiera się cała aplikacja, a nie tylko narzędzia które ma sprawić, że statyczna strona zyska dynamiczne elementy (choć trzeba przyznać, że trwają pracę nad powrotem do korzeni – zainteresowanych odsyłam do tematu Angular Elements).

Zapoznaliśmy się z pojedynczym komponentem, ale skoro komponent to pewna atomowa część interfejsu użytkownika to jasne jest, że na jednym komponencie nie może się skończyć. Wzbogacając naszą aplikację o nowe funkcjonalności będziemy ją też rozbudowywać o nowe komponenty, a w rezultacie takiego procesu będziemy mieć przed sobą tzw. drzewo komponentów. Drzewo komponentów to nic innego jak struktura, która odwzorowuje układ komponentów naszej aplikacji. Może to wyglądać np. tak:

Jak na dłoni widać tutaj modularny charakter całego projektu. Przy odpowiednio wydzielonej odpowiedzialności poszczególnych komponentów, ich wymiana nie powinna w żadnym stopniu wpływać na inne obszary aplikacji – przykładowo zmiana komponentów odpowiedzialnych za prezentowanie danych nie powinna wpływać na mechanizm filtrowania. W wielu projektach możliwość wymiany komponentów nie ogranicza się jedynie do funkcjonalności, ale i do technologii, dzięki której realizujemy daną część aplikacji.

Jeśli jesteśmy już przy schemacie, który pokazuje układ komponentów naszej aplikacji, to warto powiedzieć jeszcze co nieco o typach komponentów i przepływie danych pomiędzy nimi. Załóżmy, że App jest komponentem, w którym na początku jego cyklu życia pobierane są pewne dane inicjalizacyjne dla aplikacji. Z powodu jego odpowiedzialności za pobranie danych i wykonanie logiki innej tej związanej ściśle z interfejsem użytkownika, możemy go zaliczyć do grupy smart components. Jego przeciwieństwem może być komponent ScoreTable, którego jedynym zadaniem jest stworzenie tabeli na podstawie przekazanych do niego danych. Nie ma zależności na inne części aplikacji, nie pobiera dodatkowych danych i nie przechowuje żadnego stanu – jego określimy mianem dumb lub presentational.

W Angularze przepływ danych pomiędzy komponentami jest jednokierunkowy. Patrząc na dwa zamieszczone powyżej schematy moglibyśmy powiedzieć, że propagacja danych odbywa się z góry na dół, natomiast propagacja eventów odbywa się z dołu do góry. Poprzez pola oznaczone pokazanym wcześnie dekoratorem @Input deklarujemy swego rodzaju API przez które będziemy przekazywać dane do środka komponentu, natomiast @Output używany jest do deklarowania eventów rozgłaszanych na zewnątrz. W odróżnieniu od promowanego przez framework Angular.JS podejścia two-way binding, całą ścieżkę przepływu danych możemy teraz rozbić na dwa etapy (wejście i wyjście), przez co cały proces staje się łatwiejszy do zrozumienia i wykorzystania w praktyce.

Co na to świat?

Praktyka pokazuje, że podejście component-first przynosi zdecydowanie więcej korzyści w postaci lepszej architektury aplikacji oraz bardziej deklaratywnego, reużywalnego i łatwiejszego do utrzymania kodu, w porównaniu do dynamicznych widoków opartych o splątane ze sobą kontrolery i manipulację DOMem. Korzyści o których piszę są na tyle znaczące, że w obecnych czasach komponenty znajdziemy w większości popularnych bibliotek i frameworków front-endowych.

Na ich podstawie zbudowana została filozofia Reacta, który od początku stawiał na taki sposób budowania interfejsu użytkownika, ma je Angular.JS (w wersji 1.5+) w którym zostały wprowadzone, aby ułatwić migrację do nowszych wersji Angulara, mają je też Ember, Vue czy rozwiązania zyskujące na popularności jak Svelte czy Stencil. Co najciekawsze, podejście oparte o komponenty zostało przyjęte na tyle dobrze, że obecnie trwają intensywne pracę nad ich natywną, ustandaryzowaną wersją w postaci Web Components (https://www.webcomponents.org).

Dla obiektywnego przedstawienia tematu muszę na koniec zaznaczyć, że największą rolę w tworzeniu front-endu nadal odgrywa… czynnik ludzki. Wiąże się to z faktem, że nawet najmodniejszy framework oparty o komponenty nie sprawi automatycznie, że wszystkie dotychczasowe problemy w projekcie zostaną rozwiązane. W podejściu które dzisiaj opisałem istnieje ryzyko przeniesienia zbyt dużej części logiki biznesowej do warstwy widoku czy powstania skomplikowanego modelu zarządzania stanem aplikacji. Właśnie dlatego najlepszym sposobem na swobodną pracę z komponentami jest doświadczenie i dostosowanie tzw. “dobrych praktyk” do swojego sposobu pracy – wszystko po to, żeby wspomniane przeze mnie korzyści nie były jedynie teorią.

Artykuł ukazał się oryginalnie w magazynie Programista (wydanie 11/2017)

Powiązane

O migracji do Angulara, czyli debiut w świecie ope... Niedawno pracując przy jednym z projektów które współtworzę natrafiłem na zadanie wymagające żmudnej, manualnej pracy powtarzanej w ten sam sposób...
Getting started on ASP.NET Core & React ̵... Contents Episode 1 - Introduction (you are here) Episode 2 - Backend / Infrastructure Episode 3 - Frontend Setup Episode 4 - Data flow...
Po pierwsze – nie zepsuć internetu Dynamiczny rozwój przeglądarek oraz technologii działających po stronie klienta sprawia, że każdego roku kolejne zastępy programistów decydują się...
Getting started on ASP.NET Core & React – epi... Contents Episode 1 – Introduction Episode 2 – Backend / Infrastructure Episode 3 – Frontend Setup Episode 4 - Data flow Episode 5 –...