Cykl życia skryptu service worker

Jake Archibald
Jake Archibald

Cykl życia skryptu service worker jest jego najbardziej skomplikowanym elementem. Jeśli nie wiecie, co próbuje zrobić i jakie są korzyści, może się wydawać, że walczy z Tobą. 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 punkty na początku każdej sekcji zawierają większość niezbędnych informacji.

Zamiar

Celem cyklu życia jest:

  • Korzystaj przede wszystkim z usług offline.
  • Zezwól nowemu skryptowi service worker na przygotowanie się bez zakłócania działania bieżącego.
  • Dopilnuj, aby stroną objętej raportowaniem MRC sterował ten sam skrypt service worker (lub żaden skrypt service worker).
  • Upewnij się, że jednocześnie uruchomiona jest tylko jedna wersja Twojej witryny.

To ostatnie jest bardzo ważne. Bez mechanizmów Service Worker użytkownicy mogą wczytać jedną kartę w witrynie, a następnie otworzyć inną. Może to spowodować jednoczesne działanie 2 wersji witryny. Czasami jest to w porządku, ale jeśli zajmujesz się pamięcią masową, łatwo możesz otrzymać dwie karty, które mają bardzo różne opinie na temat zarządzania pamięcią współdzieloną. Może to spowodować błędy, a co gorsza – utratę danych.

Pierwszy skrypt service worker

W skrócie:

  • Zdarzenie install to pierwsze zdarzenie, które otrzymuje skrypt service worker. Dzieje się to tylko raz.
  • Obietnica przekazana do installEvent.waitUntil() sygnalizuje czas trwania instalacji oraz jej powodzenie lub niepowodzenie.
  • Skrypt service worker nie będzie otrzymywać zdarzeń takich jak fetch i push, dopóki nie zakończy instalacji i nie zmieni stanu na „aktywny”.
  • 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 skryptu service worker, musisz odświeżyć stronę.
  • clients.claim() może zastąpić to ustawienie domyślne i przejąć kontrolę nad stronami, które nie są kontrolowane.

Weźmy 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 pracownika usługi 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 obraz kota w pamięci podręcznej i wyświetla go, gdy tylko pojawi się żądanie /dog.svg. Jeśli jednak uruchomisz ten przykład, po pierwszym wczytaniu 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 pod adresem //example.com/foo/bar.js, jego domyślny zakres to //example.com/foo/.

Nazywamy się stronami, pracownikami i udostępnionymi pracownikami clients. Skrypt service worker może kontrolować tylko klientów, które są objęte raportowaniem MRC. Gdy klient jest „kontrolowany”, jego pobieranie jest przekazywane przez skrypt service worker w zakresie. Możesz sprawdzić, czy klient jest kontrolowany przez navigator.serviceWorker.controller, który ma wartość null, czy jest to instancja service worker.

Pobieranie, analizowanie i uruchamianie

Pierwszy skrypt service worker jest pobierany, gdy dzwonisz pod numer .register(). Jeśli skrypt nie zostanie pobrany lub przeanalizowany albo wystąpi błąd przy jego pierwszym uruchomieniu, obietnica rejestru zostanie odrzucona, a mechanizm Service Worker zostanie odrzucony.

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

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

Zainstaluj

Pierwsze zdarzenie wywoływane przez skrypt service worker to install. Jest aktywowane natychmiast po wykonaniu instancji roboczej i wywoływana tylko raz na skrypt service worker. Jeśli zmienisz skrypt skryptu service worker, przeglądarka uzna go za inny skrypt service worker i uzyska własne zdarzenie install. Zmiany omówię szczegółowo później.

Zdarzenie install to Twoja szansa, by zapisać w pamięci podręcznej wszystko, czego potrzebujesz, by mieć kontrolę nad klientami. Obietnica przekazywana do event.waitUntil() informuje przeglądarkę o tym, czy instalacja się zakończyła i czy się udała.

Jeśli obietnica zostanie odrzucona, będzie to sygnał, że instalacja się nie udała, a przeglądarka odrzuca skrypt service worker. Nigdy nie kontroluje klientów. Oznacza to, że możemy polegać na tym, że zdarzenie cat.svg jest przechowywane 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 o nazwie .register() będzie kontrolowana.

Po pierwszym wczytaniu wersji demonstracyjnej chociaż żądanie dog.svg jest wysyłane długo po aktywowaniu skryptu service worker, nie obsługuje on tego żą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ą. Ponowne wczytanie wersji demonstracyjnej (czyli odświeżenie strony) będzie możliwe. Zarówno strona, jak i obraz przejdą zdarzenia fetch, a zamiast tego zobaczysz kota.

clients.claim

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

Oto odmiana wersji demonstracyjnej powyżej, która wywołuje funkcję clients.claim() w zdarzeniu activate. Kot powinny być pierwszy raz. Mówię „powinien”, bo to zależy od czasu. Symbol kota zobaczysz tylko wtedy, gdy skrypt service worker się aktywuje, a zasada clients.claim() zostanie zastosowana przed próbą wczytania obrazu.

Jeśli skrypt service worker wczytuje strony w inny sposób niż przez sieć, interfejs clients.claim() może sprawiać problemy, ponieważ kontroluje on niektóre klienty, które ładują się bez niego.

Aktualizuję skrypt service worker

W skrócie:

  • Aktualizacja jest wywoływana, gdy:
    • Nawigacja do strony objętej raportowaniem MRC.
    • zdarzenia funkcjonalne, np. push i sync, chyba że w ciągu ostatnich 24 godzin nastąpiła kontrola aktualizacji;
    • Wywołuję .register() tylko wtedy, gdy zmienił się URL skryptu service worker. Należy jednak unikać zmiany adresu URL instancji roboczej.
  • Większość przeglądarek, w tym Chrome 68 i nowsze wersje, domyślnie ignoruje nagłówki buforowania podczas sprawdzania dostępności aktualizacji zarejestrowanego skryptu skryptu service worker. Podczas pobierania zasobów wczytywanych w skrypcie service worker przez importScripts() uwzględniają one nagłówki buforowania. To domyślne działanie możesz zastąpić, ustawiając opcję updateViaCache podczas rejestrowania skryptu service worker.
  • Skrypt service worker jest uważany za zaktualizowany, jeśli ma inny bajt niż ten, z którego już korzysta przeglądarka. (Rozszerzamy tę możliwość, aby obejmowały również zaimportowane skrypty/moduły).
  • Zaktualizowany skrypt service worker jest uruchamiany razem z bieżącym 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 odrzuci ją podczas instalacji, nowa instancja robocza zostanie odrzucona, ale bieżący pozostanie aktywny.
  • Po pomyślnym zainstalowaniu zaktualizowana instancja robocza będzie wait, dopóki nie będzie kontrolować żadnych klientów. (Pamiętaj, że klienty w trakcie odświeżania danych nakładają się na siebie).
  • 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, 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 powinno być widoczne zdjęcie 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 zawartości bieżącej, której wciąż używa stary skrypt service worker.

Ten wzorzec tworzy pamięci podręczne dla konkretnej wersji, podobne do zasobów, które aplikacja natywna będzie w pakiecie ze swoim plikiem wykonywalnym. Niektóre pamięci mogą mieć różne wersje, np. avatars.

Oczekuję

Po zainstalowaniu zaktualizowany skrypt service worker opóźnia aktywację, dopóki istniejący skrypt service worker nie będzie już kontrolować klientów. Stan ten jest nazywany „oczekiwaniem”. Dzięki niemu przeglądarka dba o to, aby w danym momencie działała tylko jedna wersja skryptu service worker.

Po uruchomieniu zaktualizowanej wersji demonstracyjnej nadal powinno być widoczne zdjęcie kota, ponieważ instancja robocza V2 nie została jeszcze aktywowana. Nowy skrypt service worker znajdziesz na karcie „Application” w Narzędziach deweloperskich:

Narzędzia deweloperskie z wyświetlonym nowym skryptem service worker oczekujący

Nawet jeśli masz otwartą tylko jedną kartę na wersję demonstracyjną, odświeżenie strony nie wystarczy, aby przejąć nad nią nową wersję. Wynika to z sposobu poruszania się w przeglądarce. Podczas poruszania się po interfejsie bieżąca strona nie znika, dopóki nie otrzymasz nagłówków odpowiedzi – nawet wtedy bieżąca strona może pozostać, jeśli odpowiedź zawiera nagłówek Content-Disposition. Z tego powodu bieżący skrypt service worker zawsze kontroluje klienta podczas odświeżania.

Aby pobrać aktualizację, zamknij wszystkie karty lub zamknij wszystkie karty za pomocą bieżącego skryptu service worker. Gdy ponownie otworzysz wersję demonstracyjną, zobaczysz konia.

Przypomina to aktualizacje Chrome. Aktualizacje Chrome są pobierane w tle, ale nie są stosowane do czasu ponownego uruchomienia Chrome. W międzyczasie możesz dalej korzystać z obecnej wersji bez zakłóceń. Jest to jednak uciążliwe w trakcie programowania, ale w Narzędziach deweloperskich znajdziesz sposoby, które to ułatwią, co omówię w dalszej części tego artykułu.

Aktywuj

Uruchamia się, gdy stary skrypt service worker zniknie, a nowy skrypt service worker będzie mógł kontrolować klientów. To idealny czas na wykonanie czynności, które nie były możliwe, gdy stara instancja robocza była nadal używana, na przykład na migrację baz danych i czyszczenie pamięci podręcznych.

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. Po uruchomieniu zdarzenia fetch aktywacja jest w pełni ukończona.

Pomiń etap oczekiwania

Etap oczekiwania oznacza, że jednocześnie korzystasz tylko z 1 wersji witryny, ale jeśli nie potrzebujesz tej funkcji, możesz aktywować nowy skrypt service worker wcześniej, dzwoniąc pod numer 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 to, że pracownik pomija instalację i tylko czeka.

Nie ma znaczenia, kiedy dzwonisz pod numer skipWaiting(), ważne jest, że robisz to w trakcie oczekiwania. W zdarzeniu install często używa się tej nazwy:

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

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

Możesz jednak nazwać ją jako wynik działania postMessage() dla skryptu service worker. Załóżmy, że chcesz skipWaiting() po interakcji użytkownika.

Oto wersja demonstracyjna, która używa języka skipWaiting(). Powinno wyświetlić się zdjęcie krowy bez konieczności opuszczania. 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 wspominałem wcześniej, przeglądarka automatycznie sprawdza dostępność aktualizacji po nawigacji i zdarzeniach działania, ale możesz też aktywować 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 wolno tak postępować. 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. Usługa sw-v1.js zapisuje dane w pamięci podręcznej i obsługuje usługę index.html, aby działała w trybie offline.
  3. Aktualizujesz urządzenie index.html, aby zarejestrowało nowe, błyszczące sw-v2.js.

Jeśli wykonasz powyższe czynności, użytkownik nie otrzyma uprawnienia sw-v2.js, ponieważ sw-v1.js udostępnia starszą wersję aplikacji index.html z pamięci podręcznej. Znajdujesz się w sytuacji, w której musisz zaktualizować swój skrypt service worker, aby go zaktualizować. Och.

Jednak w prezentacji powyżej zmieniłem adres URL skryptu service worker. W trosce o wersję demo możesz przełączać się między wersjami. Nie chciałabym tego robić.

Ułatwianie programowania

Cykl życia mechanizmów Service Worker jest projektowany z myślą o użytkownikach, ale podczas programowania jest to kłopotliwe. Na szczęście mamy kilka narzędzi, które mogą Ci w tym pomóc:

Aktualizuj po ponownym załadowaniu

To moja ulubiona gra.

Narzędzia deweloperskie z komunikatem „Aktualizuj po ponownym załadowaniu”

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

  1. Ponownie pobierz skrypt service worker.
  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ń etap oczekiwania, aby aktywować nowy skrypt service worker.
  4. Poruszanie się po stronie

Oznacza to, że te informacje będą widoczne przy każdej nawigacji (w tym przy odświeżaniu), bez konieczności ponownego ładowania strony czy zamykania karty.

Pomiń czekanie

„Pomiń czekanie” w Narzędziach deweloperskich

Jeśli czekasz na pracownika, możesz kliknąć „Pomiń czekanie” w Narzędziach deweloperskich, aby natychmiast zmienić jego stan na „aktywny”.

Shift + przeładowanie

Wymuszenie ponownego załadowania strony (shift-reload) powoduje całkowite pominięcie skryptu service worker. Nie da się go kontrolować. Ta funkcja jest częścią specyfikacji, dlatego działa w innych przeglądarkach obsługujących mechanizmy Service Worker.

Obsługa aktualizacji

Skrypt service worker został zaprojektowany jako część elastycznej sieci. Chodzi o to, że my, jako programiści przeglądarek, mamy świadomość, że nie jesteśmy lepsi w programowaniu stron internetowych niż programiści. 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 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 bez końca

Jak widać, opłaca się rozumieć cykl życia mechanizmów Service Worker. Dzięki temu zachowania tych mechanizmów powinny wydawać się bardziej logiczne i mniej tajemnicze. Wiedza ta da Ci większą pewność przy wdrażaniu i aktualizowaniu mechanizmów Service Worker.