Wróć do strony głównej
Angular

Angular 17 – co nowego?

No i doczekaliśmy się! Angular 17 został wydany jako stabilna wersja. Wydaje się, że zespół Angulara stawia sobie za zadanie zaskakiwać nas z każdym nowym wydaniem i wersja 17 nie jest wyjątkiem. Wprowadza ona zarówno duże funkcjonalności, jak i wiele usprawnień. Patrząc na to, co otrzymaliśmy, można śmiało stwierdzić, że duży nacisk został położony na optymalizację, zwłaszcza jeżeli chodzi o rozmiar paczki przy pierwszym pobraniu. Otrzymaliśmy także nowe control-flow, które jest solidną podstawą do tego, aby sygnały stały się standardem w następnych wersjach. Sprawdźmy, co dokładnie pojawiło się w tym wydaniu.

Nowa odsłona Angulara

Chwilę przed pojawieniem się v17 rozpoczął się Angular Renaissance. Wydarzenie prezentujące rebranding Angulara. Jest to pierwszy event tego typu, który miał miejsce od początku istnienia frameworku.

Zmieniona została cała paleta kolorystyczna brandingu, a znane nam dobrze czerwone logo zostało odświeżone i prezentuje sobą nowoczesne podejście jakie Angular ma oferować, jednocześnie zostając przy kształcie tarczy, który towarzyszy nam od AngularJS.

Co więcej, otrzymaliśmy nową, zaktualizowaną stronę https://angular.dev/ na której możemy znaleźć odświeżoną wersje dokumentacji z przykładami opartymi o standalone API oraz najnowsze techniki, nowe tutoriale przechodzące przez najważniejsze części Angulara oraz playground do wypróbowania frameworku w przeglądarce.

Built-in Control Flow

Jedną z największych i najbardziej zauważalnych zmian jest wprowadzenie nowego control flow do templatek, którego celem jest odejście od wbudowanych dyrektyw strukturalnych, których obecna konstrukcja nie pozwala na działanie w trybie zoneless.

Nowa składnia opiera się na strukturze nazwanej <b>block. Jej wygląd znacząco różni się od tego, co dotychczas widzieliśmy w templatkach. Każdy blok zaczynamy od prefiksu @, a następnie stosujemy składnię bardzo podobną do tego jaką znamy z języka JavaScript.

Zmiana dotyczy trzech najczęściej wykorzystywanych dyrektyw strukturalnych: ngIf, ngSwitch, ngFor. Na ten moment nie ma przewidywanej możliwości implementacji własnych bloków.

If

Switch

  • Przy instrukcji switch w odróżnieniu od standardowej składni znanej nam z JS, nie potrzebujemy dodawać keyworda break.
Loop

Ze wszystkich struktur to właśnie przy pętli otrzymaliśmy najwięcej wartościowych usprawnień:

  • Mamy dostępny blok @empty, który pozwala nam na wyświetlenie zawartości w momencie kiedy lista po której iterujemy jest pusta
  • Metodę trackBy dobrze znamy już z dyrektywy ngFor. Teraz jednak nie będzie już konieczności tworzenia specjalnej funkcji do przekazywania jej jako argumentu, wystarczy jedynie wskazać unikalny klucz obiektu. Co więcej, blok @for wymaga użycia funkcji track, co znacząco optymalizuje proces renderowania listy i zarządzania zmianami, bez konieczności ingerencji ze strony programisty. 

Obecnie, mając do dyspozycji dwa sposoby korzystania z control flow, pojawia się pytanie, co stanie się z dobrze znanymi nam dyrektywami? W wersji 17 pozostają one w niezmienionej formie, jednak wraz z przyjściem kolejnych wersji oraz wyjściem nowego control flow z developer preview przejdą one w stan deprecated. Nie ma jednak potrzeby martwić się refaktoryzacją, ponieważ zespół Angulara stworzył dla nas schemat do automatycznej migracji control flow do nowej składni, który w większości przypadków powinno sobie poradzić z przejściem na nowy syntax bez ingerencji programisty. Aby przejść na nową składnie wystarczy jedynie wykonać komendę:

Jeżeli jesteście ciekawi, dlaczego zespół Angulara wybrał właśnie tę konkretną składnię oraz chcielibyście poznać dokładniejsze powody, dla których istniejące dyrektywy nie zostały zmodyfikowane, serdecznie zachęcam do przeczytania naszego artykułu, w którym Mateusz odpowiada na te pytania.

Deferred Loading

Kolejną istotną nowością jest wprowadzenie prymitywu lazy-loadingu o nazwie defer, który opiera się na składni wprowadzonej w nowym control flow. Ten niezwykle przydatny mechanizm pozwala na kontrolowane opóźnienie ładowania wybranych elementów na stronie. Jest to szczególnie istotne, ponieważ dzięki użyciu defer, możemy istotnie zredukować początkowy rozmiar paczki, co znacząco wpływa na szybkość ładowania aplikacji, szczególnie dla użytkowników ze słabszym połączeniem internetowym.

Do kontroli tego kiedy należy wyrenderować zawartość bloku wykorzystujemy predefiniowane warunki: when oraz on. Możemy wykorzystywać je pojedynczo lub w dowolny sposób je łączyć w zależności od tego kiedy zawartość ma zostać załadowana.

When

Określa nam warunek logiczny,  który w momencie przyjęcia wartości prawdziwej załaduje zawartość bloku

Ważną informacją jest to, że po tym jak zawartość bloku zostanie już raz asynchronicznie doładowana to nie mamy możliwości cofnięcia tego załadowania. Jeżeli chcemy ukryć zawartość bloku musimy połączyć blok @defer z blokiem @if.

On

Pozwala nam na wykorzystanie predefiniowanych wyzwalaczy, dla których ładowanie zostanie zainicjowane.

Wyzwalacze: 

  • Idle – domyślny wyzwalacz, element będzie ładowany kiedy przeglądarka wejdzie w stan bezczynności. Do określenia momentu, w którym content zostanie załadowany, jest wykorzystywana metoda requestIdleCallback
  • Interaction – content będzie załadowany przy interakcji użytkownika czyli: click, focus, touch oraz na eventach inputa (keydown, blur, itd)
  • Immediate – doładowywanie nastąpi od razu po wyrenderowaniu się strony
  • Timer(x) – content zostanie doładowany po X-czasie
  • Hover – content zostanie doładowany w momencie najechania myszką przez użytkownika na obszar objęty funkcjonalnością
  • Viewport – content zostanie doładowany kiedy wskazany element pojawi się w widoku użytkownika. Do wykrywania widoczności elementu wykorzystywane jest Intersection Observer API

Mamy także możliwość łączenia zarówno warunków jak i wyzwalaczy:

Tutaj również warto zwrócić uwagę, że doładowanie contentu, tak jak w przypadku when, jest jednorazową operacją.

Prefetch

Mogą wystąpić sytuacje, w których chcielibyśmy rozdzielić proces pobierania zawartości od jej renderowania na stronie. W takim przypadku przychodzi z pomocą warunek prefetch. Pozwala on na określenie momentu (za pomocą wcześniej wspomnianych wyzwalaczy), w którym zależności potrzebne do załadowania treści bloku zostaną pobrane. Dzięki temu interakcja z zawartością, która została wcześniej pobrana, staje się możliwa znacznie szybciej, co przekłada się na lepszy UX.

Dodatkowo otrzymaliśmy 3 bardzo użyteczne opcjonalne bloki z których możemy skorzystać w ramach blok @defer

  • @placeholder – służy do określenia zawartości widocznej domyślnie, dopóki asynchronicznie ładowana zawartość nie zostanie aktywowana. Przykład:

Warunek minimum umożliwia określenie minimalnego czasu, po którym opóźniona zawartość może zostać załadowana. Oznacza to, że nawet jeśli warunek zostanie natychmiast spełniony, zawartość zostanie podmieniona po upływie 2 sekund (w tym konkretnym przypadku).

  • @loading</strong> – zawartość tego bloku jest wyświetlana w momencie, kiedy zależności są w trakcie ładowania. Przykład:

W ramach tego bloku także możemy wykorzystać warunek minimum, który działa identycznie jak w przypadku @placeholder. Wskazuje minimalny czas, przez który zawartość bloku będzie widoczna. Możemy skorzystać także z warunku after, którego używamy do wskazania po jakim czasie trwania ładowania zawartość bloku się pojawi. Jeżeli ładowanie trwa krócej niż 100ms, to loader się nie pojawi, a od razu pokaże się <deferred-cmp />.

  • @error– reprezentuje zawartość, która jest renderowana w momencie kiedy odroczone ładowanie z jakiegoś powodu się nie powiodło. Przykład:


    Podczas korzystania z bloku defer razem z blokiem @error, istnieje możliwość wykorzystania specjalnego warunku timeout. Ten warunek umożliwia ustalenie maksymalnego czasu doładowywania. Jeśli zależności będą pobierać się dłużej niż określony czas, zostanie wyświetlona zawartość bloku @error. Wewnątrz tego bloku, użytkownik ma dostęp do zmiennej $error, która zawiera informacje o błędzie, który pojawił się podczas procesu ładowania.

    Sygnały

    Sygnały w wersji developer preview są z nami od v16. Jeśli jeszcze nie mieliście okazji się z nimi zapoznać, serdecznie zapraszam do artykułu Miłosza, w którym szczegółowo przedstawione zostało ich działanie.

    W Angularze 17 wydane zostały one jako stabilne (za wyjątkiem funkcji effect(), która pozostała w developer preview), dzięki czemu śmiało możemy je wykorzystywać w komercyjnych aplikacjach. Wraz z wyjściem z developer preview otrzymaliśmy znaczące zmiany.

    Lokalne Change Detection

    Niewątpliwie jedna z najważniejszych nowości w tej wersji oraz pierwszy konkretny krok w stronę signal-based komponentów.

    Otrzymaliśmy możliwość uruchomienia Change Detection dla jednego komponentu. Jest to świetna funkcjonalność, która znacznie może wpłynąć na wydajność aplikacji.

    Aby Change Detection zadziałało tylko dla jednego komponentu należy spełnić 2 wymagania:

    • CD musi zostać wywołane przez sygnał
    • Wszystkie komponenty w naszej aplikacji muszą mieć strategię OnPush
    Dlaczego to sygnał musi być źródłem zmiany?

    Wynika to z tego, że Angular może śledzić, jakie sygnały są wykorzystywane w widoku, dzięki czemu wie, które komponenty powinny zostać zaktualizowane. Bazując na tej właściwości, zmieniony został sposób w jaki komponenty oznaczane są jako dirty, kiedy to sygnał jest źródłem zmiany.

    W v16 zmiana wartości sygnału, użytego w template, skutkowała takim samym zachowaniem, jak przy użyciu AsyncPipe. Komponent, którego zmiana dotyczyła, wraz ze wszystkimi przodkami byli oznaczani jako dirty. Dzięki temu, w następnym cyklu Change Detection, wszystkie te komponenty były ponownie sprawdzane. Proces oznaczania jako dirty dział się standardowo za sprawą metody markViewDirty(). Dokładnie tej samej która wykonuje się w ramach metody markForCheck() dostępnej na obiekcie ChangeDetectorRef.

    W przypadku v17 jeżeli wartość sygnału została zmieniona, zamiast markViewDirty() wykorzystywana jest metoda markAncestorsForTraversal(), która jako dirty ustawia jedynie component, którego dotyczy zmiana. Skutkuje to tym, że przodkowie tego komponentu są pomijani podczas CD, a tylko jeden, konkretny widok jest renderowany.

    Dlaczego wszystkie komponenty muszą mieć ustawioną strategię OnPush?

    Sposób w jaki Change Detection przechodzi przez drzewo komponentów został niezmieniony. Nadal jest to proces, który zaczyna się od Roota i leci z góry do dołu drzewa. Przez to, jeżeli jakieś komponenty będą miały ustawioną domyślną strategię to zawszę będą odświeżane, co jest niepożądanym efektem.

    Zmiana w defaultEquals

    Zmieniony został domyślny sposób porównywania wartości sygnału. 

    Implementacja funkcji defaultEquals w v16 uznawała dowolne dwa obiekty za różne, przez co nawet jeżeli zwracaliśmy referencje do tego samego obiektu to wszystkie zależne sygnały były powiadamiane o zmianie.

implementacja defaultEquals w v16

Natomiast w v17 funkcja defaultEquals opiera się wyłącznie o Object.is(). Skutkuje to tym, że jeżeli w wyniku użycia funkcji update() został zmutowany obiekt bez zmiany referencji, to inne zależne od niego sygnały nie zostaną powiadomione o zmianie.

implementacja defaultEquals w v17

Zmiana ta dla wielu może okazać się z początku dezorientująca. Jednak może mieć pozytywny wpływ na wydajność. Żeby mieć pewność, że sygnał powiadomi o zmianie należy użyć nowej instancji obiektu ze zmienionymi właściwościami (np. przez użycie spread operatora) lub stworzyć własną implementację funkcji equals, a następnie podać ją w opcjach sygnału.

Usunięcie metody mutate

Zdecydowano o usunięciu funkcji mutate, która wcześniej służyła do mutowania wartości przechowywanej przez sygnał. Funkcja mutate unikała porównywania wartości, ponieważ jej celem, z założenia, było modyfikowanie wartości sygnału. Teraz zaleca się korzystanie wyłącznie z funkcji update.

Jest to raczej pozytywna zmiana, ponieważ wprowadza ujednolicony i przewidywalny sposób modyfikowania sygnałów oraz może przyczynić się do poprawy prostoty i jakości kodu.

Server-side rendering

SSR jest to obszar, któremu zespół Angulara poświęca w ostatnim czasie dużo uwagi i wiemy, że nie zmieni się to przy kolejnych wersjach. Już teraz przy użyciu komendy ng new jednym z pytań, które zostanie nam zadane przez CLI, jest czy chcemy włączyć SSR dla naszej aplikacji.

W v17 domyślnie jest to Nie, natomiast zamysł na v18+ jest taki, żeby SSR był od razu dodawany przy tworzeniu nowej aplikacji.

Non-destructive hydration wyszło z developer preview

W wersji 16 Angulara otrzymaliśmy wsparcie dla non-destructive hydration. Hydracja to mechanizm pozwalający na wyrenderowanie aplikacji przez serwer i przesłanie jej do klienta w celu wyświetlenia. Non-destructive hydration pozwala na podłączenie do wyświetlonej aplikacji możliwości typowych dla SPA bez niszczenia wcześniej wyrenderowanego drzewa DOM. Jest to bardziej optymalne rozwiązanie względem wcześniejszego, które całkowicie zastępowało wyrenderowane przez serwer drzewo. W wersji 17 feature ten wychodzi z developer preview i jest wskazany do wykorzystywania w aplikacjach produkcyjnych. 

SSR, a deferred loading

W przypadku SSR @defer pozwoli wyrenderować po stronie serwera zawartość bloku @placeholder. W momencie gdy blok @placeholder nie zostanie zdefiniowany to cała zawartość @defer pozostanie pusta.

Planem na przyszłość, który możemy wyczytać w roadmapie jest rozbudowanie hydracji o wsparcie dla Partial Hydration, która polegać będzie na tym, że tylko niektóre fragmenty strony lub komponenty są renderowane na serwerze i przesyłane do klienta, podczas gdy reszta strony jest renderowana po stronie klienta. To rozwiązanie pozwoli na pełne wykorzystanie możliwości odroczonego ładowania z SSR.

Ulepszony sposób budowania aplikacji

Udoskonalony został builder oparty o esbuild. Do tej pory wykorzystywany był eksperymentalnie do budowania artefaktów dla przeglądarkowej wersji aplikacji (bez SSR). W wersji 17 otrzymaliśmy ulepszenie, dzięki któremu jesteśmy w stanie zbudować aplikację również w wersji ssr oraz prerender. Dzięki ujednoliconemu rozwiązaniu unikniemy potencjalnych różnic wynikających z użycia różnych bundlerów.

Lazy loading animacji

Problem z ładowaniem animacji jest taki, że do tej pory były one pobierane podczas bootstrapowania aplikacji, pomimo tego, że w większości przypadków animacje mają miejsce w czasie interakcji użytkownika z danym elementem na stronie. Nowa funkcjonalność w wersji 17 rozwiązuje ten problem i wprowadza możliwość doładowania kodu związanego z animacjami asynchronicznie. Dzięki temu rozwiązaniu wielkość głównej paczki może zostać zmniejszona nawet o 60 kB.

Aby zacząć korzystać z lazy-loadingu animacji należy dodać provideAnimationsAsync() zamiast provideAnimations() do providersów w naszej aplikacji.

I to wszystko! Teraz musimy tylko upewnić się, że wszystkie funkcje z modułu @angular/animations są importowane tylko w komponentach ładowanych dynamicznie.

Na ile kontrolowanie importów jest proste w naszej aplikacji, tak może być problematyczne przy wykorzystywanych przez nas bibliotekach. Przykładem takiej libki jest @angular/material, które mocno bazuje na @angular/animations, przez co jest bardzo duża szansa, że moduł odpowiadający za animacje zostanie zaciągnięty do głównej paczki.

Jeżeli chcemy sprawdzić czy animacje zostały pobrane asynchronicznie, możemy zbudować naszą aplikację z flagą --named-chunks. Wówczas w sekcji Lazy Chunk Files powinniśmy widzieć @angular/animations oraz @angular/animations/browser w osobnych pakietach.

aplikacja z asynchronicznie ładowanymi animacjami

aplikacja z standardowo ładowanymi animacjami

Dodatkowo na przykładzie prostej aplikacji widzimy, że początkowa waga paczki z asynchronicznie ładowanymi animacjami jest znacząco mniejsza.

Wsparcie dla View Transition API

Kolejną istotną nowością, na którą warto zwrócić uwagę, jest wprowadzenie wsparcia dla View Transition API. Jest to stosunkowo nowy mechanizm, który umożliwia tworzenie płynnych i interaktywnych efektów przejścia między różnymi widokami na stronie internetowej. Dzięki temu API, możemy dokonywać zmian w drzewie DOM w trakcie trwania animacji między dwoma stanami.

Dodanie View Transition API do naszego projektu jest niezwykle proste.

W pierwszej kolejności w miejscu bootstrapowania naszej aplikacji należy zaimportować funkcję withViewTransitions

A następnie dodać ją do konfiguracji provideRouter

Tym sposobem nasza aplikacja od razu zyskała delikatny efekt wejścia i wyjścia przy zmianie URL’a. Oczywiście, mamy możliwość stworzenia customowych animacji. W poniższym przykładzie efekt przejścia został wydłużony do 2 sekund poprzez zastosowanie przygotowanych pseudo-elementów w pliku styles.css

Ten przykład stanowi jedynie bardzo niewielką próbkę tego, co możemy osiągnąć za pomocą tego API. Jeśli interesują Cię praktyczne zastosowania tego mechanizmu, zachęcam do zapoznania się z materiałem dostępnym w dokumentacji przeglądarki Chrome.

Warto jednak pamiętać, że jest to względnie nowa funkcjonalność w fazie eksperymentalnej. Oznacza to, że nie ma jeszcze pełnego wsparcia w niektórych przeglądarkach. Zakres wsparcia dla poszczególnych przeglądarek sprawdzisz na caniuse.

Inne warte uwagi zmiany w Angular 17

  • Aplikacje tworzone w wersji 17 domyślnie będą standalone oraz będą wykorzystywać Vite bazujący na esbuild jako domyślny system budowania aplikacji.
  • Wsparcie dla Node 16 zostaje zakończone. Od Angulara 17 minimalną wersją, która jest kompatybilna to Node 18.13
  • Najniższą wspieraną wersją TypeScripta, od tej wersji został 5.2
  • Niewielkim usprawnieniem, ale ciepło przyjętym przez community jest dodanie wartości styleUrl dla dekoratora @Component, która przyjmuje stringa z jedną ścieżką do pliku ze stylami.
  • Angular DevTools zostały wzbogacone o nową, niezwykle przydatną funkcjonalność. W najnowszej aktualizacji dają nam możliwość przeglądania hierarchii injectorów w naszej aplikacji, co znacząco poprawia jakość DX podczas procesu debugowania.

Podsumowanie

Jak możemy zauważyć, zmiany wprowadzone w tej wersji Angulara są znaczące i mocno wpłyną na to, jak będzie wyglądała praca z tym frameworkiem w przyszłości. Migracja do nowego control flow stanie się w pewnym momencie konieczna, aby móc efektywnie korzystać z signal based componentów. Ponadto SSR ma stać się stałą i integralną częścią Angulara. Ważnym aspektem tej wersji jest także to, że wprowadzono funkcje mające na celu zmniejszenie obciążenia aplikacji przy jej ładowaniu.

Co sądzicie o kierunku, w jakim zmierza Angular? Czy jesteście zadowoleni z wprowadzonych zmian w tej wersji oraz rebrandingu, który został przeprowadzony? Czekamy z niecierpliwością na Wasze opinie, którymi możecie podzielić się w komentarzach!

Wcześniejsze wersje

Sprawdź zmiany we wcześniejszych wersjach Angular:

O autorze

Piotr Wiórek

Angular developer, uwielbiający czysty i czytelny kod. W wolnych chwilach gra w squasha i szuka dobrego ramenu

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.

3 komentarzy

  1. Mateusz

    Hej. Dzięki za artykuł. Fajnie wszystko wyjaśnia. Co do samych zmian, to mam mieszane uczucia. Z jednej strony @defer może rzeczywiście zostać game changerem, sygnały też są spoko, chociaż nie powiem, żebym miał coś przeciwko rxjs. Zobaczymy, jak to się rozwinie. Ale z drugiej strony składnia bloków jak @if czy @for za bardzo przypomina to, czego kiedyś nienawidziłem w JSP (każdy, kto kiedyś musiał analizować stronę JSP wie, ile bólu potrafiło to kosztować).

    • Hej Mateusz, dzięki za komentarz! W pełni rozumiem to, że masz mieszane uczucia co do nowej składni, sam je na początku miałem 🙂 Jednak jakby nie patrzeć to składnia jest bardzo podobna do JS, jedynie poprzedzona “@”, a ilość dobrych zmian zdecydowanie przeważa nowy syntax. Nie musimy korzystać już ze zmiennych w template, aby wyświetlić blok “else”, a do tego otrzymaliśmy @else-if, a przy @for blok @empty! Dodatkowo dzięki odejściu od dyrektyw i dodaniu wbudowanego control-flow było możliwe wiele znaczących optymalizacji co istotnie wpłynie na wydajność naszych aplikacji.
      Na szczęście dla nas, topowe IDE już dostosowują się do nowej składni (np. WebStorm https://youtrack.jetbrains.com/issue/WEB-63164/Angular-17-new-control-flow-syntax), dzięki czemu przyzwyczajenie się do niej powinno być znacznie łatwiejsze. Mam nadzieję, że za jakiś czas będziesz z przyjemnością wspominał jak to dobrze się używa nowego control-flow (a przynajmniej lepiej niż w JSP :D).

      Jeżeli chodzi o współdziałanie sygnałów z rxjs to raczej nie musimy się martwić, że jedno wygryzie drugie. Oba koncepty świetnie się uzupełniają! Należy pamiętać, że sygnały są synchroniczne przez co nadają się idealne do przechowywania stanu oraz działań związanych z DOMem, natomiast rxjs z pewnością zostanie z nami na dłużej przy eventach asynchronicznych, gdzie jego operatory są i będą przez długi czas niezastąpione. Jeżeli to Cię nie przekonuje to zachęcam do zapoznania się z artykułem Mateusza https://www.angular.love/2023/07/13/dlaczego-sygnaly-nie-zastapia-rxjs/.

  2. Maciek

    Cześć, garść świetnych informacji ale wiadomo już może czy jest rozwiązanie pozwalające na podniesienie istniejącego projektu do wersji 17 bez konieczności przejścia na standalone lub pozwalające wyłączyć tą opcję ?

Leave a Reply

Your email address will not be published. Required fields are marked *