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 to dla Ciebie niekorzystne. Gdy już dowiesz się, jak to działa, możesz zapewnić użytkownikom płynne, dyskretne aktualizacje, łącząc w sobie to, co najlepsze w wzorach internetowych i natywnych.

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

Celem cyklu życia jest:

  • Uwzględnij tryb offline.
  • Umożliw nowemu skryptowi service worker przygotowanie się do działania bez zakłócania obecnego.
  • Dopilnuj, aby stroną objętej raportowaniem MRC sterował ten sam skrypt service worker (lub żaden skrypt service worker).
  • 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 ono 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 odbywa się przez skrypt service worker, chyba że samo żądanie strony zostało przesłane przez ten skrypt. 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 skrypt service worker i po 3 sekundach dodaje zdjęcie psa.

Oto jej 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 w pamięci podręcznej obraz kota i wyświetla 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 wynosi ./ względem adresu URL skryptu. Oznacza to, że jeśli zarejestrujesz skrypt service worker w adresie //example.com/foo/bar.js, jego domyślny zakres będzie wynosił //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 są objęci zakresem. 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ędzia deweloperskie 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 inny skrypt usługi i będzie traktować go jako osobny skrypt usługi. Otrzyma on też 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 mieć możliwość kontrolowania 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, będzie to sygnał, że instalacja się nie udała, a przeglądarka odrzuca skrypt service worker. 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 zdarzeniach fetch. Jest 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 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. Wartością domyślną jest spójność. Jeśli strona wczytuje się bez skryptu service worker, jej zasoby podrzędne też nie będą. 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

Możesz przejąć kontrolę nad klientami niekontrolowanymi, wywołując clients.claim() w mechanizmie Service Worker po jego aktywacji.

Oto wariant powyższego przykładu, który wywołuje funkcję clients.claim() w zdarzeniu activate. Należy zobaczyć kota. Piszę „powinien”, ponieważ jest to kwestia czasowa. Kota zobaczysz tylko wtedy, gdy usługa robocza zostanie uruchomiona i ustawienie clients.claim() zacznie obowiązywać, zanim obraz zacznie się ładować.

Jeśli używasz pracownika usługi do wczytywania stron w inny sposób niż przez sieć, dyrektywa clients.claim() może być kłopotliwa, ponieważ pracownik usługi będzie kontrolować niektórych klientów, którzy wczytali się bez niej.

Aktualizuję skrypt service worker

W skrócie:

  • Aktualizacja jest uruchamiana w jednym z tych przypadków:
    • Nawigacja do strony objętej raportowaniem MRC.
    • 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 nowa instancja robocza ma kod stanu nieprawidłowy (na przykład 404), nie może jej przeanalizować, zwróci błąd podczas wykonywania lub odrzuca instalację podczas instalacji, nowa instancja robocza jest odrzucana, ale obecna pozostaje aktywna.
  • Po pomyślnym zainstalowaniu zaktualizowana instancja robocza będzie wait, dopóki nie będzie kontrolować żadnych klientów. (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 zmieniliśmy skrypt skryptu service worker tak, aby w odpowiedzi pojawiał się obraz 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 demonstrację 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, której nadal używa stary worker usługi.

Ten wzór tworzy pamięć podręczną dla poszczególnych wersji, która jest podobna do zasobów, 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 skrypt service worker opóźnia aktywację, dopóki istniejący skrypt service 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 skrypt service worker znajdziesz na karcie „Application” w Narzędziach deweloperskich:

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 do czasu ponownego uruchomienia Chrome. Do tego czasu 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

Uruchamia się, gdy stary skrypt service worker zniknie, a nowy skrypt service worker 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 powyższej wersji demonstracyjnej mam listę pamięci podręcznych, które powinny się tam znajdować, a w zdarzeniu activate pozbywam się wszystkich innych pamięci podręcznej, co powoduje usunięcie starej pamięci podręcznej static-v1.

Jeśli przekażesz obietnicę usłudze event.waitUntil(), będzie ona buforować zdarzenia funkcjonalne (fetch, push, sync itp.), dopóki obietnica nie zostanie rozwiązana. 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 skrypt service worker wyeliminuje aktualnie aktywną instancję roboczą i aktywuje się, gdy tylko rozpocznie się faza oczekiwania (lub natychmiast, jeśli jest już w fazie oczekiwania). 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 nazwać ją jako wynik działania postMessage() dla skryptu service worker. 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(), krowa jest wyścigiem, więc zobaczysz krowę tylko wtedy, gdy nowy mechanizm Service Worker pobierze, zainstaluje i aktywuje obraz, zanim strona spróbuje wczytać obraz.

Aktualizacje ręczne

Jak już wspomniałem, 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łuższy czas bez ponownego wczytywania strony, możesz co jakiś czas wywoływać metodę update() (np. co godzinę).

Unikaj zmieniania adresu URL skryptu service worker

Po przeczytaniu mojego posta o sprawdzonych metodach dotyczących buforowania możesz rozważyć przyznanie każdej wersji mechanizmu Service Worker unikalnego adresu URL. Nie rób tego! Zazwyczaj jest to niekorzystna metoda w przypadku mechanizmów Service Worker, wystarczy zaktualizować skrypt w jego bieżącej lokalizacji.

Może pojawić się następujący problem:

  1. index.html rejestruje sw-v1.js jako skrypt service worker.
  2. sw-v1.js przechowuje w pamięci podręcznej i udostępnia index.html, więc działa głównie offline.
  3. Aktualizujesz 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 trosce o wersję demo możesz przełączać się między wersjami. Nie robię tego w wersji produkcyjnej.

Ułatwianie programowania

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ą:

Aktualizuj po ponownym załadowaniu

To moja ulubiona gra.

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

W ten sposób zmienia się cykl życia, aby stał się bardziej przyjazny dla programistów. Każda nawigacja będzie:

  1. Ponowne pobieranie usługi.
  2. Zainstaluj ją jako nową wersję, nawet jeśli ma identyczne bajty, co oznacza, że zdarzenie install jest uruchamiane, a pamięci podręczne są aktualizowane.
  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 service workera. Nie będzie ona kontrolowana. Ta funkcja jest opisana w specyfikacji, więc działa w innych przeglądarkach obsługujących workery usług.

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. W związku z tym nie udostępniamy wąskich, ogólnych interfejsów API, które rozwiązują konkretne problemy przy użyciu wzorców nam. Zamiast tego dają dostęp do wnętrza przeglądarki i pozwalają Ci robić to tak, jak chcesz w sposób najbardziej odpowiedni dla 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 łatwiej będzie Ci wdrażać i aktualizować usługi.