Wróć do strony głównej
Angular

Progresywny Angular cz. 1

Pewnie wiele razy spotkaliście się z sytuacją, gdy pomimo dostępu do sieci strona internetowa nie potrafiła się załadować, a Wy bez końca wpatrywaliście się w biały ekran. Jest to jedno z tych doświadczeń, gdzie Wasza cierpliwość się kurczy z każdą sekundą, ale mimo wszystko macie nadzieję, że za chwilę jednak coś się na ekranie pojawi.

Wszystko to wynika z założenia, że użytkownik ma zawsze dobrej jakości połączenie internetowe, Online first (lub Network-first). Aby dostarczyć użytkownikowi jak najlepszych odczuć, powinniśmy zmienić nasze podejście i zastosować Offline first (lub Cache-first), czyli wyrzucamy dostępność sieci na drugi plan i zakładamy, że nasza aplikacja działa w trybie offline.

W tym celu możemy skorzystać z Progressive Web Apps. Na łamach naszego bloga w pierwszej części poruszymy samą koncepcję tej technologii, a w kolejnej przedstawimy w jaki sposób możemy przekształcić naszą istniejącą aplikację Angularową w PWA.

Czym w takim razie są aplikacje PWA?

 

  1. Progressive Web App
    1.1. PWA vs Native vs Hybrid
  2. Service Worker
    2.1. Scope
    2.2. Rejestracja
    2.3. Cykl życia
    2.4. Zdarzenia
    2.5. Komunikacja
    2.6. Precaching
    2.7. Wersjonowanie
    2.8. Strategie Cache’owania
    2.9. Wiele Service Workerów
    2.10. Recipes
    2.11. Angular Service Worker
    2.12. WorkBox
  3. Podsumowanie

Progressive Web App

Są to aplikacje instalowane z poziomu przeglądarki internetowej podczas odwiedzin strony posiadającej wsparcie dla omawianej technologii. PWA są dostępne zarówno na desktopach jak i platformach mobilnych, były natomiast tworzone z myślą głównie o tych drugich.

Podczas odwiedzin strony aplikacji ze wsparciem PWA istnieje możliwość dodania takiej aplikacji do naszego urządzenia. Wówczas na naszym urządzeniu dostępna jest ona jak natywna.

Jednym z podstawowych założeń PWA było działanie bez dostępu do sieci. Jest to możliwe dzięki wykorzystaniu tzw. Service Workerów, które pośredniczą w komunikacji pomiędzy przeglądarką a internetem. Umożliwiają one wówczas realizację takich strategii jak stale-while-revalidate, dostarczając użytkownikowi dostęp do aplikacji oraz jej zasobów pobranych z pamięci podręcznej, jednocześnie synchronizując się z siecią w tle.

 

Poglądowy schemat aplikacji PWA.

PWA vs Native vs Hybrid

Aplikacje PWA w odróżnieniu od natywnych bądź hybrydowych w celu swojego prawidłowego oraz pełnego działania muszą mieć dostęp do sieci, w końcu są one budowane na podstawie istniejących stron internetowych.

Dostęp do natywnych funkcjonalności jest stosunkowo ograniczony, liczba dostępnych funkcji stale rośnie, jednak ostatecznie odbiega od hybrydowych bądź natywnych aplikacji (aktualnie wspierane funkcje można znaleźć na What Web Can Do Today).

PWA pozwala natomiast na instalację na dowolnej platformie posiadającej przeglądarkę wraz z odpowiednim wsparciem dla Service Workerów. Dodatkową zaletą jest również brak konieczności publikacji naszej aplikacji w sklepie danej platformy.

Należy również wspomnieć o stosunkowo niskiej wydajności tego typu aplikacji w porównaniu z natywnymi oraz o niebezpieczeństwie wynikającym z ograniczonych zabezpieczeń. Kompleksowe porównanie aplikacji natywnych, PWA oraz hybrydowych w wybranych kategoriach zostało przedstawione na grafice poniżej.

 


Zestawienie aplikacji natywnych, PWA oraz hybrydowych w wybranych kategoriach.

Service Worker

Jest to mechanizm działający jako swego rodzaju proxy pomiędzy aplikacją, przeglądarką internetową oraz siecią. Umożliwiają m.in. cache’owanie zasobów aplikacji, synchronizację danych w tle czy też wywoływanie Push Notifications. Funkcjonalności te są możliwe m.in. poprzez przechwytywanie żądań wychodzących.

W odróżnieniu od innych skryptów aplikacji, Service Worker zostaje zachowany po zamknięciu taba czy przeglądarki. W przypadku kolejnego uruchomienia aplikacji, SW zostaje załadowany jako pierwszy i jest w stanie przechwytywać wszelkie żądania do źródeł i zasobów aplikacji. Odpowiednio zaimplementowany SW jest w stanie całkowicie załadować aplikację bez dostępu do sieci.

Service Workery są całkowicie asynchroniczne i działają w indywidualnym wątku nie blokującym renderowania. Z racji tego nie posiadają dostępu do drzewa DOM oraz innych synchronicznych funkcjonalności i bibliotek jak np. local storage (mają dostęp natomiast do IndexedDb) czy też XHR.

Oprócz tego ze względów bezpieczeństwa, mogą być wykorzystane tylko z użyciem protokołu HTTPS (poza lokalnym środowiskiem gdzie localhost jest dostępny). Pełną listę dostępnych funkcji można znaleźć na stronie HTML5 worker test.

Scope

W obrębie pojedynczej aplikacji może działać wiele różnych SW, będących odpowiedzialnych za różne zakresy aplikacji. Zakres w jakim dany SW operuje nazywamy scopem.

W obrębie danego scope’a tylko jeden Service Worker może być zarejestrowany i aktywny.

W uproszczeniu, scope danego SW to wszystkie pliki oraz katalogi znajdujące się “pod nim” w drzewie aplikacji, tzn. SW zdefiniowany na poziomie naszego głównego modułu kontroluje całą aplikację.

Istnieje możliwość ustawienia scope’a danego SW podczas jego rejestracji, ograniczając jego działanie np. do pewnej puli adresów. Przykładowo ustawiając scope na /api/, nasz SW będzie mógł przechwytywać zapytania do /api/ lub /api/image, nie będzie kontrolował natomiast adresów będących wyżej w hierarchii, jak np. /api (bez końcowego slasha) lub /.

W przypadku Angular Service Worker, należy przekazać dodatkowy obiekt konfiguracyjny.

Rejestracja

Podczas pierwszej wizyty w naszej aplikacji następuje instalacja Service Workera oraz jego natychmiastowa aktywacja.

W przypadku kolejnych odwiedzin, gdy przeglądarka wykryje, że dostępna jest nowa wersja SW, wówczas jest ona instalowana, ale jeszcze nie aktywowana. Mówimy wtedy, że jest to worker in waiting, Service Worker czekający na swoją kolej.

Aktywacja czekającego SW następuje w momencie, gdy żadna ze stron kontrolowana przez poprzedniego, starego SW nie jest już załadowana.

Może jednak nastąpić sytuacja, gdy będziemy chcieli od razu aktywować zaktualizowanego SW. Wówczas możemy skorzystać z ServiceWorkerGlobalScope.skipWaiting(), funkcji aktywującej naszego nowego SW wraz z Clients.claim(), czyli metody przekazującej wszystkich aktualnych “klientów” pod kontrolę nowego SW.

Od tej pory wszelkie wychodzące zapytania oraz dodatkowe funkcjonalności będą kontrolowane przez zaktualizowanego Service Workera.

Cykl życia

Każdy Service Worker charakteryzuje się określonym cyklem życia. W skrócie sprowadza się to do rejestracji wybranego SW, jego instalacji oraz aktywacji. W przypadku wystąpienia błędu podczas któregoś z wymienionych etapów, wybrany SW nie zostaje zarejestrowany lub zostaje zastąpiony innym.

Nasłuchując na wybrane zdarzenia jesteśmy w stanie dostosować kroki wykonywane podczas inicjalizacji do naszych wymagań, przykładowo w celu cache’owania zasobów.

 

Diagram ilustrujący poszczególne etapy rejestracji Service Workera.

Zdarzenia

Korzystając z Service Workera mamy możliwość podpięcia się pod różnego rodzaju zdarzenia. Część z nich związana jest z cyklem życia (Lifecycle Events), co daje nam możliwość przeprowadzania inicjalizujących operacji w konkretnym momencie. Pozostałe natomiast związane są z różnymi funkcjonalnościami jakie daje nam SW (Functional Events).

Do najczęściej wykorzystywanych eventów należą:

  • install – emitowany po wstępnym parsowaniu SW, w trakcie jego instalacji; w tym miejscu najczęściej następuje pre-caching, bardziej omówiony w rozdziale Precaching. W tym miejscu możemy również pominąć etap oczekiwania za pomocą wspomnianej wcześniej funkcji ServiceWorkerGlobalScope.skipWaiting().
  • activate – emisja następuje po zakończeniu instalacji; w tym miejscu możemy dokończyć operacje związane z inicjalizacją, dokonać czyszczenia po poprzednim SW,
  • message – zdarzenie związane z komunikacją między SW a aplikacją przy użyciu PostMessage API, bardziej szczegółowo opisane w kolejnym rozdziale,
  • fetch – zdarzenie związane z wszelkimi wychodzącymi requestami z aplikacji, co daje nam możliwość realizacji różnych strategii cache’owania; SW działa tutaj jako interceptor.

Po pełną listę obsługiwanych zdarzeń odsyłamy do draftu W3C. Znajdują się tam m.in. zdarzenia umożliwiające przeprowadzanie synchronizacji w tle (Background Synchronization API) czy też użycie Push Notifications.

Komunikacja

Jak wcześniej wspomnieliśmy przy okazji omawiania dostępnych zdarzeń, komunikacja między aplikacją a Service Workerem odbywa się przy użyciu PostMessage API.

W praktyce sprowadza się to do wysyłania oraz nasłuchiwania na wiadomości przekazywane wraz z zdarzeniem message.

W przypadku SW, wiadomość wysyłamy uzyskując najpierw instancję Client a następnie wywołując na nim metodę Client.postMessage. Możemy to zrobić wywołując metodę Clients.matchAll() lub Clients.get() jeżeli mamy dostęp do identyfikatora klienta.

Od strony aplikacji natomiast, komunikacja jest realizowana przy użyciu instancji ServiceWorkerContainer dostępnej w globalnym obiekcie Navigator.serviceworker. Wówczas mamy dostęp do metody postMessage odpowiadającej za wysyłanie wiadomości do SW oraz do propercji ServiceWorkerContainer.onmessage, będącej jednocześnie funkcją wywoływaną podczas nadejścia wiadomości.

Mechanizm ten wykorzystamy w kolejnej części artykułu poświęconej integracji, w celu przekazywania informacji do klienta o nowej dostępnej grafice.

Precaching

Źródła oraz zasoby aplikacji możemy podzielić na statyczne oraz dynamiczne, podlegające modyfikacji w trakcie jej działania. Do pierwszych możemy zaliczyć np. obfuskowane pliki zawierające kod wykonawczy aplikacji, style czy też assetsy. Do zasobów zmiennych z kolei należą głównie cache’owane odpowiedzi z serwera, grafiki.

Mając to rozróżnienie na uwadze, samo zapisanie stałych zasobów aplikacji do pamięci podręcznej możemy zrealizować już na etapie instalacji SW. Podejście to nazywamy PreCachingiem, gdyż operacja ta jest przeprowadzana przed aktywacją SW.

Można się również spotkać z określeniem cache warming, które mówi o przeniesieniu części logiki cache’ującej w trakcie działania aplikacji (runtime) do metody wywoływanej podczas instalacji SW.

Wersjonowanie

“There are only two hard things in Computer Science: cache invalidation and naming things.”

Phil Karlton.

Sama idea cache’owania zasobów aplikacji w celu optymalizacji jej wydajności jest jak najbardziej słuszna, niestety jak to zwykle bywa, jest problematyczna w utrzymaniu.

W przypadku PWA najwięcej kłopotów sprawia przypadek, gdy pojawia się nowa wersja SW. Wówczas nowszy SW może wysyłać requesty po nowe dane, które w dalszym ciągu będą jednak obsługiwane przez aktualnie aktywnego, co może prowadzić do nieprzewidzianych zachowań.

Rozwiązaniem powyższego problemu jest wersjonowanie cache’a na podstawie aktualnej wersji aplikacji. Jako że podczas uruchamiania aplikacji, poprzedni SW w dalszym ciągu zaciągnie zaktualizowane źródła do starego cache’a, podczas aktywacji (zdarzenie activate) naszego nowego SW, musimy zadbać o wyczyszczenie danych z poprzedniego cache’a.

Szczegółowe informacje wraz z przykładami można znaleźć w dokumentacji MDN.

Strategie Cache’owania

Decydując się na użycie SW z pewnością będziemy chcieli skorzystać z możliwości cache’owania danych. W zależności od potrzeb aplikacji wyróżniamy kilka głównych strategii:

  1. Stale-while-revalidate – strategia ta umożliwia serwowanie danych z cache’a, przeprowadzając w międzyczasie synchronizację w tle z serwerem w celu aktualizacji danych. Jest to najczęściej wykorzystywane podejście w przypadku aplikacji, gdzie aktualność danych nie jest priorytetem.
  2. Network first (online first) – podejście to polega na tym, że w pierwszej kolejności próbujemy pobrać dane ze zdalnego źródła, a w momencie gdy się to nie powiedzie, wykorzystujemy dane z pamięci podręcznej.
  3. Cache first (offline first) – w przeciwieństwie do powyższej strategii, w pierwszej kolejności pobierane są dane aktualnie zawarte w pamięci podręcznej. W przypadku gdy nie są one dostępne, pobieramy je z sieci.

Więcej informacji na ten temat można znaleźć w dokumentacji WorkBoxa. Po bardziej zaawansowane strategie odsyłamy na blog Jake’a Archibalda.

Wiele Service Workerów

Pisząc naszą aplikację może się zdarzyć, że będziemy chcieli wykorzystać wiele SW w obrębie wspólnego scope’a. Możliwa jest również sytuacja, gdy kilka SW będzie nasłuchiwać na te same zdarzenie. Przywołując akapit dotyczący scope’a wiemy, że w obrębie danego scope’a może funkcjonować tylko jeden Service Worker.

W przypadku rejestracji wielu SW, faktycznie funkcjonować będzie ten, który później się zarejestruje, co raczej nie rozwiązuje naszego problemu. Co w takim wypadku powinniśmy zrobić?

Z pomocą przychodzi nam metoda WorkerGlobalScope.importScripts(), umożliwiająca zaimportowanie definicji innego SW. W skrócie, rozwiązanie to polega na zmergowaniu dwóch lub więcej implementacji w pojedynczego SW.

W przypadku kilku metod nasłuchujących na to samo zdarzenie, kolejność ich wykonywania zależy od umiejscowienia importScripts() oraz samych definicji tych metod. Zasada jest prosta i polega na tym, że w pierwszej kolejności wykonywana jest logika po prostu wcześniej zdefiniowanych handlerów.

Dodatkowo, w przypadku przechwytywania zapytań wychodzących (fetch) kolejne metody nasłuchujące na dane zdarzenie będą wywoływane tylko jeśli poprzednia nie zakończyła się wywołaniem FetchEvent.respondWith().

Należy również wspomnieć o tym, że zasoby importowane przy użyciu importScripts()cache’owane z automatu.

W kolejnej części artykułu poświęconej integracji wykorzystujemy ten mechanizm w ramach rozszerzania Angular Service Workera o komunikację z aplikacją na temat dostępności nowych danych.

Recipes

Implementacja SW bywała niestety problematyczna i zawierała wiele boilerplate kodu. Jako że funkcje wykonywane przez SW są wspólne dla większości aplikacji typu PWA, Mozilla wraz z innymi kontrybutorami stworzyła zestaw “przepisów” w jaki sposób pewne funkcjonalności można zrealizować, dostępny pod adresem serviceworke.rs. Jest to jednocześnie świetna dokumentacja API, znacząco ułatwiająca zrozumienie za co dany kawałek kodu odpowiada.

Angular Service Worker

W przypadku aplikacji Angularowych, wsparcie dla PWA realizowane jest przy użyciu paczki @angular/service-worker. Udostępnia ona dedykowany moduł, przy użyciu którego konfigurujemy naszą aplikację.

Dodatkowo zawiera serwisy opakowujące zdarzenia emitowane przez SW umożliwiające realizację operacji związanych z aktualizacjami (SwUpdate) oraz wykorzystaniem Push Notifications przy użyciu serwisu SwPush.

Paczka ta definiuje SW zawierającego implementację szeregu mechanizmów takich jak cache, Push Notifications, background sync. Oznacza to m.in., że możliwa jest realizacja strategii zarówno Offline-first, Online-first jak i stale-while-revalidate.

To które z tych funkcjonalności nasza aplikacja będzie wykorzystywać zależy od konfiguracji w pliku ngsw-config.json. W skrócie definiuje on zachowanie naszej aplikacji w przypadku pobierania różnych zasobów. Szczegółowe omówienie dostępnych opcji znajduje się w dokumentacji Service Worker Config.

Dodatkową zaletą użycia NGSW jest jego integracja z aplikacją Angularową, a konkretniej mamy możliwość konfiguracji momentu rejestracji SW, nie zakłócając tym samym działania naszej aplikacji (więcej o tym powiemy w kolejnej części artykułu).

Przedstawione rozwiązanie użycia gotowego SW, którego można skonfigurować do swoich potrzeb jest świetne, w przypadku gdy chcemy skorzystać z podstawowych funkcjonalności.

Problemy zaczynają się w przypadku gdy nasza aplikacja wymaga czegoś niestandardowego. Wówczas możliwe jest “rozszerzenie” Angularowego SW o dodatkowe funkcjonalności. Proces ten zostanie opisany szczegółowo w kolejnej części artykułu.

WorkBox

Alternatywnym podejściem jest wykorzystanie biblioteki WorkBox. Jest to zestaw gotowych paczek wyspecjalizowanych w konkretnych celach, rozwijany przez inżynierów z Google’a. Pełna lista dostępnych modułów znajduje na oficjalnej stronie WorkBox.

Użycie sprowadza się do implementacji SW przy wykorzystaniu wspomnianych paczek, z odpowiednią konfiguracją spełniającą nasze wymagania. Przykładowe wykorzystanie zostanie przedstawione w kolejnej części artykułu

Podsumowanie

Aplikacje progresywne zdecydowanie są interesującą propozycją jeśli chodzi o wsparcie dla platform mobilnych czy też desktopowych. Wykorzystując udostępniane nam funkcjonalności w odpowiedni sposób możemy znacznie poprawić User Experience naszej aplikacji.

Należy jednak liczyć się z tym, że tego typu aplikacje w dalszym ciągu pod wieloma względami odbiegają od aplikacji natywnych, będąc bardziej swego rodzaju alternatywą dla produktów hybrydowych (patrz PWA vs Native vs Hybrid).

Obecnie każda przeglądarka oraz większość platform posiada wsparcie dla PWA, dlatego uważamy, że jest to propozycja którą zdecydowanie warto rozważyć.

Niebawem ukaże się kolejna część artykułu, w którym skupimy się na praktycznym wykorzystaniu PWA oraz przedstawimy proces integracji przykładowej aplikacji Angularowej. Stay patient!

O autorze

Marcin Leśniczek

Wiecznie głodny wiedzy z pasją dla aplikacji mobilnych oraz hybrydowych. Zawsze otwarty na nowe pomysły i technologie, szukający dziury w całym. Po godzinach entuzjasta nauki i astronomii.

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.

Jeden komentarz

  1. Adam

    Świetny artykuł wprowadzający do PWA.

    Pracowałem krótki czas nad rozwojem aplikacji PWA, i mam takie przemyślenia, że PWA jest dobre dla aplikacji webowych, w celu poprawy UX. Jednak moim zdaniem wybór tej technologii do wytwarzania aplikacji tylko mobilnej jest trochę dyskusyjny. Mimo wszystko jest to całkiem ciekawa technologia, dająca dużo nowych możliwości. Zastanawiam się trochę jak rozwija się aplikacje oparte na ionicu + angular – słyszałem głównie negatywne opinie.

    Dzięki za ten artykuł ?

Dodaj komentarz

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