Wróć do strony głównej
Angular

PWA (Progressive Web Apps) w Angular – progresywne aplikacje w Angular

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. Angular PWA
    3.1. Angular.json
    3.2. App.module
    3.3. Index.html
    3.4. Ikony
    3.5. Service Worker Service
    3.6. Repository Service
  4. Serwer
  5. Recipes
    5.1. Service Worker
  6. Angular Service Worker
  7. WorkBox
  8. Recipes vs Angular Service Worker vs WorkBox
    8.1. Recipes
    8.2. Angular Service Worker
    8.3. WorkBox
  9. Podsumowanie
  10. Przydatne linki

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

Service Worker

Nadszedł nareszcie czas na przedstawienie procesu integracji istniejącej aplikacji z Service Workerami. W obrębie tego artykułu przedstawimy trzy różne dostępne podejścia jeśli chodzi o przekształcanie projektu w PWA.

Na potrzeby artykułu stworzyliśmy prostą aplikację mającą za zadanie wyświetlanie grafiki pobranej z API wystawionego przez nasz serwer. Sama grafika zwracana przez API zmienia się co 5 sekund.

Nasze SW natomiast będą realizowały strategię stale-while-revalidate, co oznacza, że wyświetlana grafika będzie pobierana z cache’a, natomiast w tle przeprowadzana będzie synchronizacja.

W momencie, gdy podczas uruchomienia aplikacji nowa grafika będzie dostępna, powinien zostać wyświetlony SnackBar informujący użytkownika o dostępnej aktualizacji. Gdy użytkownik wyrazi chęć pobrania nowych danych, aplikacja zostanie przeładowana.

Całość prezentuje się następująco:

Poglądowa aplikacja.

Jeśli chodzi o samą aplikację, gotowy kod źródłowy dostępny jest w naszym repozytorium. Zachęcamy do pobrania i eksperymentowania, a po więcej informacji odsyłamy do README.md.

Angular PWA

Aby przekształcić naszą aplikację w PWA musimy dodać paczkę @angular/pwa. W tym celu wykonujemy polecenie:

Uwaga! Istotne aby nie używać w tym celu Nx CLI.

Powyższe polecenie spowoduje wygenerowanie Service Workera mającego za zadanie cache’owanie zasobów naszej aplikacji. Szczegółowy proces jest zawarty w dokumentacji Angulara.

Po dodaniu wspomnianej paczki nasza aplikacja została dodatkowo spatchowana w kilku miejscach.

Angular.json

W pliku konfiguracyjnym zostały wprowadzone zmiany powodujące budowanie i kopiowanie predefiniowanego Angular Service Workera wraz z naszą aplikacją.

Zmiany w Angular.json.

Jak widać na powyższym porównaniu, do zasobów naszej aplikacji został dołączony manifest, włączona została flaga serviceWorker oraz ustawiona została ścieżka do ngswConfigPath, zawierającego konfigurację dla budowanego Service Workera.

Wspomniany Web App Manifest zawiera podstawowe informacje o naszej aplikacji oraz odpowiada za to w jaki sposób będzie prezentowana użytkownikowi po jej instalacji. Po więcej informacji odsyłamy do dokumentacji MDN lub do artykułu web.dev omawiającego pokrótce większość dostępnych opcji.

App.module

Moduł naszej aplikacji dodatkowo został wzbogacony o ServiceWorkerModule, który odpowiada za zarejestrowanie samego Service Workera.

Zmiany w app.module.ts.

Index.html

Wspomniany wyżej Web App Manifest musi zostać jeszcze uwzględniony w naszym index.html, wówczas przeglądarka po otwarciu strony z naszą aplikacją będzie wiedziała, że jest to PWA. Oprócz tego domyślnie zostaje ustawiony theme-color, odpowiadający za kolorystykę baneru przeglądarki w trakcie odwiedzin.

Zmiany w index.html.

Ikony

Oprócz wspomnianych powyżej zmian do aplikacji dołączane są ikony, które będą wykorzystywane w PWA (ikona samej aplikacji, ikona na SplashScreenie).

Niestety ng serve nie wspiera PWA, dlatego aby przetestować poprawność integracji naszej aplikacji musimy skorzystać z osobnego serwera HTTP. Możemy w tym celu użyć http-server (więcej szczegółów znajdziemy w dokumentacji).

Dodatkowym ograniczeniem jest konieczność instalacji paczki @angular/pwa tylko w obrębie modułu samej aplikacji (konieczne jest zaimportowanie ServiceWorkerModule w app.module), nie można jej zainstalować w obrębie innego modułu lub biblioteki.

Service Worker Service

Aby zapewnić komunikację między SW oraz naszą aplikacją, musimy nasłuchiwać na zdarzenie message definiując właściwość ServiceWorkerContainer.onmessage. Na tę potrzebę zdefiniowany został ServiceWorkerService, mający metodę rozpoczynającą nasłuchiwanie.

Repository Service

W celu wydzielenia logiki pobierającej zdalne zasoby utworzony został osobny serwis RepositoryService. Na tą chwilę zawiera on pojedynczą metodę pobierającą grafikę z naszego serwera, który utworzymy w kolejnym rozdziale.

Serwer

W ramach naszej integracji konieczne jest również utworzenie serwera wystawiającego API do pobierania grafiki zmieniającej się co 5 sekund.

W tym celu musimy mieć zainstalowany plugin @nrwl/nest, z którego skorzystamy w celu wygenerowania serwerowej aplikacji NestJS. Wykonujemy wówczas polecenia:

 

oraz

Implementacja sprowadza się do wystawienia endpointa, po zapytaniu którego zwracany jest jeden z dostępnych obrazków. Kod serwisu odpowiedzialnego za ustalanie grafiki sprowadza się do następującej metody:

Metoda odpowiedzialna za zmianę serwowanej grafiki.

Kluczowym punktem jest ustawienie ETagu w nagłówku odpowiedzi na unikalną wartość odpowiadającą aktualnej grafice, na podstawie którego nasza aplikacja wykryje zmianę grafiki.

Dodatkowym krokiem, który musimy jeszcze wykonać jest zezwolenie na dostęp do wspomnianego wyżej nagłówka po stronie klienta. Zmiany należy wprowadzić w pliku main.ts.

Konfiguracja serwera dająca dostęp do wybranego nagłówka po stronie klienta.

Recipes

Bazując na konfiguracji którą przeprowadziliśmy w poprzednich rozdziałach, możemy w końcu zabrać się za implementację Service Workera.

Na początek zaczniemy bez korzystania z gotowych bibliotek a jedynie z Service Worker API. W celu realizacji strategii stale-while-revalidate możemy skorzystać z gotowego przepisu.

Service Worker

Implementacja sprowadza się do kilku kluczowych punktów oraz metod. Podczas instalacji SW do pamięci podręcznej wrzucane są stałe pliki. W przypadku naszej aplikacji sprowadza się to do pliku index.html, ikony aplikacji oraz manifestu.

PreCaching zasobów aplikacji.

Najważniejsza logika natomiast znajduje się w metodzie wywoływanej na zdarzeniu fetch, czyli za każdym razem gdy jakikolwiek zasób jest pobierany.

Metoda wywoływana gdy zasób aplikacji jest requestowany.

Jak widać powyżej, w momencie gdy aplikacja wysyła żądanie po jakiś zasób, jest on odczytywany z pamięci podręcznej. W międzyczasie jest on pobierany, zapisywany w cache’u oraz wysyłana jest wiadomość do aplikacji o dostępności nowych danych.

Należy tutaj zwrócić uwagę, że mechanizm ten jest wykorzystywany dopiero w momencie, gdy SW zostaje aktywowany. Aktywacja natomiast następuje w zależności od registrationStrategy ustawionej podczas rejestracji SW.

W przypadku registerImmediately, SW jest rejestrowany natychmiastowo i przy użyciu Clients.claim() możemy od razu przejąć kontrolę nad stroną, zapisując do pamięci podręcznej dane pochodzące z pierwszych requestów.

Gdy jednak nie mamy możliwości przechwytywania już pierwszych żądań, aktywacja SW nastąpi po przeładowaniu aplikacji, dopiero wówczas żądania będą cache’owane.

Aby złagodzić powyższy problem, można ewentualnie pokusić się o cache’owanie większej ilości plików podczas instalacji, nie rozwiąże to natomiast kwestii z grafikami dostarczanymi przez nasz serwer. Tutaj z kolei możemy zastosować mechanizm fallbacku, wyświetlając jakiś placeholder zamiast aktualnej grafiki. To jakie ostateczne rozwiązanie zastosujemy zależy od charakteru aplikacji oraz wymaganych upodobań.

Ostatnią rzeczą jest usunięcie właściwości dotyczących Angular Service Worker z pliku angular.json tak aby nie kopiować nie wykorzystywanego NGSW oraz zminimalizować ostateczną wielkość aplikacji.

Zmiany w angular.json mówiące o tym, że nie chcemy korzystać z NGSW.

Angular Service Worker

W naszej aplikacji wykorzystanie predefiniowanego Angular Service Workera sprowadza się do odpowiedniej konfiguracji w pliku ngsw-config.json.

Najistotniejszym punktem jest ustawienie dataGroups definiującego mechanizm cache’owania zasobów pobieranych z określonych adresów. W przypadku naszej aplikacji, najbardziej interesuje nas cache’owanie grafiki pobieranej z serwera przy użyciu strategii stale-while-revalidate, a można to zrealizować ustawiając odpowiednie wartości pól strategy oraz timeout.

Cacheowanie obrazków przy użyciu strategii stale-while-revalidate.

W tym momencie pozostaje nam jedynie implementacja komunikacji z SW do naszej aplikacji o dostępnych nowych danych.

Realizujemy to poprzez utworzenie nowego Service Workera, który nasłuchuje na zdarzenie fetch i w przypadku gdy dotyczy to interesujących nas danych, wysyła wiadomość do klienta. Aby nie utracić przy okazji funkcjonalności NGSW, musimy skorzystać z WorkerGlobalScope.importScripts() o którym była mowa w poprzednim artykule, w rozdziale “Wiele Service Workerów”.

Pomijając metodę refresh(), całość wygląda następująco:

 

Rozszerzenie Angular Service Worker.

WorkBox

W przypadku definicji Service Workera przy użyciu modułów WorkBox, nasza implementacja sprowadza się do skorzystania z jednej z metod udostępnianych przez paczkę workbox-recipes.

Stosując podobne rozwiązanie co w przypadku Angular Service Worker, rozszerzamy naszą implementację o wysyłanie wiadomości do klienta o dostępnej nowej grafice.

Implementacja SW przy użyciu WorkBoxa.

Należy również tutaj zwrócić uwagę, że podczas parsowania naszego SW pobierana jest biblioteka workbox-sw z CDN, udostępniająca globalny obiekt workbox, z którego to wyciągamy interesujące nas moduły. Używane przez nas moduły są automatycznie cache’owane.

Dodatkowo, analogicznie jak w implementacji przy użyciu Przepisu, usuwamy właściwości dotyczące Angular Service Workera z pliku angular.json.

Recipes vs Angular Service Worker vs WorkBox

Po przekształcaniu naszej aplikacji w PWA oraz integracji z różnego rodzaju Service Workerami, mamy mniej więcej pogląd na to w jaki sposób możemy to zrobić oraz czym różnią się przedstawione podejścia. Spróbujmy zatem podsumować w kilku słowach zebraną wiedzę oraz informacje.

Recipes

Surowa implementacja Service Workera przy użyciu Service Worker API zawiera stosunkowo dużo boilerplate kodu w porównaniu do pozostałych. Problemy sprawia również wersjonowanie cache’a (trzeba ręcznie zmieniać nazwę wraz ze zmianą wersji), a implementacja niektórych funkcjonalności staje się niewspółmiernie zawiła.

Jest to natomiast doskonałe miejsce do rozpoczęcia swojej przygody z SW, gdyż tylko w ten sposób w pełni zrozumiemy co dzieje się “pod spodem”. Ponadto mamy pełną kontrolę nad zachowaniem SW więc możemy implementować swoje własne, niestandardowe rozwiązania.

Należy jednak liczyć się z tym, że niekoniecznie będą one bezpieczne, w tym przypadku bezpieczeństwo aplikacji leży całkowicie po stronie programisty. Dodatkowo próg wejścia jest stosunkowo wysoki w porównaniu do alternatywnych rozwiązań.

Angular Service Worker

Jeśli chodzi o Angular Service Worker, świetne sprawdza się w przypadku aplikacji Angularowych, które chcą skorzystać tylko z podstawowych funkcjonalności PWA takich jak cache’owanie czy Push Notifications. Proces instalacji jest prosty a implementacja zazwyczaj sprowadza się do uzupełnienia kilku właściwości w pliku konfiguracyjnym.

Kłopoty zaczynają się gdy chcemy wprowadzić jakąś naszą własną logikę, wówczas musimy rozszerzać bazową implementację.

Niekoniecznie również sposób w jaki pewne funkcjonalności są rozwiązane w Angular Service Worker będą odpowiadały naszym potrzebom, wówczas nawet rozszerzanie może być niewystarczające. W takim wypadku możemy skorzystać z WorkBoxa.

WorkBox

Korzystając z WorkBoxa pozbywamy się znacznej części boilerplate kodu. Dodatkowo mamy dużą kontrolę nad zachowaniem Service Workera, możemy dowolnie konfigurować indywidualne moduły ze sobą, realizując różnego rodzaju strategie, a nawet definiować własne strategie czy też pluginy.

Jeżeli w wymaganiach naszej aplikacji znajdują się niestandardowe funkcjonalności, jest to zdecydowanie najlepsza propozycja. Dodatkowo inżynierowie z Google’a postarali się również o doskonałą dokumentację wraz z faktycznymi przykładami użycia.

Podsumowanie

Korzystając z PWA z pewnością poprawimy User Experience naszej aplikacji. Istnieje kilka dostępnych sposobów na jej przekształcenie w PWA, a główne z nich przedstawiliśmy Wam w ramach naszego artykułu.

Każde z omawianych podejść ma swoje lepsze i gorsze strony i to które z nich ostatecznie wybierzemy zależy w dużej mierze od stawianych wymagań aplikacji.

Zbierając do kupy omówioną wiedzę przedstawiamy krótkie graficzne zestawienie omawianych podejść w wybranych kategoriach.

Kompleksowe porównanie wybranych aspektów implementacji Service Workerów na różne sposoby.

Mamy nadzieję, że artykuł rzucił trochę światła na ten ciągle ewoluujący i rozległy świat Progressive Web Apps i że również wynieśliście z naszego artykułu coś nowego i wartościowego.

Przydatne linki

  1. https://developers.google.com/web/tools/workbox/modules
  2. https://angular.io/guide/service-worker-getting-started
  3. https://serviceworke.rs/
  4. https://pwa-fundamentals.nl/chapters/service-workers.html
  5. https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers
  6. https://web.dev/two-way-communication-guide/
  7. https://jakearchibald.com/2014/offline-cookbook
  8. https://stackoverflow.com/questions/45257602/sharing-fetch-handler-logic-defined-across-multiple-service-workers
  9. https://developers.google.com/web/fundamentals/primers/service-workers
  10. https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
  11. https://wicg.github.io/background-sync/spec/
  12. https://whatwebcando.today/

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 *