Tworzenie aplikacji PWA w Google, część 1

Czego zespół Bulletin dowiedział się o skryptach service worker podczas tworzenia aplikacji PWA.

Douglas Parker
Douglas Parker
Joel Riley
Joel Riley
Dikla Cohen
Dikla Cohen

To pierwszy z serii postów na blogu o wnioskach wyciągniętych przez zespół Google Bulletin podczas tworzenia aplikacji PWA przeznaczonej dla użytkowników zewnętrznych. W tych postach będziemy informować o niektórych wyzwaniach, jakie napotkaliśmy, o tym, jak je przezwyciężyć, a także ogólne wskazówki na temat unikania pułapek. Nie oznacza to jednak pełnego omówienia aplikacji PWA. Chcemy dzielić się wiedzą zdobytą przez nasz zespół.

W pierwszym poście przedstawimy podstawowe informacje, a potem zagłębimy się w wszystkie rzeczy, których dowiedzieliśmy się o skryptach usługowych.

Wprowadzenie

Aplikacja Bulletin była rozwijana od połowy 2017 roku do połowy 2019 roku.

Dlaczego zdecydowaliśmy się stworzyć PWA

Zanim przyjrzymy się procesowi programowania, sprawdźmy, dlaczego utworzenie aplikacji PWA było atrakcyjną opcją w tym projekcie:

  • Możliwość szybkiego wprowadzania poprawek. Przydatna szczególnie wtedy, gdy Bulletin będzie wdrażany na wielu rynkach.
  • Jeden kod jako baza. Użytkownicy zostali podzieleni mniej więcej po równo między Androida i iOS. PWA oznaczało, że mogliśmy stworzyć jedną aplikację internetową, która działała na obu platformach. Zwiększyło to szybkość i wpływ całego zespołu.
  • Aktualizacje są szybkie i niezależne od zachowań użytkowników. Progresywne aplikacje internetowe mogą aktualizować się automatycznie, co zmniejsza liczbę nieaktualnych klientów działających bez ograniczeń. W krótkim czasie udało nam się wprowadzić najważniejsze zmiany w backendzie klientów.
  • Łatwa integracja z aplikacjami własnymi i innych firm. Taka integracja była wymagana w przypadku aplikacji. W przypadku PWA często oznaczało to samo otwarcie adresu URL.
  • Uproszczone instalowanie aplikacji.

Nasze zasady

W usłudze Bulletin użyliśmy oprogramowania Polymer, ale możesz użyć każdej nowoczesnej, dobrze wspieranej platformy.

Czego dowiedzieliśmy się o skryptach service worker

Nie można opracować aplikacji PWA bez skryptu service worker. Skrypty service worker mają spore możliwości, np. zaawansowane strategie buforowania, możliwość pracy w trybie offline, synchronizację w tle itp. Chociaż mechanizmy Service Worker zwiększają złożoność procesów, odkryliśmy, że ich zalety przewyższają dodatkową złożoność.

Wygeneruj ją, jeśli możesz

Unikaj ręcznego pisania skryptu skryptu service worker. Ręczne zapisywanie instancji service worker wymaga ręcznego zarządzania zasobami pamięci podręcznej i przepisywania logiki typowej dla większości bibliotek mechanizmów Service Worker, takich jak Workbox.

Jednak ze względu na nasz wewnętrzny stos technologiczny nie mogliśmy użyć biblioteki do wygenerowania skryptu service worker i zarządzania nim. Z naszych wniosków wynika, że Więcej informacji znajdziesz w artykule Problemy związane z niewygenerowanymi skryptami service worker.

Nie wszystkie biblioteki są zgodne z mechanizmami Service Worker

Niektóre biblioteki JS uruchamiane przez skrypt service worker nie działają zgodnie z oczekiwaniami. Przy założeniu, że dostępne są na przykład window lub document albo interfejs API jest niedostępny dla mechanizmów Service Worker (XMLHttpRequest, pamięć lokalna itp.). Upewnij się, że wszystkie newralgiczne biblioteki potrzebne aplikacji są zgodne z mechanizmami Service Worker. W przypadku tej konkretnej aplikacji PWA chcieliśmy użyć do uwierzytelniania pliku gapi.js, ale nie było to możliwe, ponieważ nie obsługuje on mechanizmów Service Worker. Autorzy bibliotek powinni też w miarę możliwości ograniczyć lub usunąć niepotrzebne założenia dotyczące kontekstu JavaScriptu, aby wspierać przypadki użycia mechanizmów Service Worker, na przykład przez unikanie interfejsów API niezgodnych z instancjami service worker i unikanie stanu globalnego.

Unikaj dostępu do IndexedDB podczas inicjowania

Nie odczytuj danych IndexedDB podczas inicjowania skryptu mechanizmu Service Worker, bo może się to zdarzyć w takiej sytuacji:

  1. Użytkownik ma aplikację internetową w wersji N IndexedDB (IDB)
  2. Nowa aplikacja internetowa została przekazana za pomocą IDB w wersji N+1
  3. Użytkownik otwiera PWA, co uruchamia pobieranie nowego skryptu service worker
  4. Nowy skrypt service worker odczytuje z IDB przed zarejestrowaniem modułu obsługi zdarzeń install, co aktywuje cykl uaktualniania IDB z N do N+1
  5. Użytkownik ma starego klienta w wersji N, więc proces uaktualniania instancji service worker zawiesza się, ponieważ aktywne połączenia są nadal otwarte na starej wersji bazy danych
  6. Skrypt service worker zawiesza się i nigdy nie instaluje

W naszym przypadku pamięć podręczna została unieważniona podczas instalacji skryptu service worker, więc jeśli nigdy nie został on zainstalowany, użytkownicy nie otrzymali zaktualizowanej aplikacji.

Zadbaj o odporność

Chociaż skrypty skryptu service worker działają w tle, mogą też zostać zakończone w dowolnym momencie, nawet w trakcie operacji wejścia-wyjścia (sieć, IDB itp.). Każdy długotrwały proces powinien być możliwy do wznowienia w dowolnym momencie.

W przypadku procesu synchronizacji, w którym duże pliki były przesyłane na serwer i zapisywane w IDB, naszym rozwiązaniem w przypadku przerw w przesyłaniu częściowych było wykorzystanie systemu wznawiania naszej wewnętrznej biblioteki przesyłania, zapisanie adresu URL przesyłania wznawianego w IDB przed przesłaniem i użycie tego adresu URL do wznowienia przesyłania, jeśli nie zostało ukończone za pierwszym razem. Również przed długotrwałą operacją wejścia-wyjścia stan pliku został zapisany w IDB, by wskazać, na którym etapie całego procesu byliśmy w przypadku każdego rekordu.

Nie zależą od stanu globalnego

Skrypty service worker występują w innym kontekście, więc może nie być dostępnych wiele symboli. Duża część naszego kodu działała zarówno w kontekście window, jak i w kontekście skryptu service worker (np. logowanie, flagi, synchronizacja itp.). Kod musi zabezpieczać używane usługi, takie jak pamięć lokalna czy pliki cookie. Za pomocą globalThis możesz odwoływać się do obiektu globalnego w sposób działający we wszystkich kontekstach. Z danych przechowywanych w zmiennych globalnych też należy korzystać z oszczędnością, ponieważ nie ma gwarancji, kiedy skrypt zostanie wyłączony, a stan nie zostanie trwale usunięty.

Programowanie lokalne

Głównym składnikiem mechanizmów Service Worker jest możliwe lokalne buforowanie zasobów. W fazie rozwoju rozwiązanie to jest jednak przeciwieństwem tego, czego oczekujesz, zwłaszcza gdy aktualizacje są przeprowadzane leniwie. Nadal potrzebujesz zainstalowanego mechanizmu serwerowego, aby można było debugować problemy lub pracować z innymi interfejsami API, takimi jak synchronizacja w tle czy powiadomienia. W Chrome możesz to zrobić przy użyciu Narzędzi deweloperskich w Chrome, zaznaczając pole wyboru Pomijaj dla sieci (panel Aplikacja > panel Skrypty robocze) oraz pole wyboru Wyłącz pamięć podręczną w panelu Sieć w celu wyłączenia pamięci podręcznej. Aby uwzględnić większą liczbę przeglądarek, wybraliśmy inne rozwiązanie, dodając flagę wyłączenia buforowania w naszym mechanizmie Service Worker, która jest domyślnie włączona w kompilacjach programistycznych. Dzięki temu deweloperzy zawsze uzyskują najnowsze zmiany bez problemów z buforowaniem. Pamiętaj też o umieszczeniu nagłówka Cache-Control: no-cache, by zapobiec buforowaniu zasobów przez przeglądarkę.

Latarnia morska

Lighthouse udostępnia wiele narzędzi do debugowania przydatnych w przypadku aplikacji PWA. Skanuje witrynę i generuje raporty obejmujące progresywne aplikacje internetowe, wydajność, ułatwienia dostępu, SEO i inne sprawdzone metody. Zalecamy uruchamianie Lighthouse w trybie ciągłej integracji, aby otrzymywać alerty o naruszeniu jednego z kryteriów wymaganych do uzyskania statusu PWA. Zdarzało się to przypadkiem, kiedy skrypt service worker nie instalujeł się, a przed przejściem do środowiska produkcyjnego nie zdawaliśmy sobie z tego sprawy. Wdrożenie narzędzia Lighthouse w ramach CI byłoby temu zapobiec.

Korzystaj z ciągłego dostarczania

Skrypty service worker mogą się automatycznie aktualizować, więc użytkownicy nie mają możliwości ograniczenia uaktualnień. Znacznie zmniejsza to liczbę nieaktualnych klientów w środowisku naturalnym. Gdy użytkownik otwiera aplikację, mechanizm Service Worker obsługuje starego klienta, ale leniwie pobiera nowego. Po pobraniu nowego klienta użytkownik zobaczy prośbę o odświeżenie strony, aby uzyskać dostęp do nowych funkcji. Nawet jeśli użytkownik zignoruje to żądanie, przy następnym odświeżeniu strony otrzyma nową wersję klienta. W związku z tym użytkownik może mieć trudności z odrzuceniem aktualizacji tak samo jak w przypadku aplikacji na iOS/Androida.

W krótkim czasie udało nam się wprowadzić zmiany powodujące niezgodności w backendzie z myślą o klientach. Zwykle dajemy użytkownikom miesiąc na aktualizację do nowszych wersji klientów, zanim wprowadzą one nieistotne zmiany. Ponieważ aplikacja będzie nieaktualna, użytkownicy starszych klientów mogliby bez obaw trafić na ich miejsce, gdyby użytkownik nie uruchamiał ich od dłuższego czasu. W systemie iOS mechanizmy Service Worker są usuwane po kilku tygodniach, więc taki przypadek nie występuje. W przypadku Androida ten problem można rozwiązać, jeśli treści nie wyświetlają się w czasie, gdy są nieaktualne lub tracą ważność po kilku tygodniach. W praktyce nie napotkaliśmy problemów ze strony nieaktualnych klientów. Wymagania danego zespołu zależą od konkretnego przypadku użycia, ale aplikacje PWA zapewniają znacznie większą elastyczność niż aplikacje na iOS/Androida.

Uzyskiwanie wartości plików cookie w skrypcie service worker

Czasami dostęp do wartości plików cookie w kontekście mechanizmu Service Worker jest konieczne. W naszym przypadku musieliśmy uzyskać dostęp do wartości plików cookie, aby wygenerować token do uwierzytelniania żądań do interfejsu API własnych. W mechanizmie Service Worker synchroniczne interfejsy API, takie jak document.cookies, są niedostępne. Zawsze możesz wysłać z skryptu service worker z prośbą o wartości plików cookie do aktywnych (sąsiadujących okna) klientów, chociaż może się on jednak uruchamiać w tle bez dostępnych klientów w oknie, np. podczas synchronizacji w tle. Aby obejść ten problem, utworzyliśmy punkt końcowy na naszym serwerze frontendu, który po prostu powtarza wartość pliku cookie z powrotem do klienta. Skrypt service worker wysłał żądanie sieciowe do tego punktu końcowego i odczytało odpowiedź, aby pobrać wartości plików cookie.

Po wprowadzeniu interfejsu Cookie Store API to obejście nie powinno być już potrzebne w przeglądarkach, które go obsługują, ponieważ zapewnia asynchroniczny dostęp do plików cookie w przeglądarce i może być używany bezpośrednio przez mechanizm Service Worker.

Problemy związane z niewygenerowanymi mechanizmami Service Worker

Sprawdź, czy skrypt skryptu service worker zmienia się w przypadku zmiany dowolnego statycznego pliku w pamięci podręcznej

Typowym wzorcem PWA jest instalowanie przez skrypt service worker wszystkich statycznych plików aplikacji w fazie install. Umożliwia to klientom bezpośrednie trafienie do pamięci podręcznej interfejsu Cache Storage API przy wszystkich kolejnych wizytach. Skrypty service worker są instalowane tylko wtedy, gdy przeglądarka wykryje, że w jakiś sposób zmienił się skrypt skryptu service worker, dlatego musieliśmy się upewnić, że po zmianie pliku w pamięci podręcznej sam plik skryptu service worker uległ zmianie. Zrobiliśmy to ręcznie, umieszczając hasz statycznego zbioru zasobów w naszym skrypcie service worker, dzięki czemu w każdej wersji pojawia się oddzielny plik JavaScript skryptu service worker. Biblioteki instancji Service Worker, takie jak Workbox, automatyzują ten proces.

Testowanie jednostkowe

Interfejsy API mechanizmu Service Worker można wykonać, dodając detektory zdarzeń do obiektu globalnego. Na przykład:

self.addEventListener('fetch', (evt) => evt.respondWith(fetch('/foo')));

Testowanie może być trudne, ponieważ przed ostatecznym zatwierdzeniem wyniku musisz wypróbować algorytm zdarzenia, obiekt zdarzenia, zaczekać na wywołanie zwrotne respondWith(), a następnie oczekiwać na obietnicę. Łatwiejszym sposobem na stworzenie takiej struktury jest delegowanie całej implementacji do innego pliku, co łatwiej sprawdzić.

import fetchHandler from './fetch_handler.js';
self.addEventListener('fetch', (evt) => evt.respondWith(fetchHandler(evt)));

Ze względu na trudności związane z testowaniem jednostkowym skryptu skryptu service worker zachowywaliśmy bardzo ogólny skrypt, dzieląc większość implementacji na pozostałe moduły. Ponieważ były to tylko standardowe moduły JS, można je było łatwiej testować jednostkowe za pomocą standardowych bibliotek testowych.

Wkrótce udostępnimy część 2 i 3

W częściach 2 i 3 tej serii zajmiemy się zarządzaniem multimediami i problemami związanymi z iOS. Jeśli chcesz zapytać nas więcej o tworzenie aplikacji PWA w Google, sprawdź, jak możesz się z nami skontaktować: