Wróć do strony głównej
Angular

Nx i Angular Elements – studium przypadku

Co jeśli zostajemy przydzieleni do projektu, który bardzo dobrze funkcjonuje na produkcji, ale technologia, która za nim stoi jest uznawana za “przestarzałą” i chcemy ją wymienić? Czasami można porzucić stary projekt i zacząć tworzyć w jego miejsce nowy. Jeśli projekt jest duży, a firma na nim zarabia, może to się okazać niemożliwe.

Na ratunek przychodzi wzorzec “dusiciela” (strangler pattern).

Polega on na przebudowie starego systemu od środka, zastępując i dodając w nim funkcjonalności w innej technologii zamiast dotychczasowej. Z czasem liczba takich “nowych” funkcjonalności rośnie, a my możemy ograniczyć do minimum stary kod, zachowując ciągłe działanie systemu. Z takim wyzwaniem zmierzyłem się przy budowie aplikacji administracyjnej w firmie Unilink, a w poniższym artykule opowiem w jaki sposób udało mi się zaimplementować mikro frontendy w Angularze.

Przygotowanie

Pierwszym etapem implementacji nowej części systemu było stworzenie kilku nowych widoków dla dotychczasowej aplikacji. Do rozwiązania tego zadania zdecydowałem się na użycie mikro frontów z wykorzystaniem Web Components. Wszystkie nowe widoki stanowiły oddzielne mikro aplikacje, ale korzystały z podobnego kodu, dlatego były rozwijane wewnątrz jednego repozytorium, tak aby móc skorzystać z szybkiego tworzenia i modyfikowania wspólnych komponentów. Docelowo, po osiągnięciu odpowiedniej liczby funkcjonalności w Angularze, miała powstać oddzielna aplikacja typu SPA.

Do stworzenia mono repozytorium skorzystałem z narzędzia Nx. Dodatkowo, w kontekście Angulara, rozbudowuje ono CLI o dodatkowe możliwości i lekko zmienia zestaw domyślnych ustawień nowo utworzonego projektu Angularowego.

Kilka zauważalnych różnic w nowym projekcie utworzonym w Nx względem nowego projektu utworzonego przy pomocy oryginalnego CLI:

  • dodany jest eslint do statycznej analizy kodu
  • dodany jest prettier do formatowania kodu
  • zamiast Jasmine i Karma do testów jednostkowych dodany jest Jest
  • dodany jest cypress do wykonywania testów e2e

Istnieje również możliwość utworzenia projektu bez powyższych zależności.

W podejściu monorepo, kod wielu różnych bibliotek i projektów przechowuje się w jednym repozytorium. Nx pozwala na skorzystanie z dwóch rodzajów monorepozytorium – zintegrowanego (integrated monorepo) i opartego na paczkach (package based monorepo) – które można ze sobą łączyć. Zintegrowane skupia się na wydajności i łatwości utrzymania. Repozytorium oparte na paczkach daje większą elastyczność i łatwość użycia. 

Nx dodaje bardzo ważną komendę do CLI – affected, która pozwala wylistować wszystkie biblioteki i projekty wewnątrz monorepoztorium, które zostały dotknięte wprowadzonymi zmianami w porównaniu do poprzedniego lub wskazanego miejsca w historii gita. Na każdym elemencie tej listy można wykonać dowolne zadania, np. lint, testy, build itd. Dodatkowo Nx przechowuje w pamięci wykonane już polecenia, a także pozwala na synchronizację pamięci podręcznej między wieloma stacjami roboczymi (Nx Cloud).

Web Components pozwala na utworzenie własnego znacznika HTML. Taki znacznik może zawierać całą (prawie) aplikację w Angularze. Do tego służy właśnie biblioteka Angular Elements. Każdy widok (feature), można zamknąć w oddzielnej mikro aplikacji. Angular w Web Components ma jednak duże ograniczenie – nie posiada wsparcia dla RouterModule. W przypadku nawigacji między ekranami aplikacji trzeba posiłkować się innymi rozwiązaniami. Więcej o Angular Elements znajdziecie w tym artykule.

Podział aplikacji na biblioteki

Nx posiada wiele różnych “presetów” umożliwiających wygenerowanie innej struktury folderów. Używając presetu dla Angulara w projekcie pojawią się dwa główne foldery: apps i libs.

Apps – zawiera wszystkie aplikacje, które można zbudować/zaserwować. Ich konfiguracje są zawarte w zmodyfikowanym pliku angular.json, który importuje je z plików project.json z poszczególnych aplikacji. Mogą to być także aplikacje nie napisane w Angularze.

Libs – zawiera biblioteki, czyli moduły z których będą korzystały aplikacje. Ich definicje również trafiają do pliku angular.json, a sama konfiguracja znajduje się w pliku project.json w folderze danej biblioteki. Biblioteka nie jest pełnoprawną aplikacją. Możemy ją zbudować i opublikować w dowolnym repozytorium, np. NPM lub prywatnym Verdaccio lub wykorzystać bezpośrednio w kodzie. Nx pozwala na bardzo wygodne importowanie plików z bibliotek przez uproszczone ścieżki (ang. aliasy), których konfigurację można znaleźć w pliku tsconfig.base.json.

Większość logiki naszej aplikacji powinna znaleźć się właśnie w folderze libs. W ten sposób aplikacje możemy składać z odpowiednich modułów w bibliotekach.

Składanie aplikacji z takich modułów jest bardzo przydatne jeśli chcemy wykorzystać daną funkcjonalność w więcej niż jednej aplikacji. Cała Funkcjonalność jest zamknięta w jednej bibliotece. Następnie trafia ona do systemu legacy w jednej aplikacji – Web Componencie. Obok może powstać druga aplikacja, która z wykorzystaniem routingu wyświetli dokładnie ten sam widok razem z innymi funkcjonalnościami – bibliotekami.

Podział aplikacji na domeny

Jak to wszystko razem spiąć i nie zwariować?

Aby zachować porządek, biblioteki możemy podzielić na różne obszary biznesowe, a także na różne warstwy. Taki obszar biznesowy możemy określić domeną. Mogą to być np. szkolenia, płatności, zamówienia, biblioteka wspólnych elementów itd. Każda z tych domen może zostać dodatkowo podzielona na dowolne warstwy. Możemy wykorzystać podział zaproponowany przez Manfreda Steiera w artykule Tactical Domain-Driven Design with Angular and Monorepos 

  • Api
  • Feature
  • UI
  • Domena
  • Util

Jeśli chcesz dowiedzieć się więcej o podziale na warstwy i sposobie na utrzymanie porządku w kodzie polecam zapoznać się z dokumentacją Nx, artykułem w tej tematyce z oficjalnego bloga Nx oraz serią artykułów Manfreda. W tym artykule pokażę Ci jak wykorzystać jedną funkcjonalność w dwóch różnych aplikacjach i nie będę wchodził w detale podziału na domeny i warstwy.

Dodanie pierwszej mikroaplikacji

Dodajemy workspace Nx

Interaktywny kreator zada nam kilka pytań:

Wybieramy angulara jako framework i konfigurację dla naszego projektu.

Ostatnie z pytań w interaktywnym kreatorze nowego workspace będzie brzmiało:

Nx oferuje możliwość skorzystania z cache wykonanych poleceń (lint, testy, build) w chmurze, co poprawia wydajność skryptów, które w przypadku braku zmian w kodzie skorzystają z wcześniej zbudowanych elementów.

Nasza pierwsza aplikacja została dodana. Stanie się ona z automatu domyślną aplikacją uruchamianą po wywołaniu komendy npm start bez dodatkowych parametrów. Jeśli chcemy zmienić domyślną aplikację, należy zmienić jej nazwę w pliku nx.json.

W przypadku omawianego projektu, każdy ekran (widok) aplikacji powinien być oddzielną biblioteką. Dodajmy zatem nowy widok – bibliotekę, która trafi do mikroaplikacji – Web Componentu.

Dodanie biblioteki feature

 

Wcześniej została dodana już aplikacja, która będzie kontenerem dla naszego widoku. Dodajmy teraz bibliotekę, która będzie zawierała właściwy widok.

Zostaniemy zapytani jakiego generatora chcemy użyć. 

Wybieramy @nrwl/angular:library

Do folderu libs zostanie dodany folder billing, który zawiera folder z nowo utworzoną biblioteką. Folder billing to w tym wypadku konwencja utrzymania bibliotek w obrębie jednej domeny biznesowej. Może to być dowolna nazwa, która reprezentuje jakiś obszar biznesowy. Użyta nazwa feature również jest konwencją, która może przydać się przy dodaniu podziału aplikacji na warstwy, aby móc je z łatwością rozpoznać w strukturze projektu.

Plik na który warto zwrócić uwagę to index.ts, który eksportuje wybrane elementy z biblioteki, dając do nich dostęp z zewnątrz, spoza biblioteki.

Do biblioteki dodajmy teraz Angularowy komponent, który będzie naszym widokiem. Możemy skorzystać z CLI lub dowolnego pluginu do generowania komponentów, np. Nx Console.

Pamiętaj, żeby po dodaniu nowego komponentu, w konfiguracji dodawanego modułu dodać komponent w tablicy exports. Dzięki temu, będziemy mogli skorzystać z tego komponentu po zaimportowaniu modułu BillingFeatureInvoicesModule.

Możemy teraz zaimportować moduł z biblioteki we wcześniej utworzonej aplikacji. Otwórz app.module.ts w aplikacji invoices i zaimportuj feature module.

Po wpisaniu nazwy modułu w tablicy importów i próbie automatycznego zaimportowania modułu, VS Code zasugeruje nam dwie ścieżki do modułu. Jeśli wybierzemy absolutną ścieżkę (2 pozycja), IDE od razu podświetli ją na czerwono. To eslint i reguła wprowadzona przez Nx, która chroni między innymi przed takim rodzajem importów. Wybieramy w takim razie preferowaną – pierwszą – opcję, czyli import ze ścieżką ukrytą za aliasem, zgodnym ze standardem npm.

Jeśli chcesz sprawdzić wszystkie dostępne ścieżki do bibliotek, są one skonfigurowane w pliku nx.json w głównym folderze projektu.

Dodajemy jeszcze wywołanie komponentu w szablonie app.component.html

Zmiana nazwy znacznika

Wygenerowany znacznik dla komponentu Invoices nie do końca oddaje z jakim rodzajem komponentu mamy do czynienia – czy jest to feature czy coś innego. W pliku zawierającym InvoicesComponent zmieńmy nazwę selectora HTML na f</span><span style="font-weight: 400">eature-invoices</span><span style="font-weight: 400">.

IDE pokaże nam ostrzeżenie z eslinta:

Musimy zmienić prefix dla selektorów w obrębie tej biblioteki. Wchodzimy do pliku .eslintrc.json w folderze libs/billing/feature-invoices i zmieniamy konfigurację na:

Z takim prefiksem coś jest jednak nie tak. Każdy komponent zadeklarowany w tej bibliotece będzie musiał mieć prefiks feature, co nie będzie zgodne z prawdą. Możemy przyjąć np. prefiks zgodny z nazwą domeny, z której pochodzi feature, np. billing. Zmieniamy w takim razie ponownie konfigurację, aktualizujemy selektor naszego komponentu i zmieniamy szablon w naszej mikro aplikacji.

Zbudowanie mikroaplikacji do Web Componentu

Aplikacja ma jedynie importować feature module i zapewniać niezbędną konfigurację. Budowanie do Web Componentu będzie wymagało dodania kilku zmian do domyślnie wygenerowanych plików. Skorzystamy z instrukcji z artykułu Angular Elements na blogu.

Dodajemy dwie niezbędne biblioteki:

Nx dekoruje CLI Angulara dodając funkcjonalności pod maską, ale jak widać nie wszystkie. Użyjmy więc proponowanego skryptu:

I wprowadzamy zmiany w module i komponencie z aplikacji invoices.

apps/invoices/src/app/app.module.ts

apps/invoices/src/app/app.component.ts

Budowanie Web Componentu

Serwujemy aplikację, żeby sprawdzić czy wszystko działa:

Wszystko działa. Schody zaczynają się jeśli chcemy mieć ciastko i zjeść ciastko, czyli zbudować aplikację invoices jako Web Component i nadal być w stanie ją zaserwować.

Zgodnie z artykułem, aby umożliwić budowanie mikroaplikacji, usuwamy konfigurację bootstrap z dekoratora AppModule.

Z naszego localhost:4200 zniknęła zawartość aplikacji. Naprawimy to. 

Możemy przyjąć, że będziemy budować aplikację do Web Componentu tylko w konfiguracji produkcyjnej, a w przeciwnym wypadku skorzystamy z bootstrapowania aplikacji. Skorzystajmy więc z pola production z pliku environment.ts w aplikacji.

Hook ngDoBootstrap w module jest zawsze wywoływany, nawet jeśli aplikacja zostanie zbudowana do Web Componentu, dlatego przeniesiemy tam logikę, która zadecyduje w jakim trybie uruchomić aplikację.

Modyfikujemy trochę app.module i kod głównego komponentu app.component.ts:

apps/invoices/src/app/app.module.ts

apps/invoices/src/app/app.component.ts

Teraz po zapisaniu plików powinniśmy zobaczyć, że nasza aplikacja działa. 

Co się dzieje w ngDoBootstrap?

Sprawdzamy czy flaga production w pliku environment jest ustawiona na true. Następnie sprawdzamy, czy w Web Componentach nie został już zadeklarowany inny komponent pod tym samym znacznikiem HTML. Może to być przydatne, jeśli używamy tego samego komponentu w więcej niż jednym miejscu w statycznym pliku HTML. Następnie tworzymy Angular Element przekazując do niego niezbędne argumenty i deklarujemy Web Component pod podanym znacznikiem. Jeśli flaga production jest jednak ustawiona na false, bootstrapujemy komponent AppComponent do Angularowej aplikacji.

Sprawdźmy czy uda się uruchomić aplikację jako Web Component. Zgodnie z artykułem Łukasza wywołujemy komendę budowania i połączenia plików w jeden

i wywołujemy Web Component w statycznym pliku HTML:

Więcej o Angular Elements możesz przeczytać we wspomnianym wcześniej artykule na blogu.

Wykorzystanie nowego modułu feature w aplikacji SPA

Wygenerowanie kolejnej aplikacji do workspace w Nx jest dostępne pod komendą:

Na pytanie kreatorze w CLI wpisujemy w konsoli y:

W folderze apps mamy teraz dwie aplikacje. Możemy zaserwować z tego samego workspace jednocześnie drugą aplikację (i wiele innych). Wystarczy wywołać skrypt

Ponieważ port 4200 jest już zajęty, możemy do komendy przekazać ręcznie port na którym chcemy wystawić naszą aplikację. Możemy też pozostawić CLI wykrycie innego dostępnego portu nie przekazując tego parametru, ale wtedy dostaniemy za każdym razem inny port.

Dodajmy do głównego app.module.ts nowej aplikacji feature, ale tym razem przez routing.

Przy próbie wygenerowania ścieżki do importu, IDE nie podpowie tym razem prawidłowej ścieżki. Należy pamiętać, że wszystkie pliki, które mają być eksportowane z biblioteki na zewnątrz, muszą zostać dodane do pliku index.ts w odpowiadającej bibliotece.

libs/billing/feature-invoices/src/index.ts

Importujemy teraz komponent w app.module.ts. Przykład takiej konfiguracji routingu jest bardzo podstawowy. Moglibyśmy skorzystać tu z dodatkowo utworzonej biblioteki – Shell – która zawierałaby potrzebną konfigurację routingu dla całej domeny billing. Jeśli chcesz dowiedzieć się jak to zrobić, daj znać w komentarzu.

Teraz po otworzeniu adresu localhost z wybranym portem powinniśmy zobaczyć ten sam feature, który mamy w mikro aplikacji serwowanej na porcie 4200.

Plusy i minusy Web Componentów

Wykorzystanie mikro frontów w formie Web Componentów ma jasne i ciemne strony. Zacznijmy od minusów.

Minusy

  • rozmiary paczek – mimo optymalizacji, nadal trudno będzie osiągnąć małe rozmiary zbudowanych mikro aplikacji (jednym ze sposobów na złagodzenie negatywnych skutków jest lazy loading web-componentów),
  • brak routingu – w kontekście Web Componentu nie możemy skorzystać z router-outletów, ponieważ nasza aplikacja nie podpina się pod adresy przeglądarki. Taka mikro aplikacja jest zależna od nawigacji rodzica. Możemy oczywiście korzystać ze standardowych anchor tagów zamiast routerLinków,
  • z czasem może być potrzebny DevOps – podział na wiele bibliotek powoduje wydłużenie czasu przechodzenia procesu budowania. Lintowanie, testowanie i budowanie całego projektu może wydłużyć się do kilkudziesięciu minut (jeśli nie godzin). Monorepo będzie potrzebowało wprowadzenia kilku optymalizacji w procesie CI/CD, np. z wykorzystaniem komendy affected i cache’owania.

Plusy

  • dostajemy możliwość stopniowego tworzenia nowych funkcjonalności systemu, przenosząc dotychczasowy legacy system jedynie w tryb utrzymania,
  • w przypadku korzystania z sesji w cookie, Web Component nie musi posiadać własnej autoryzacji. Wszystkie akcje wykonywane są w kontekście rodzica, czyli systemu w którym są osadzone nasze Web Componenty. To rodzic “użycza” sesji,
  • migracja do aplikacji SPA z systemu Legacy jest stosunkowo prosta, jeśli pokrycie funkcjonalności systemu będzie wystarczająco duże. Musimy dodać oddzielną aplikację – ramkę, która będzie zarządzała routingiem, autoryzacją i innymi detalami, którymi Web Componenty nie musiały się przejmować.

Podsumowanie

Architektura mikro frontendów może być przydatna nie tylko w dużych, rozproszonych zespołach. Można skorzystać z niej także w przypadku stopniowego przebudowywania już istniejącego systemu. Podział aplikacji na moduły sprawi, że nasz kod stanie się łatwiejszy w utrzymaniu, ale należy pamiętać, że nie jest to rozwiązanie, które magicznie sprawi, że modyfikowanie kodu stanie się prostsze. Jeśli będziemy dbali o dobre praktyki, dodawanie nowych funkcjonalności stanie się szybsze przez zwiększone reużycie istniejącego kodu między kolejnymi funkcjonalnościami / projektami.

P.S. A propos dużych rozmiarów paczek z Web Componentami. Aktualnie nasza mikro aplikacja, która wyświetla jedynie tekst invoices works! waży aż 226 kB. To stanowczo za dużo! Jeśli potrzebujesz czegoś mniejszego, warto rozważyć stworzenie web componentów bez użycia frameworków. Z takim podejściem będzie można się spotkać np. w Google Material v3.

O autorze

Jakub Pawlak

Angularowy programista saksofonista. Entuzjasta czytelnych i łatwych w użyciu rzeczy – od kodu, przez elektronikę, po UX.

Zapisz się do naszego newslettera. Bądź na bieżąco z najnowszymi trendami, poradami, meetupami i stań się częścią społeczności Angulara w Polsce. Rynek pracy docenia członków społeczności.

2 komentarzy

  1. Hubert

    Napisałeś, że minusem jest brak routingu – nie jest to prawda. Routing jest możliwy wystarczy, że zamiast RouterModule użyjemy RouterTestingModule, który wykorzystywany jest do testowania – nie ingeruje w przeglądarkę. Wszystkie ustawienia routingu używamy tak jak wcześniej tylko adres nie będzie się zmieniać.

    • Hej, zaintrygowałeś mnie. Mógłbyś podać fragment implementacji? Z tego co próbowałem tego podejścia nie działa, więc nadal trzeba statycznie podać komponent na sztywno (można się było spodziewać 😉 ). routerLink i routerLinkActive też nie. Jaka jest w takim razie korzyść z importowania RouterTestingModule zamiast niedziałającego RouterModule? Wygląda na to, że w obu przypadkach potrzebna jest customowa implementacja jeśli nie chcemy użyć hrefa.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *