Cykl życia skryptu service worker

Jake Archibald
Jake Archibald

Cykl życia skryptu service worker jest jego najbardziej skomplikowaną częścią. Jeśli nie wiesz, co próbuje zrobić i jakie są korzyści, możesz mieć wrażenie, że działa przeciwko Tobie. Gdy jednak zrozumiesz, jak działa, możesz dostarczać użytkownikom płynne, dyskretne aktualizacje, łącząc najlepsze wzorce internetowe i natywne.

To szczegółowe omówienie, ale punkty na początku każdej sekcji zawierają większość potrzebnych informacji.

intencja,

Cykl życia ma na celu:

  • Umożliwia działanie w trybie offline.
  • Zezwalaj nowemu skryptowi service worker na przygotowanie się bez zakłócania działania bieżącego skryptu.
  • Upewnij się, że strona w zakresie jest kontrolowana przez ten sam skrypt service worker (lub nie jest kontrolowana przez żaden skrypt service worker).
  • Upewnij się, że działa tylko jedna wersja witryny.

Ta ostatnia kwestia jest bardzo ważna. Bez skryptów service worker użytkownicy mogą wczytać jedną kartę w Twojej witrynie, a później otworzyć kolejną. Może to spowodować, że dwie wersje witryny będą działać jednocześnie. Czasami jest to w porządku, ale jeśli chodzi o pamięć, łatwo może dojść do sytuacji, w której 2 karty będą miały bardzo różne opinie na temat tego, jak należy zarządzać ich pamięcią współdzieloną. Może to spowodować błędy, a nawet utratę danych.

Pierwszy skrypt service worker

W skrócie:

  • Zdarzenie install jest pierwszym zdarzeniem, które otrzymuje skrypt service worker, i występuje tylko raz.
  • Obiekt Promise przekazany do installEvent.waitUntil() sygnalizuje czas trwania oraz powodzenie lub niepowodzenie instalacji.
  • Skrypt service worker nie będzie otrzymywać zdarzeń takich jak fetch i push, dopóki nie zostanie zainstalowany i nie stanie się „aktywny”.
  • Domyślnie pobieranie strony nie przechodzi przez skrypt service worker, chyba że żądanie strony przeszło przez skrypt service worker. Aby zobaczyć efekty działania skryptu service worker, musisz odświeżyć stronę.
  • Użytkownik clients.claim() może zastąpić to ustawienie domyślne 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 obraz psa.

Oto skrypt service worker dla tej aplikacji (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 za każdym razem, gdy pojawia się żądanie dotyczące /dog.svg. Jeśli jednak uruchomisz powyższy przykład, przy pierwszym wczytaniu strony zobaczysz psa. Odśwież stronę, a zobaczysz kota.

Zakres i kontrola

Domyślny zakres rejestracji skryptu service worker to ./ względem adresu URL skryptu. Oznacza to, że jeśli zarejestrujesz skrypt service worker w //example.com/foo/bar.js, będzie on miał domyślny zakres //example.com/foo/.

Strony, skrypty i udostępnione skrypty nazywamy clients. Skrypt service worker może kontrolować tylko klientów, którzy są w zakresie. Gdy klient jest „kontrolowany”, jego pobrania przechodzą przez skrypt service worker w zakresie. Możesz wykryć, czy klient jest kontrolowany przez navigator.serviceWorker.controller, który będzie miał wartość null lub będzie instancją roboczą.

Pobieranie, analizowanie i wykonywanie

Pierwszy skrypt service worker jest pobierany, gdy wywołujesz .register(). Jeśli skrypt nie zostanie pobrany lub przeanalizowany albo podczas jego początkowego wykonywania wystąpi błąd, obietnica rejestracji zostanie odrzucona, a skrypt service worker zostanie odrzucony.

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

Błąd wyświetlany na karcie Narzędzia deweloperskie skryptu service worker

Zainstaluj

Pierwsze zdarzenie, które otrzymuje skrypt service worker, to install. Jest wywoływane, gdy tylko skrypt service worker zostanie wykonany, i tylko raz na skrypt service worker. Jeśli zmienisz skrypt skryptu service worker, przeglądarka uzna go za inny skrypt i otrzyma on własne zdarzenie install. Później szczegółowo omówię aktualizacje.

Zdarzenie install to okazja do zapisania w pamięci podręcznej wszystkiego, czego potrzebujesz, zanim będziesz mieć możliwość kontrolowania klientów. Obiekt Promise przekazywany do metody event.waitUntil() informuje przeglądarkę, kiedy instalacja została zakończona i czy się powiodła.

Jeśli obietnica zostanie odrzucona, oznacza to, że instalacja się nie powiodła, a przeglądarka odrzuci skrypt service worker. Nigdy nie będzie sterować klientami. Oznacza to, że możemy polegać na obecności cat.svg w pamięci podręcznej w przypadku zdarzeń 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, która wywołała funkcję .register(), będzie kontrolowana.

Gdy po raz pierwszy wczytasz wersję demonstracyjną, mimo że żądanie dog.svg jest wysyłane długo po aktywacji skryptu service worker, nie obsługuje on żądania i nadal widzisz obraz psa. Domyślnie jest to spójność – jeśli wczytanie strony następuje bez skryptu service worker, jej zasoby podrzędne również nie będą go używać. Jeśli wczytasz wersję demonstracyjną po raz drugi (czyli odświeżysz stronę), będzie ona kontrolowana. Zarówno strona, jak i obraz przejdą przez fetch zdarzenia, a zamiast tego zobaczysz kota.

clients.claim

Możesz przejąć kontrolę nad niekontrolowanymi klientami, wywołując clients.claim() w skrypcie service worker po jego aktywowaniu.

Oto odmiana powyższej wersji demonstracyjnej, która wywołuje funkcję clients.claim() w zdarzeniu activate. Za pierwszym razem powinien pojawić się kot. Mówię „powinno”, ponieważ jest to zależne od czasu. Kota zobaczysz tylko wtedy, gdy skrypt service worker zostanie aktywowany i clients.claim() zacznie działać, zanim obraz spróbuje się załadować.

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

Aktualizowanie skryptu service worker

W skrócie:

  • Aktualizacja jest wywoływana, gdy:
    • przejście na stronę objętą zakresem,
    • Zdarzenia funkcjonalne, takie jak push i sync, chyba że w ciągu ostatnich 24 godzin przeprowadzono sprawdzanie dostępności aktualizacji.
    • Wywołanie .register() tylko wtedy, gdy adres URL skryptu service worker uległ zmianie. Należy jednak unikać zmiany adresu URL skryptu service worker.
  • Większość przeglądarek, w tym Chrome 68 i nowsze, domyślnie ignoruje nagłówki buforowania podczas sprawdzania dostępności aktualizacji zarejestrowanego skryptu service worker. Nadal uwzględniają 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 uznawany za zaktualizowany, jeśli różni się od tego, który jest już w przeglądarce. (Rozszerzamy tę funkcję, aby obejmowała również importowane skrypty i moduły).
  • Zaktualizowany skrypt service worker jest uruchamiany obok istniejącego i otrzymuje własne zdarzenie install.
  • Jeśli nowy skrypt service worker ma kod stanu inny niż OK (np. 404), nie uda się go przeanalizować, zgłosi błąd podczas wykonywania lub zostanie odrzucony podczas instalacji, zostanie odrzucony, ale bieżący pozostanie aktywny.
  • Po zainstalowaniu zaktualizowany proces roboczy będzie wait, dopóki dotychczasowy proces roboczy nie będzie kontrolować żadnych klientów. (Pamiętaj, że podczas odświeżania klienci się nakładają).
  • self.skipWaiting() zapobiega oczekiwaniu, co oznacza, że skrypt service worker aktywuje się natychmiast po zakończeniu instalacji.

Załóżmy, że zmieniliśmy skrypt skryptu service worker, aby odpowiadał obrazem 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'));
  }
});

Obejrzyj prezentację Nadal powinien być widoczny obraz kota. Oto dlaczego…

Zainstaluj

Zwróć uwagę, że 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 elementów w obecnej, z której nadal korzysta stary skrypt service worker.

Ten wzorzec tworzy pamięci podręczne dla konkretnych wersji, podobnie jak zasoby, które aplikacja natywna dołącza do swojego pliku wykonywalnego. Możesz też mieć pamięci podręczne, które nie są powiązane z konkretną wersją, np. avatars.

Czekam

Po pomyślnej instalacji zaktualizowany skrypt service worker opóźnia aktywację, dopóki dotychczasowy skrypt service worker nie przestanie kontrolować klientów. Ten stan jest nazywany „oczekiwaniem” i w ten sposób przeglądarka dba o to, aby w danym momencie była uruchomiona tylko jedna wersja skryptu service worker.

Jeśli uruchomisz zaktualizowaną wersję demonstracyjną, nadal powinien wyświetlać się obraz kota, ponieważ skrypt service worker w wersji 2 nie został jeszcze aktywowany. Nowy skrypt service worker oczekujący na karcie „Aplikacja” w Narzędziach deweloperskich:

Narzędzia deweloperskie pokazujące nowy skrypt service worker w stanie oczekiwania

Nawet jeśli masz otwartą tylko jedną kartę z wersją demonstracyjną, odświeżenie strony nie wystarczy, aby nowa wersja przejęła kontrolę. Wynika to ze sposobu działania nawigacji w przeglądarce. Podczas nawigacji bieżąca strona nie znika, 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 nakładania się bieżący skrypt service worker zawsze kontroluje klienta podczas odświeżania.

Aby pobrać aktualizację, zamknij wszystkie karty korzystające z bieżącego skryptu service worker lub przejdź do innych kart. Gdy ponownie przejdziesz do wersji demonstracyjnej, powinien pojawić się koń.

Ten wzorzec jest podobny do sposobu aktualizowania Chrome. Aktualizacje Chrome są pobierane w tle, ale nie są stosowane, dopóki nie uruchomisz ponownie Chrome. Do tego czasu możesz bez przeszkód korzystać z obecnej wersji. Jest to jednak uciążliwe podczas programowania, ale Narzędzia deweloperskie mają sposoby, aby to ułatwić. Omówię je później w tym artykule.

Aktywuj

Zostanie on uruchomiony, gdy stary skrypt service worker zostanie usunięty, a nowy będzie mógł kontrolować klientów. To idealny moment na wykonanie czynności, których nie można było wykonać, gdy stary proces roboczy był nadal używany, takich jak migracja baz danych i czyszczenie pamięci podręcznej.

W powyższej wersji demonstracyjnej przechowuję listę pamięci podręcznych, które powinny tam być, 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 event.waitUntil(), będzie ona buforować zdarzenia funkcjonalne (fetch, push, sync itp.) do momentu rozwiązania obietnicy. Gdy więc zostanie wywołane zdarzenie fetch, aktywacja będzie w pełni zakończona.

Pominięcie fazy oczekiwania

Faza oczekiwania oznacza, że w danym momencie działa tylko jedna wersja witryny. Jeśli jednak nie potrzebujesz tej funkcji, możesz przyspieszyć aktywację nowego skryptu service worker, wywołując self.skipWaiting().

Spowoduje to, że skrypt service worker usunie bieżący aktywny skrypt i aktywuje się, gdy tylko wejdzie w fazę oczekiwania (lub natychmiast, jeśli już w niej jest). Nie powoduje to pominięcia instalacji przez proces roboczy, a jedynie oczekiwanie.

Nie ma znaczenia, kiedy wywołasz funkcję skipWaiting(), o ile nastąpi to w trakcie oczekiwania lub przed nim. Często wywołuje się ją w zdarzeniu install:

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

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

Możesz jednak wywołać tę metodę w wyniku zdarzenia postMessage() w skrypcie service worker. Na przykład chcesz skipWaiting() po interakcji użytkownika.

Oto wersja demonstracyjna, która korzysta z skipWaiting(). Powinien pojawić się obraz krowy bez konieczności opuszczania strony. Podobnie jak w przypadku clients.claim() jest to wyścig, więc krowę zobaczysz tylko wtedy, gdy nowy skrypt service worker zostanie pobrany, zainstalowany i aktywowany, zanim strona spróbuje wczytać obraz.

Aktualizacje ręczne

Jak wspomnieliśmy wcześniej, przeglądarka automatycznie sprawdza dostępność aktualizacji po nawigacji i zdarzeniach funkcjonalnych, ale możesz też wywołać 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 jej ponownego wczytywania, możesz wywoływać funkcję update() w określonych odstępach czasu (np. co godzinę).

Unikaj zmieniania adresu URL skryptu skryptu service worker

Jeśli znasz mój post o sprawdzonych metodach dotyczących buforowania, możesz rozważyć nadanie każdej wersji skryptu service worker unikalnego adresu URL. Nie rób tego! W przypadku skryptów service worker jest to zwykle niewskazane. Lepiej zaktualizować skrypt w jego bieżącej lokalizacji.

Może to prowadzić do takich problemów:

  1. index.html rejestruje sw-v1.js jako skrypt service worker.
  2. sw-v1.js zapisuje w pamięci podręcznej i wyświetla index.html, dzięki czemu działa przede wszystkim offline.
  3. Aktualizujesz index.html, aby zarejestrować nowy, lśniący sw-v2.js.

Jeśli wykonasz powyższe 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. Znajdujesz się w sytuacji, w której musisz zaktualizować skrypt service worker, aby zaktualizować skrypt service worker. Fuj.

Jednak w powyższej wersji demonstracyjnej zmieniliśmy adres URL skryptu service worker. Dzięki temu na potrzeby wersji demonstracyjnej możesz przełączać się między wersjami. Nie zrobiłbym tego w środowisku produkcyjnym.

Łatwe tworzenie aplikacji

Cykl życia skryptu service worker został zaprojektowany z myślą o użytkowniku, ale podczas programowania może być nieco uciążliwy. Na szczęście jest kilka narzędzi, które mogą Ci w tym pomóc:

Aktualizuj przy ponownym wczytywaniu

To jest mój ulubiony.

Narzędzia deweloperskie z opcją „Aktualizuj przy ponownym załadowaniu”

Dzięki temu cykl życia jest bardziej przyjazny dla deweloperów. Każda nawigacja:

  1. Ponownie pobierz skrypt service worker.
  2. Zainstaluj go jako nową wersję, nawet jeśli jest identyczny pod względem bajtów, co oznacza, że zdarzenie install zostanie uruchomione, a pamięć podręczna zostanie zaktualizowana.
  3. Pomiń fazę oczekiwania, aby aktywować nowy skrypt service worker.
  4. Poruszaj się po stronie.

Oznacza to, że aktualizacje będą widoczne przy każdej nawigacji (w tym odświeżaniu) bez konieczności dwukrotnego przeładowywania strony lub zamykania karty.

Pomiń oczekiwanie

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

Jeśli masz oczekujący skrypt, możesz kliknąć „Pomiń oczekiwanie” w Narzędziach deweloperskich, aby natychmiast zmienić jego stan na „aktywny”.

Shift-reload

Jeśli wymusisz ponowne załadowanie strony (shift-reload), skrypt service worker zostanie całkowicie pominięty. Będzie niekontrolowany. Ta funkcja jest w specyfikacji, więc działa w innych przeglądarkach obsługujących skrypty service worker.

Obsługa aktualizacji

Skrypt service worker został zaprojektowany jako część rozszerzalnej sieci. Chodzi o to, że jako deweloperzy przeglądarek zdajemy sobie sprawę, że nie jesteśmy lepsi w tworzeniu stron internetowych niż deweloperzy stron internetowych. Dlatego nie powinniśmy udostępniać wąskich interfejsów API wysokiego poziomu, które rozwiązują konkretny problem za pomocą wzorców, które nam się podobają. Zamiast tego powinniśmy dać Ci dostęp do wnętrza przeglądarki i pozwolić Ci robić to, co chcesz, w sposób, który najlepiej odpowiada Twoim użytkownikom.

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

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 widać, warto zrozumieć cykl życia skryptu service worker. Dzięki temu jego zachowania powinny wydawać się bardziej logiczne i mniej tajemnicze. Ta wiedza da Ci większą pewność podczas wdrażania i aktualizowania skryptów service worker.