Tworzenie aplikacji PWA w Google, część 1

Co zespół ds. 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 omówimy niektóre problemy, z którymi się spotkaliśmy, sposoby ich rozwiązania oraz ogólne wskazówki, które pomogą Ci uniknąć błędów. Nie oznacza to jednak pełnego omówienia aplikacji PWA. Chcemy dzielić się wiedzą zdobytą przez nasz zespół.

W tym pierwszym poście przedstawimy trochę informacji ogólnych, a potem omówimy wszystko, co wiemy o usługach roboczych.

Tło

Bulletin był aktywnie rozwijany od połowy 2017 r. do połowy 2019 r.

Dlaczego zdecydowaliśmy się na tworzenie aplikacji internetowej

Zanim przejdziemy do procesu tworzenia, sprawdźmy, dlaczego w przypadku tego projektu tworzenie aplikacji internetowej na potrzeby tej platformy było atrakcyjną opcją:

  • Możliwość szybkiego wprowadzania poprawek. Jest to szczególnie cenna informacja, ponieważ testy pilotażowe dotyczące wiadomości byłyby przeprowadzane na wielu rynkach.
  • Jednolita baza kodu. 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ększyliśmy dzięki temu tempo pracy i wpływ zespołu.
  • Aktualizacje są szybkie i niezależne od zachowań użytkowników. PWA mogą się automatycznie aktualizować, co zmniejsza liczbę przestarzałych klientów w środowisku. W krótkim czasie udało nam się wprowadzić najważniejsze zmiany w backendzie klientów.
  • Łatwa integracja z aplikacjami własnymi i aplikacjami innych firm Takie integracje były wymagane w przypadku aplikacji. W przypadku aplikacji PWA często oznaczało to po prostu otwarcie adresu URL.
  • Uproszczenie procesu instalowania aplikacji.

Nasza platforma

W przypadku Bulletin użyliśmy Polymer, ale sprawdzi się też dowolna nowoczesna, dobrze obsługiwana platforma.

Czego się nauczyliśmy o skryptach service worker

Nie możesz mieć PWA bez usługodawcy. Skrypty service worker dają wiele możliwości, takich jak zaawansowane strategie buforowania, funkcje offline czy synchronizacja w tle. Chociaż skrypty service worker zwiększają złożoność, stwierdziliśmy, że korzyści z ich stosowania przeważają nad dodatkowymi trudnościami.

Jeśli to możliwe, wygeneruj

Unikaj ręcznego pisania skryptu usługi. Ręczne pisanie skryptów service worker wymaga ręcznego zarządzania zasobami w pamięci podręcznej i przepisywania logiki, która jest wspólna dla większości bibliotek service worker, takich jak Workbox.

Z powodu naszego wewnętrznego pakietu technologicznego nie mogliśmy jednak użyć biblioteki do generowania i zarządzania naszym service workerem. Nasze wnioski mogą to odzwierciedlać. Aby dowiedzieć się więcej, przeczytaj artykuł Pułapki w przypadku skryptów service worker niegenerowanych przez Google.

Nie wszystkie biblioteki są zgodne z usługami w tle

Niektóre biblioteki JS opierają się na założeniach, które nie działają zgodnie z oczekiwaniami, gdy są uruchamiane przez usługę workera. Na przykład: załóżmy, że interfejsy window lub document są dostępne, albo że używasz interfejsu API niedostępnego dla pracowników obsługi klienta (XMLHttpRequest, pamięć lokalna itp.). Upewnij się, że wszystkie niezbędne biblioteki w aplikacji są zgodne z serwerem workera. W przypadku tej konkretnej aplikacji internetowej chcieliśmy użyć do uwierzytelniania skryptu gapi.js, ale nie było to możliwe, ponieważ nie obsługiwał on skryptów service worker. Autorzy bibliotek powinni też w miarę możliwości ograniczyć lub usunąć niepotrzebne założenia dotyczące kontekstu JavaScriptu, aby obsługiwać przypadki użycia usług workera, np. unikając interfejsów API niezgodnych z usługami workera i unikanie stanu globalnego.

Unikaj dostępu do IndexedDB podczas inicjalizacji

Nie czytaj IndexedDB podczas inicjowania skryptu usługi, ponieważ może to spowodować niepożądaną sytuację:

  1. Użytkownik ma aplikację internetową z wersją N interfejsu IndexedDB (IDB)
  2. Nowa aplikacja internetowa jest wysyłana z wersją IDB N+1
  3. Użytkownik odwiedza PWA, co powoduje pobranie nowego modułu usługi
  4. Nowy skrypt service worker odczytuje dane z IDB, zanim zarejestruje event handlera install, powodując cykl uaktualniania IDB z N na N+1.
  5. Użytkownik ma starego klienta w wersji N, więc proces uaktualniania usługi pracującej w tle się zawiesza, ponieważ aktywne połączenia są nadal otwarte w starszej wersji bazy danych.
  6. Skrypt service worker zawiesza się i nigdy nie instaluje

W naszym przypadku pamięć podręczna została unieważniona podczas instalowania usługi w tle, więc jeśli usługa w tle nigdy nie została zainstalowana, użytkownicy nigdy nie otrzymali zaktualizowanej aplikacji.

Zadbaj o odporność

Chociaż skrypty usługi pracują w tle, można je w dowolnym momencie zakończyć, nawet w trakcie operacji wejścia/wyjścia (sieci, IDB itp.). Każdy długotrwały proces powinien być możliwy do wznowienia w dowolnym momencie.

W przypadku procesu synchronizacji, który przesyła duże pliki na serwer i zapisuje je w IDB, nasze rozwiązanie dotyczące przerwanych częściowych przesyłań polegało na wykorzystaniu systemu umożliwiającego wznowienie przesyłania w naszej wewnętrznej bibliotece przesyłania. Przed przesyłaniem zapisujemy adres URL umożliwiający wznowienie przesyłania w IDB, a jeśli przesyłanie nie zostało ukończone za pierwszym razem, używamy tego adresu URL do wznowienia przesyłania. Ponadto przed każdą długotrwałą operacją wejścia-wyjścia stan był zapisywany w pliku IDB, aby wskazywać, na jakim etapie procesu znajduje się każda pozycja.

Nie polegaj na stanie globalnym

Skrypty service worker działają w innym kontekście, więc wiele symboli, których się spodziewasz, nie jest obecnych. Wiele naszego kodu działało zarówno w kontekście window, jak i w kontekście usługi (np. rejestrowanie, flagi, synchronizacja itp.). Kod musi chronić usługi, których używa, takie jak pamięć lokalna czy pliki cookie. Możesz używać globalThisdo odwoływania się do obiektu globalnego w sposób, który będzie działać we wszystkich kontekstach. Zmiennych globalnych używaj też oszczędnie, ponieważ nie ma gwarancji, kiedy skrypt zostanie zakończony i stan zostanie usunięty.

Lokalny proces programowania

Głównym elementem usług w tle jest buforowanie zasobów lokalnie. W fazie rozwoju rozwiązanie to jest jednak przeciwieństwem tego, czego oczekujesz, zwłaszcza gdy aktualizacje są przeprowadzane leniwie. Nadal musisz mieć zainstalowanego agenta serwera, aby móc debugować problemy z tym narzędziem lub korzystać z innych interfejsów API, takich 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ć, które pozwala wyłączyć pamięć podręczną pamięci. Aby objąć więcej przeglądarek, zdecydowaliśmy się na inne rozwiązanie, czyli dodanie flagi do wyłączenia buforowania w naszym serwisie, która jest domyślnie włączona w wersjach dla deweloperów. Dzięki temu deweloperzy zawsze otrzymują najnowsze zmiany bez problemów z pamięcią podręczną. Ważne jest też uwzględnienie nagłówka Cache-Control: no-cache, aby uniemożliwić przeglądarce zapisywanie komponentów w pamięci podręcznej.

Latarnia morska

Lighthouse udostępnia kilka narzędzi do debugowania przydatnych w przypadku PWA. Skanuje witrynę i generuje raporty dotyczące m.in. progresywnych aplikacji internetowych (PWA), wydajności, ułatwień dostępu, SEO i innych sprawdzonych metod. Zalecamy uruchomienie 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. Użycie Lighthouse w naszym procesie CI zapobiegłoby temu problemowi.

Stosuj tryb ciągłego dostarczania

Skrypty service worker mogą się automatycznie aktualizować, więc użytkownicy nie mogą ograniczać uaktualnień. Dzięki temu znacznie zmniejsza się liczba przestarzałych klientów w środowisku naturalnym. Gdy użytkownik otwierał naszą aplikację, pracownik usługi wyświetlał starszego klienta, a w tym samym czasie pobierał nowego klienta. Po pobraniu nowego klienta użytkownik zostanie poproszony 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 efekcie użytkownikom trudno jest odrzucić aktualizacje w taki sam sposób jak w przypadku aplikacji na iOS lub 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 przejście na nowsze wersje klienta przed wprowadzeniem zmian. Aplikacja była wyświetlana, mimo że była nieaktualna, więc starsze klienci mogli nadal istnieć, nawet jeśli użytkownik nie otwierał aplikacji przez długi czas. W iOS usługa kończy działać po kilku tygodniach, więc w tym przypadku nie ma takiej możliwości. W przypadku Androida można ograniczyć ten problem, nie wyświetlając treści, które są stale dostępne, lub ręcznie ustawiając datę wygaśnięcia treści po kilku tygodniach. W praktyce nie napotkaliśmy problemów ze strony nieaktualnych klientów. To, jak rygorystyczne mają być te zasady, zależy od konkretnego przypadku użycia, ale PWA zapewniają znacznie większą elastyczność niż aplikacje na iOS lub Androida.

Pobieranie wartości plików cookie w usługach workera

Czasami w kontekście usługi roboczej konieczny jest dostęp do wartości plików cookie. W naszym przypadku musieliśmy uzyskać dostęp do wartości plików cookie, aby wygenerować token do uwierzytelniania żądań interfejsu API własnych. W mechanizmie Service Worker synchroniczne interfejsy API, takie jak document.cookies, są niedostępne. Z serwisowego workera zawsze możesz wysłać wiadomość do aktywnych (oknowanych) klientów, aby poprosić o wartości plików cookie. Jednak możliwe jest, że serwisowy worker będzie działać w tle bez żadnych dostępnych klientów oknowanych, 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. Pracownik usługi wysłał żądanie sieci do tego punktu końcowego i odczytał odpowiedź, aby uzyskać wartości plików cookie.

Po wydaniu interfejsu Cookie Store API to obejście nie powinno być już potrzebne w przypadku przeglądarek, które go obsługują, ponieważ zapewnia on asynchroniczny dostęp do plików cookie przeglądarki i może być używany bezpośrednio przez usługę roboczą.

Pułapki związane z niegenerowanymi skryptami 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 usługę workera wszystkich statycznych plików aplikacji w fazie install, co umożliwia klientom bezpośrednie korzystanie z pamięci podręcznej interfejsu Cache Storage API w przypadku wszystkich kolejnych wizyt. Usługi są instalowane tylko wtedy, gdy przeglądarka wykryje, że skrypt usługi uległ zmianie. Dlatego musieliśmy się upewnić, że sam plik skryptu usługi uległ zmianie, gdy zmienił się plik w pamięci podręcznej. Zrobiliśmy to ręcznie, umieszczając w skrypcie usługi pracownika zasobów hasz zestawu zasobów statycznych, dzięki czemu każda wersja generowała oddzielny plik JavaScript usługi pracownika. Biblioteki usług roboczych, takie jak Workbox, automatyzują ten proces.

Testowanie jednostkowe

Interfejsy API usług workera działają poprzez dodawanie odbiorników zdarzeń do obiektu globalnego. Na przykład:

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

Testowanie może być uciążliwe, ponieważ musisz zasymulować zdarzenie aktywujące i obiekt zdarzenia, zaczekać na wywołanie zwrotne respondWith(), a potem na obietnicę, zanim wreszcie przetestujesz wynik. Łatwiej jest delegować całą implementację do innego pliku, który jest łatwiejszy do przetestowania.

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 usługi zachowaliśmy go w jak najbardziej podstawowej formie, przenosząc większość implementacji do innych modułów. Ponieważ te pliki były tylko standardowymi modułami JS, można je było łatwiej testować za pomocą standardowych bibliotek testowych.

Części 2 i 3

W częściach 2 i 3 tej serii omówimy zarządzanie multimediami oraz problemy związane z iOS. Jeśli chcesz zapytać nas więcej o tworzenie aplikacji PWA w Google, sprawdź, jak możesz się z nami skontaktować: