Cykl życia skryptu service worker

Jake Archibald
Jake Archibald

Jego najbardziej skomplikowaną częścią jest cykl życia skryptu service worker. Jeśli nie wiesz, co chcesz zrobić i jakie są korzyści, możesz mieć wrażenie, jakby Cię to interesowało. Gdy już wiesz, jak to działa, możesz dostarczać użytkownikom płynne, dyskretne aktualizacje, łącząc w ten sposób najlepsze cechy aplikacji internetowej z natywnymi.

To szczegółowa analiza, ale punkty na początku każdej sekcji zawierają większość niezbędnych informacji.

Zamiar

Celem cyklu życia jest:

  • Zapewnij głównie możliwość pracy w trybie offline.
  • Zezwól nowemu skryptowi service worker na przygotowanie się bez zakłócania działania obecnego.
  • Zadbaj o to, aby strona w zakresie była kontrolowana przez ten sam skrypt service worker (lub jego brak).
  • Upewnij się, że jednocześnie działa tylko jedna wersja witryny.

To ostatnie jest dość ważne. Bez mechanizmów Service Worker użytkownicy mogą wczytać jedną kartę w witrynie, a następnie otworzyć inną. W efekcie mogą działać jednocześnie 2 wersje witryny. Czasami jest to normalne, ale jeśli w związku z miejscem na dane masz do czynienia z dwiema kartami, które zawierają różne opinie na temat zarządzania pamięcią współdzieloną. Może to spowodować błędy lub, co gorsza, utratę danych.

Pierwszy skrypt service worker

W skrócie:

  • Zdarzenie install jest pierwszym zdarzeniem, które otrzymuje skrypt service worker. Występuje tylko raz.
  • Obietnica przekazana do installEvent.waitUntil() wskazuje czas trwania oraz powodzenie lub niepowodzenie instalacji.
  • Skrypt service worker nie będzie otrzymywać zdarzeń takich jak fetch czy push, dopóki nie zakończy instalacji i nie stanie się „aktywny”.
  • Domyślnie pobieranie stron nie przechodzi przez skrypt service worker, chyba że samo żądanie strony przechodzi przez ten mechanizm. Aby zobaczyć efekty działania skryptu service worker, musisz odświeżyć stronę.
  • clients.claim() może zastąpić to ustawienie domyślne i przejąć kontrolę nad stronami niekontrolowanymi.

Wybierz taki 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 skrypt service worker i dodaje obraz psa po 3 sekundach.

Oto jego skrypt service worker, 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'));
  }
});

Zapisuje obraz kota w pamięci podręcznej i wyświetla go, gdy pojawia się prośba o /dog.svg. Jeśli jednak uruchomisz powyższy przykład, przy pierwszym wczytaniu strony zobaczysz psa. Kliknij Odśwież, a zobaczysz kota.

Zakres i kontrola

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

Do pracowników, pracowników i udostępnionych pracowników nazywamy clients. Skrypt service worker może kontrolować tylko te klienty, które są objęte raportowaniem MRC. Gdy klient zostanie „kontrolowany”, jego pobieranie przechodzi przez skrypt service worker uwzględniony w zakresie. Możesz wykryć, czy klient jest sterowany za pomocą navigator.serviceWorker.controller, który ma wartość null, lub instancję skryptu service worker.

Pobieranie, analizowanie i wykonywanie

Pierwszy skrypt service worker jest pobierany, gdy dzwonisz do: .register(). Jeśli podczas początkowego wykonania skryptu nie uda się pobrać lub przeanalizować skryptu bądź napotka on błąd, obietnica rejestru zostanie odrzucona, a mechanizm Service Worker zostanie odrzucony.

Narzędzia deweloperskie w Chrome pokazują błąd w konsoli i w sekcji skryptu service worker na karcie aplikacji:

Błąd wyświetlany na karcie Narzędzia deweloperskie w mechanizmie Service Worker

Zainstaluj

Pierwsze zdarzenie wywoływane przez skrypt service worker to install. Jest wyzwalana natychmiast po wykonaniu przez instancję roboczą i w przypadku każdego skryptu service worker jest wywoływana tylko raz. Jeśli zmienisz skrypt skryptu service worker, przeglądarka uzna go za inny skrypt i uzyska własne zdarzenie install. Dalsze informacje o aktualizacjach omówię szczegółowo później.

Zdarzenie install to szansa na zapisanie w pamięci podręcznej wszystkiego, czego potrzebujesz, zanim uzyskasz możliwość kontrolowania klientów. Obietnica, którą przekazujesz usłudze event.waitUntil(), informuje przeglądarkę o zakończeniu instalacji i o tym, czy się powiodła.

Jeśli obietnica zostanie odrzucona, sygnalizuje to, że instalacja się nie udała, a przeglądarka pomija skrypt service worker. Nigdy nie kontroluje klientów. Oznacza to, że nie możemy polegać na tym, że cat.svg występuje w pamięci podręcznej w zdarzeniach fetch. To zależność.

Aktywuj

Gdy skrypt service worker będzie gotowy do kontrolowania klientów i obsługi zdarzeń funkcjonalnych, takich jak push i sync, otrzymasz zdarzenie activate. Nie oznacza to jednak, że strona .register() będzie kontrolowana.

Przy pierwszym wczytaniu demonstracji, mimo że żądanie dog.svg jest wysyłane długo po aktywacji skryptu service worker, nie obsługuje on żądania i nadal widzisz zdjęcie psa. Wartość domyślna to spójność. Jeśli strona wczytuje się bez skryptu service worker, jej zasoby podrzędne też nie są. Jeśli wczytasz wersję demonstracyjną ponownie (czyli odświeżysz stronę), będzie można to kontrolować. Zarówno strona, jak i obraz będą przechodzić przez zdarzenia fetch. Zamiast nich zobaczysz kota.

clients.claim

Aby przejąć kontrolę nad klientami niekontrolowanymi, możesz wywołać metodę clients.claim() w skrypcie service worker po aktywacji.

Oto wersja demonstracji powyżej, która wywołuje w zdarzeniu activate polecenie clients.claim(). Kot powinna się zobaczyć za pierwszym razem. mówię „powinno”, bo to zależy od czasu. Kot zobaczysz tylko wtedy, gdy mechanizm Service Worker się aktywuje, a clients.claim() zadziała przed próbą wczytania obrazu.

Jeśli używasz skryptu service worker do wczytywania stron inaczej niż wczytywane przez sieć, clients.claim() może być uciążliwe, ponieważ będzie kontrolować niektóre klienty, które ładują się bez niego.

Aktualizowanie skryptu service worker

W skrócie:

  • Aktualizacja jest wywoływana, gdy:
    • Przejście na stronę w zakresie.
    • Działanie zdarzeń, np. push i sync, chyba że w ciągu ostatnich 24 godzin nastąpiła zmiana ich dostępności.
    • Wywołuję .register() tylko wtedy, gdy adres URL skryptu service worker uległ zmianie. Unikaj jednak zmiany adresu URL instancji roboczej.
  • Większość przeglądarek, w tym Chrome 68 i nowsze, domyślnie ignoruje nagłówki buforowania podczas sprawdzania dostępności aktualizacji zarejestrowanego skryptu skryptu service worker. Nadal respektują nagłówki buforowania podczas pobierania zasobów załadowanych w skrypcie service worker za pomocą importScripts(). Możesz zastąpić to działanie domyślne, ustawiając opcję updateViaCache podczas rejestrowania skryptu service worker.
  • Skrypt service worker jest uważany za zaktualizowany, jeśli jest inny w bajtach niż ten, który ma już przeglądarka. (Rozszerzamy tę kwestię także o zaimportowane skrypty/moduły).
  • Zaktualizowany skrypt service worker zostanie uruchomiony razem z już istniejącym i będzie mieć własne zdarzenie install.
  • Jeśli nowa instancja robocza ma nieprawidłowy kod stanu (na przykład 404), nie zostanie przeanalizowana, zgłosi błąd podczas wykonywania lub zostanie odrzucona podczas instalacji, nowa instancja robocza zostanie odrzucona, ale bieżąca pozostanie aktywna.
  • Po zainstalowaniu zaktualizowana instancja robocza będzie wait, dopóki istniejący instancja robocza nie będzie kontrolować żadnych klientów. (Pamiętaj, że klienty nakładają się na siebie podczas odświeżania).
  • self.skipWaiting() zapobiega oczekiwaniu, co oznacza, że skrypt service worker aktywuje się zaraz po zakończeniu instalacji.

Załóżmy, że zmieniliśmy skrypt skryptu service worker, tak by odpowiadał zdjęcie konia, a nie kota:

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'));
  }
});

Zobacz prezentację powyższego rozwiązania. Nadal powinno być widoczne zdjęcie kota. Oto powody...

Zainstaluj

Nazwa pamięci podręcznej została zmieniona z static-v1 na static-v2. Oznacza to, że mogę skonfigurować nową pamięć podręczną bez zastępowania zawartości obecnej, której wciąż używa stary skrypt service worker.

Ten wzorzec tworzy pamięć podręczną dla konkretnej wersji, podobne do zasobów, które aplikacja natywna mogłaby dołączyć do swojego pliku wykonywalnego. Mogą też występować pamięci podręczne, które nie są związane z konkretnymi wersjami, na przykład avatars.

Czekam

Po zainstalowaniu zaktualizowany skrypt service worker opóźnia aktywację obecnego skryptu, aż istniejący mechanizm Service Worker przestanie kontrolować klienty. Ten stan jest nazywany „oczekiwaniem” i pozwala przeglądarka zadbać o to, aby w danym momencie działała tylko jedna wersja skryptu service worker.

Jeśli uruchomiono zaktualizowaną prezentację, nadal powinno być widoczne zdjęcie kota, ponieważ instancja robocza V2 nie została jeszcze aktywowana. Nowy skrypt service worker czeka na karcie „Application” (Aplikacja) w Narzędziach deweloperskich:

Narzędzia deweloperskie z informacją o oczekiwaniu na nowy skrypt service worker

Nawet jeśli masz otwartą tylko jedną kartę w wersji demonstracyjnej, odświeżenie strony nie wystarczy, by umożliwić użycie nowej wersji. Wynika to ze sposobu działania nawigacji w przeglądarce. Podczas nawigacji bieżąca strona nie zniknie, dopóki nie zostaną odebrane nagłówki odpowiedzi. Nawet wtedy bieżąca strona może pozostać, jeśli odpowiedź zawiera nagłówek Content-Disposition. Z powodu tego pokrywania się bieżący skrypt service worker zawsze kontroluje klienta podczas odświeżania.

Aby pobrać aktualizację, zamknij lub zamknij wszystkie karty za pomocą bieżącego skryptu service worker. Gdy ponownie otworzysz wersję demonstracyjną, powinien Ci się wyświetlić koń.

Przypomina to sposób aktualizowania Chrome. Aktualizacje Chrome są pobierane w tle, ale nie są stosowane, dopóki Chrome nie uruchomi się ponownie. Do tego czasu możesz korzystać z bieżącej wersji bez zakłóceń. Jest to jednak uciążliwe w fazie rozwoju, ale za pomocą Narzędzi deweloperskich znajdują się sposoby, które je omówię w dalszej części tego artykułu.

Aktywuj

Ta opcja jest uruchamiana, gdy stary skrypt service worker zniknie, a nowy skrypt będzie mógł kontrolować klienty. To idealny moment na wykonanie czynności, które nie były możliwe, gdy stara instancja robocza była jeszcze używana, na przykład przeniesienie baz danych lub wyczyszczenie pamięci podręcznych.

W powyższej wersji demonstracyjnej zarządzam listą pamięci podręcznych, które powinny się tam znajdować, a w przypadku zdarzenia activate pozbywam się wszystkich innych, co spowoduje usunięcie starej pamięci podręcznej static-v1.

Jeśli przekażesz obietnicę funkcji event.waitUntil(), spowoduje to buforowanie zdarzeń funkcjonalnych (fetch, push, sync itp.), dopóki obietnica nie zniknie. Gdy więc zdarzenie fetch się uruchomi, oznacza to, że aktywacja się zakończyła.

Pomiń fazę oczekiwania

Faza oczekiwania oznacza, że w danym momencie korzystasz tylko z jednej wersji witryny. Jeśli nie potrzebujesz tej funkcji, możesz wcześniej aktywować nowy skrypt service worker, wywołując metodę self.skipWaiting().

Powoduje to, że skrypt service worker uruchamia bieżącą aktywną instancję roboczą i aktywuje się, gdy tylko przejdzie do fazy oczekiwania (lub natychmiast, jeśli jest już w fazie oczekiwania). Nie powoduje to, że instancje robocze pomijają instalację, tylko czekają.

Nie ma znaczenia, kiedy dzwonisz do: skipWaiting(), o ile dzwonisz w trakcie oczekiwania na rozmowę, czy przed czekaniem. Często jest to wywoływane w zdarzeniu install:

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

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

Możesz jednak wywołać to jako wynik wywołania postMessage() dla skryptu service worker. Podobnie jak w przypadku skipWaiting() chcesz obserwować interakcję użytkownika.

Ta wersja demonstracyjna używa skipWaiting(). Zobaczysz zdjęcie krowy, nawet jeśli nie musisz uciekać. Podobnie jak clients.claim() jest to wyścig, więc zobaczysz krowę tylko wtedy, gdy nowy skrypt service worker pobierze, zainstaluje i aktywuje się, zanim strona spróbuje wczytać obraz.

Aktualizacje ręczne

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

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

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

Unikaj zmieniania adresu URL skryptu skryptu service worker

Jeśli znasz mojego posta o sprawdzonych metodach buforowania, zastanów się nad nadaniem unikalnemu adresowi URL każdej wersji skryptu service worker. Nie rób tego! Zwykle jest to niewłaściwa metoda w przypadku mechanizmów Service Worker. Wystarczy zaktualizować skrypt w jego bieżącej lokalizacji.

Może to spowodować następujący problem:

  1. index.html rejestruje sw-v1.js jako skrypt service worker.
  2. sw-v1.js zapisuje w pamięci podręcznej i udostępnia zasób index.html, aby działał najpierw offline.
  3. Aktualizujesz index.html, więc rejestruje Twój nowy, błyszczący sw-v2.js.

Jeśli wykonasz powyższe czynności, użytkownik nie otrzyma nigdy polecenia sw-v2.js, ponieważ sw-v1.js udostępnia starą wersję index.html z pamięci podręcznej. Zdarzyło Ci się, że musisz zaktualizować skrypt service worker, aby go zaktualizować. Ojej.

W powyższej demonstracji zmieniłem adres URL skryptu service worker. W ramach wersji demonstracyjnej możesz przełączać się między wersjami. Nie chciałabym tego zrobić w wersji produkcyjnej.

Ułatwianie programowania

Cykl życia mechanizmów Service Worker został stworzony z myślą o użytkownikach, ale podczas programowania może to być kłopotliwe. Na szczęście jest kilka narzędzi, które mogą Wam pomóc:

Zaktualizuj przy ponownym załadowaniu

To mój ulubiony.

Narzędzia deweloperskie z informacją o aktualizacji po ponownym załadowaniu strony

Zmienia to cykl życia w taki sposób, aby był przyjazny dla programistów. Poszczególne elementy nawigacyjne:

  1. Ponownie pobierz skrypt service worker.
  2. Zainstaluj ją jako nową wersję, nawet jeśli ma ona identyczne bajty. Oznacza to, że zdarzenie install jest uruchamiane, a pamięć podręczna jest aktualizowana.
  3. Pomiń etap oczekiwania, aby aktywować nowy skrypt service worker.
  4. Poruszaj się po stronie.

Oznacza to, że będziesz otrzymywać zaktualizowane dane po każdym nawigowaniu (także przy odświeżaniu) bez konieczności ponownego ładowania strony i zamykania karty.

Pomiń oczekiwanie

Narzędzia deweloperskie z opcją „pomiń oczekiwanie”

Jeśli na Twojej liście roboczej znajduje się oczekująca instancja robocza, możesz kliknąć „pomiń oczekiwanie” w Narzędziach deweloperskich, aby natychmiast zmienić jej stan na „aktywna”.

Shift-przeładowanie

Jeśli wymuszone ponowne wczytanie strony (shift-reload) zostanie wymuszone, mechanizm Service Worker zostanie całkowicie pominięty. Wszystko będzie niekontrolowane. Ta funkcja znajduje się w specyfikacji, więc działa w innych przeglądarkach obsługujących skrypty service worker.

Obsługa aktualizacji

Skrypt service worker jest częścią internetu rozszerzalnego. Chodzi o to, że my, jako deweloperzy przeglądarek, zdajemy sobie sprawę, że nie jesteśmy lepsi w programowaniu stron internetowych niż deweloperzy stron internetowych. Dlatego nie powinniśmy udostępniać wąskich, ogólnych interfejsów API, które rozwiązują konkretny problem z wykorzystaniem wzorców, które my lubią. Zamiast tego dajemy dostęp do intuicji przeglądarki i pozwalają robić to tak, jak chcesz, w sposób najlepiej dostosowany do Twoich użytkowników.

Aby można było włączyć jak najwięcej wzorców, można obserwować cały cykl aktualizacji:

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 nieprzerwanie

Jak widać, warto poznać cykl życia mechanizmów Service Worker, a zachowania takich pracowników powinny wydawać się bardziej logiczne i mniej tajemnicze. Ta wiedza zapewni Ci większą pewność podczas wdrażania i aktualizowania mechanizmów Service Worker.