Cykl życia skryptu service worker

Jake Archibald
Jake Archibald

Cykl życia usługi jest najbardziej skomplikowaną częścią. Jeśli nie wiesz, czego chce od Ciebie Google i jakie korzyści Ci to przyniesie, możesz mieć wrażenie, że jest Twoim wrogiem. Gdy już poznasz działanie tej funkcji, możesz dostarczać użytkownikom płynne i nieinwazyjne aktualizacje, łącząc najlepsze wzorce internetowe i natywne.

To szczegółowy przewodnik, ale punkty na początku każdej sekcji zawierają większość informacji, które są Ci potrzebne.

Intencja

Celem cyklu życia jest:

  • Uwzględnij tryb offline.
  • Umożliw nowemu skryptowi service worker przygotowanie się do działania bez zakłócania obecnego.
  • Upewnij się, że strona objęta zakresem jest kontrolowana przez ten sam skrypt service worker (lub żaden skrypt service worker) na całej stronie.
  • Upewnij się, że w danym momencie działa tylko jedna wersja witryny.

Ten ostatni jest bardzo ważny. Bez usługowych workerów użytkownicy mogą wczytywać jedną kartę Twojej witryny, a potem otworzyć kolejną. Może to spowodować jednoczesne działanie 2 wersji witryny. Czasami jest to w porządku, ale jeśli masz do czynienia z pamięcią, możesz łatwo skończyć z 2 kartami, które mają bardzo różne opinie na temat tego, jak zarządzać współdzieloną pamięcią. Może to spowodować błędy lub, co gorsza, utratę danych.

Pierwszy skrypt service worker

W skrócie:

  • Zdarzenie install to pierwsze zdarzenie, które otrzymuje usługa w ramach usługi w tle. Występuje tylko raz.
  • Obietnica przekazana do installEvent.waitUntil() sygnalizuje czas trwania i skuteczność instalacji.
  • Usługa wątek usługi nie będzie otrzymywać zdarzeń takich jak fetchpush, dopóki nie zakończy się jej instalacja i nie stanie się ona „aktywna”.
  • Domyślnie pobieranie strony nie przechodzi przez skrypt service worker, chyba że samo żądanie strony zostało przesłane przez skrypt service worker. Aby zobaczyć efekty działania usługi, musisz odświeżyć stronę.
  • clients.claim() może zastąpić tę domyślną wartość i przejąć kontrolę nad stronami, które nie są kontrolowane.

Weź ten kod HTML:

<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered!', reg))
    .catch(err => console.log('Boo!', err));

  setTimeout(() => {
    const img = new Image();
    img.src = '/dog.svg';
    document.body.appendChild(img);
  }, 3000);
</script>

Rejestruje usługę w tle i po 3 sekundach dodaje obraz psa.

Oto jego usługa sw.js:

self.addEventListener('install', event => {
  console.log('V1 installing…');

  // cache a cat SVG
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/cat.svg'))
  );
});

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

Zapisują w pamięci podręcznej obraz kota i wyświetlają go, gdy tylko pojawi się żądanie /dog.svg. Jeśli jednak uruchomisz powyższe przykładowe zapytanie, przy pierwszym załadowaniu strony zobaczysz psa. Kliknij odśwież, a zobaczysz kota.

Zakres i kontrola

Domyślny zakres rejestracji skryptu service worker to ./ w stosunku do adresu URL skryptu. Oznacza to, że jeśli zarejestrujesz skrypt service worker w adresie //example.com/foo/bar.js, jego domyślny zakres to //example.com/foo/.

Strony, pracowników i współużytkowanych pracowników nazywamy clients. Twój serwis worker może kontrolować tylko klientów, którzy znajdują się w zakresie. Gdy klient jest „kontrolowany”, pobiera go za pomocą workera usługi w zakresie. Możesz wykryć, czy klient jest kontrolowany za pomocą navigator.serviceWorker.controller, który będzie miał wartość null, czy instancji service workera.

Pobieranie, analizowanie i wykonywanie

Pierwszy pracownik usługi pobiera się, gdy wywołasz .register(). Jeśli skrypt nie może zostać pobrany, przeanalizowany lub wywołany z błędem podczas początkowego wykonania, obietnica rejestracji zostanie odrzucona, a usługa workera zostanie odrzucona.

Narzędzia deweloperskie w Chrome wyświetlają błąd w konsoli oraz w sekcji service worker na karcie aplikacji:

Błąd wyświetlany na karcie Narzędzi deweloperskich w przypadku skryptu service worker

Zainstaluj

Pierwszym zdarzeniem, które otrzymuje usługa, jest install. Jest ona wywoływana, gdy tylko pracownik zostanie uruchomiony, i tylko raz na pracownika usługi. Jeśli zmienisz skrypt usługi, przeglądarka uzna go za inną usługę i przypisze mu własne zdarzenie install. Aktualizacje omówię bardziej szczegółowo później.

Zdarzenie install to Twoja szansa na zapisanie w pamięci podręcznej wszystkiego, czego potrzebujesz, zanim będziesz mógł kontrolować klientów. Obietnica przekazywana do event.waitUntil() informuje przeglądarkę, kiedy instalacja została zakończona i czy zakończyła się pomyślnie.

Jeśli obietnica zostanie odrzucona, oznacza to, że instalacja się nie udała, a przeglądarka usuwa pracownika usługi. Nigdy nie będzie kontrolować klientów. Oznacza to, że możemy polegać na tym, że cat.svg jest obecny w pamięci podręcznej w naszych zdarzeniach fetch. Jest to zależność.

Aktywuj

Gdy Twój serwis worker będzie gotowy do kontrolowania klientów i obsługiwania zdarzeń funkcjonalnych, takich jak pushsync, otrzymasz zdarzenie activate. Nie oznacza to jednak, że strona o nazwie .register() będzie kontrolowana.

Gdy po raz pierwszy wczytujesz demo, mimo że usługa dog.svg jest wywoływana długo po aktywacji pracownika usługi, nie obsługuje ona żądania i nadal widzisz obraz psa. Domyślnie jest to zgodność, czyli jeśli strona wczytuje się bez skryptu usługi, jej zasoby podrzędne też nie będą dostępne. Jeśli wczytasz demo po raz drugi (czyli odświeżasz stronę), będzie ono kontrolowane. Strona i obraz zostaną przetworzone przez zdarzenia fetch, a zamiast nich zobaczysz kota.

clients.claim

Po aktywowaniu możesz przejąć kontrolę nad niezarządzanymi klientami, wywołując clients.claim() w ramach swojego service workera.

Oto wariant powyższego przykładu, który wywołuje funkcję clients.claim() w zdarzeniu activate. Należy zobaczyć kota. Mówię „powinien”, ponieważ jest to kwestia czasu. Kota zobaczysz tylko wtedy, gdy usługa robocza zostanie uruchomiona i wprowadzi clients.claim() w efekt przed próbą załadowania obrazu.

Jeśli używasz serwisu workera do wczytywania stron w inny sposób niż przez sieć, clients.claim() może być kłopotliwy, ponieważ serwis workera będzie kontrolować niektórych klientów, którzy wczytali się bez niego.

Aktualizacja serwisu workera

W skrócie:

  • Aktualizacja jest uruchamiana w jednym z tych przypadków:
    • Nawigacja po stronie objętej zakresem.
    • Zdarzenia funkcjonalne, takie jak pushsync, chyba że w ciągu ostatnich 24 godzin nastąpiła weryfikacja aktualizacji.
    • Wywoływanie .register() tylko wtedy, gdy adres URL usługi roboczej uległ zmianie. Należy jednak unikać zmiany adresu URL pracownika.
  • Większość przeglądarek, w tym Chrome 68 i nowsze, domyślnie ignoruje nagłówki pamięci podręcznej podczas sprawdzania aktualizacji zarejestrowanego skryptu usługi. Nadal uwzględniają nagłówki pamięci podręcznej podczas pobierania zasobów załadowanych w ramach instancji roboczej za pomocą importScripts(). Możesz zmienić to domyślne zachowanie, ustawiając opcję updateViaCache podczas rejestrowania pracownika usługi.
  • Twój serwis worker jest uważany za zaktualizowany, jeśli różni się od tego, który jest już w przeglądarce, o co najmniej 1 bajt. (rozszerzamy to na importowane skrypty i moduły).
  • Zaktualizowany serwis worker jest uruchamiany obok dotychczasowego i otrzymuje własne zdarzenie install.
  • Jeśli nowy worker ma kod stanu inny niż OK (np. 404), nie udaje mu się przeanalizować danych, zgłasza błąd podczas wykonywania lub odrzuca instalację, nowy worker jest odrzucany, ale bieżący pozostaje aktywny.
  • Po zainstalowaniu zaktualizowany proces roboczy będzie wait, dopóki istniejący proces roboczy nie będzie kontrolował żadnego klienta. (Pamiętaj, że podczas odświeżania klienci nakładają się na siebie).
  • self.skipWaiting() zapobiega oczekiwaniu, co oznacza, że usługa workera uruchamia się, gdy tylko zakończy się jej instalacja.

Załóżmy, że zmodyfikowaliśmy skrypt usługi, aby zamiast kota wyświetlał obraz konia:

const expectedCaches = ['static-v2'];

self.addEventListener('install', event => {
  console.log('V2 installing…');

  // cache a horse SVG into a new cache, static-v2
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  // delete any caches that aren't in expectedCaches
  // which will get rid of static-v1
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(() => {
      console.log('V2 now ready to handle fetches!');
    })
  );
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the horse SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

Obejrzyj prezentację powyższych funkcji Nadal powinieneś/powinnaś widzieć obraz kota. Oto dlaczego…

Zainstaluj

Zmieniłem nazwę pamięci podręcznej z static-v1 na static-v2. Oznacza to, że mogę skonfigurować nową pamięć podręczną bez nadpisywania elementów w obecnej pamięci, z której nadal korzysta stary worker usługi.

Ten wzór tworzy pamięć podręczną dla poszczególnych wersji, podobnie jak zasoby, które aplikacja natywna łączy z plikiem wykonywalnym. Możesz też mieć pamięci podręczne, które nie są powiązane z konkretną wersją, np. avatars.

Czekam

Po zainstalowaniu zaktualizowany serwis worker opóźnia aktywację do momentu, gdy istniejący serwis worker nie będzie już kontrolował klientów. Ten stan nazywa się „czekanie” i w ten sposób przeglądarka zapewnia, że jednocześnie działa tylko jedna wersja Twojego pracownika usługi.

Jeśli uruchomisz zaktualizowane demo, nadal zobaczysz zdjęcie kota, ponieważ pracownik V2 nie został jeszcze aktywowany. Nowy serwis worker znajdziesz na karcie „Aplikacja” w narzędziu DevTools:

Narzędzia programistyczne pokazujące oczekiwanie na nowego pracownika w ramach usługi

Nawet jeśli masz otwartą tylko jedną kartę z wersją demonstracyjną, odświeżenie strony nie wystarczy, aby nowa wersja została załadowana. Wynika to z działania nawigacji w przeglądarce. Gdy się przemieszczasz, bieżąca strona nie zniknie, dopóki nie zostaną odebrane nagłówki odpowiedzi. Nawet wtedy może ona pozostać, jeśli odpowiedź zawiera nagłówek Content-Disposition. Z powodu tego nakładania się skrypt service worker zawsze kontroluje klienta podczas odświeżania.

Aby uzyskać aktualizację, zamknij wszystkie karty lub zamknij usługę w tle. Gdy ponownie przejdziesz do wersji demonstracyjnej, powinieneś zobaczyć konia.

Ten schemat jest podobny do schematu aktualizacji Chrome. Aktualizacje Chrome są pobierane w tle, ale nie są stosowane, dopóki nie uruchomisz ponownie przeglądarki. W międzyczasie możesz nadal korzystać z obecnej wersji bez żadnych zakłóceń. Jest to jednak uciążliwe podczas tworzenia aplikacji, ale w narzędziach deweloperskich znajdziesz sposoby na ułatwienie tego procesu. Omówię je później w tym artykule.

Aktywuj

Ta funkcja jest wywoływana, gdy stary usługowiec przestanie działać, a nowy będzie mógł kontrolować klientów. To idealny moment na wykonanie zadań, których nie można było wykonać, gdy używano starego procesu roboczego, takich jak migracja baz danych i czyszczenie pamięci podręcznej.

W tym pokazie utrzymuję listę pamięci podręcznej, która powinna się tam znajdować, a w przypadku zdarzenia activate pozbywam się wszystkich innych, co powoduje usunięcie starej pamięci podręcznej static-v1.

Jeśli przekażesz obietnicę do funkcji event.waitUntil(), będzie ona buforować zdarzenia funkcjonalne (fetch, push, sync itp.) do czasu jej spełnienia. Gdy więc zdarzenie fetch zostanie uruchomione, aktywacja zostanie w pełni zakończona.

Pomiń fazę oczekiwania

Faza oczekiwania oznacza, że w danym momencie działa tylko jedna wersja witryny, ale jeśli nie potrzebujesz tej funkcji, możesz wcześniej aktywować nowego skryptu service worker, wywołując funkcję self.skipWaiting().

Spowoduje to, że proces roboczy usługi wyrzuci bieżący aktywny proces roboczy i aktywuje się, gdy tylko wejdzie w fazę oczekiwania (lub natychmiast, jeśli jest już w tej fazie). Nie powoduje, że pracownik pomija instalację, tylko czeka.

Nie ma znaczenia, kiedy zadzwonisz do skipWaiting(), o ile nastąpi to w trakcie oczekiwania lub przed nim. Zwykle jest to wywoływane w zdarzeniu install:

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

Możesz jednak wywołać tę funkcję jako wynik wywołania postMessage() w ramach usługi. Oznacza to, że chcesz skipWaiting() po interakcji z użytkownikiem.

Oto wersja demonstracyjna, która korzysta z elementu skipWaiting(). Powinieneś zobaczyć obraz krowy bez konieczności przewijania. Podobnie jak w przypadku clients.claim(), jest to wyścig, więc zobaczysz krowę tylko wtedy, gdy nowy skrypt service worker pobiera, instaluje i aktywuje się, zanim strona spróbuje wczytać obraz.

Aktualizacje ręczne

Jak już wspomniałem, przeglądarka automatycznie sprawdza dostępność aktualizacji po nawigacji i wydarzeniach funkcjonalnych, ale możesz też uruchamiać je ręcznie:

navigator.serviceWorker.register('/sw.js').then(reg => {
  // sometime later…
  reg.update();
});

Jeśli przewidujesz, że użytkownik będzie przez długi czas korzystać z Twojej witryny bez jej odświeżania, możesz wywoływać funkcję update() w określonych odstępach czasu (np. co godzinę).

Unikaj zmiany adresu URL skryptu usługi.

Jeśli przeczytasz mój post na temat najlepszych praktyk dotyczących pamięci podręcznej, możesz rozważyć nadanie unikalnego adresu URL każdej wersji usługi dla workera. Nie rób tego! Jest to zwykle niewskazane w przypadku serwisów workerów. Wystarczy zaktualizować skrypt w jego bieżącej lokalizacji.

Może to spowodować taki problem:

  1. index.html rejestruje sw-v1.js jako skrypt service worker.
  2. sw-v1.js przechowuje w pamięci podręcznej i przekazuje index.html, więc działa głównie offline.
  3. Uaktualniasz index.html, aby zarejestrował nowy, lśniący sw-v2.js.

Jeśli wykonasz te czynności, użytkownik nigdy nie otrzyma sw-v2.js, ponieważ sw-v1.js wyświetla starą wersję index.html z pamięci podręcznej. Musisz zaktualizować swojego pracownika usługi, aby zaktualizować pracownika usługi. Fuj.

demo powyżej zmieniłem jednak adres URL usługi. W ramach tej funkcji możesz przełączać się między wersjami. Nie robię tego w wersji produkcyjnej.

Ułatwianie tworzenia

Cykl życia skryptu service worker jest zaprojektowany z uwzględnieniem użytkownika, ale podczas tworzenia może być trochę kłopotliwy. Na szczęście masz do dyspozycji kilka narzędzi, które Ci w tym pomogą:

Zaktualizuj przy ponownym załadowaniu

To jest mój ulubiony.

Narzędzia deweloperskie wyświetlają „Zaktualizuj przy ponownym załadowaniu”

Zmiana ta ułatwia pracę programistom. Każda nawigacja:

  1. Ponowne pobieranie usługi roboczej.
  2. Zainstaluj ją jako nową wersję, nawet jeśli jest identyczna bit po bicie, co oznacza, że zdarzenie install zostanie uruchomione, a pamięci podręczne zostaną zaktualizowane.
  3. Pomiń fazę oczekiwania, aby nowy serwis worker został aktywowany.
  4. Przejdź na stronę.

Oznacza to, że aktualizacje będą się pojawiać po każdej zmianie strony (w tym po odświeżeniu) bez konieczności ponownego wczytywania strony ani zamykania karty.

Pomiń oczekiwanie

Narzędzia deweloperskie wyświetlają opcję „Pomiń oczekiwanie”

Jeśli masz oczekującą instancję roboczą, możesz kliknąć „Pomiń oczekiwanie” w Narzędziach dla programistów, aby natychmiast przenieść ją do stanu „aktywna”.

Shift-reload

Jeśli wczytasz stronę przy użyciu przycisku Shift (czyli wczytujesz ją przymusowo), całkowicie pomijasz usługę. Nie będzie ona kontrolowana. Ta funkcja jest opisana w specyfikacji, więc działa w innych przeglądarkach obsługujących workery usługi.

Obsługa aktualizacji

Usługa workera została zaprojektowana jako część rozszerzalnego internetu. Chodzi o to, że my, jako twórcy przeglądarek, zdajemy sobie sprawę, że nie jesteśmy lepsi w programowaniu stron internetowych niż web developerzy. Dlatego nie udostępniamy wąskich interfejsów API na wysokim poziomie, które rozwiązują konkretny problem za pomocą wzorów, które nam odpowiadają. Zamiast tego dajemy Ci dostęp do rdzenia przeglądarki i pozwalamy na działanie według uznania, w sposób, który najlepiej sprawdza się w przypadku Twoich użytkowników.

Aby umożliwić jak najwięcej wzorców, cały cykl aktualizacji jest widoczny:

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // the installing worker, or undefined
  reg.waiting; // the waiting worker, or undefined
  reg.active; // the active worker, or undefined

  reg.addEventListener('updatefound', () => {
    // A wild service worker has appeared in reg.installing!
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - the install event has fired, but not yet complete
    // "installed"  - install complete
    // "activating" - the activate event has fired, but not yet complete
    // "activated"  - fully active
    // "redundant"  - discarded. Either failed install, or it's been
    //                replaced by a newer version

    newWorker.addEventListener('statechange', () => {
      // newWorker.state has changed
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // This fires when the service worker controlling this page
  // changes, eg a new worker has skipped waiting and become
  // the new active worker.
});

Cykl życia trwa dalej

Jak widzisz, warto poznać cykl życia usługi. Dzięki temu działanie usługi powinno wydawać się bardziej logiczne i mniej tajemnicze. Dzięki temu zyskasz pewność, że wdrażasz i aktualizujesz usługi w sposób prawidłowy.