Service Worker-Lebenszyklus

Jake Archibald
Jake Archibald

Der Lebenszyklus eines Service Workers ist der kompliziertste Teil. Wenn du nicht weißt, worum es geht und welche Vorteile es bringt, kann es sich anfühlen, als kämpfst du gegen dich. Aber sobald Sie wissen, wie es funktioniert, können Sie nahtlose, unaufdringliche Updates für Nutzer bereitstellen und dabei die besten Webmuster mit nativen Mustern kombinieren.

Dies ist eine detaillierte Analyse, aber die Stichpunkte am Anfang jedes Abschnitts decken die meisten Punkte ab, die Sie wissen müssen.

Die Absicht

Der Zweck des Lebenszyklus ist:

  • Erstklassige Offlinenutzung
  • Erlauben Sie einem neuen Service Worker, sich vorzubereiten, ohne den aktuellen Dienst zu unterbrechen.
  • Eine Seite, die unter die Vorgaben fällt, muss durchgehend vom selben Service Worker (oder keinem Service Worker) kontrolliert werden.
  • Achten Sie darauf, dass nur eine Version Ihrer Website gleichzeitig ausgeführt wird.

Das letzte ist ziemlich wichtig. Ohne Service Worker können Nutzer einen Tab auf Ihrer Website laden und später einen anderen öffnen. Dies kann dazu führen, dass zwei Versionen Ihrer Website gleichzeitig ausgeführt werden. Manchmal ist das in Ordnung, aber wenn es um Speicher geht, kann es leicht passieren, dass zwei Tabs ganz unterschiedliche Meinungen darüber haben, wie der gemeinsame Speicher verwaltet werden soll. Dies kann zu Fehlern oder, noch schlimmer, zu Datenverlust führen.

Der erste Service Worker

Kurz gesagt bedeutet das:

  • Das install-Ereignis ist das erste Ereignis, das ein Service Worker erhält, und geschieht nur einmal.
  • Ein an installEvent.waitUntil() weitergegebenes Versprechen signalisiert die Dauer und den Erfolg oder Misserfolg deiner Installation.
  • Ein Service Worker empfängt Ereignisse wie fetch und push erst, wenn die Installation erfolgreich abgeschlossen wurde und „aktiv“ wird.
  • Standardmäßig werden die Abrufe einer Seite nur dann durch einen Service Worker geleitet, wenn die Seitenanfrage selbst einen Service Worker durchlaufen hat. Sie müssen also die Seite aktualisieren, um die Auswirkungen des Service Workers zu sehen.
  • clients.claim() kann diese Standardeinstellung überschreiben und die Kontrolle über nicht kontrollierte Seiten übernehmen.

Nehmen Sie diesen HTML-Code:

<!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>

Es registriert einen Service Worker und fügt nach 3 Sekunden ein Bild eines Hundes hinzu.

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

Er speichert das Bild einer Katze im Cache und stellt es bereit, wenn eine Anfrage für /dog.svg vorliegt. Wenn Sie jedoch das obige Beispiel ausführen, wird beim ersten Laden der Seite ein Hund angezeigt. Klicke auf „Aktualisieren“ und du siehst die Katze.

Umfang und Kontrolle

Der Standardumfang einer Service Worker-Registrierung ist ./ relativ zur Skript-URL. Wenn Sie also einen Service Worker unter //example.com/foo/bar.js registrieren, hat er den Standardbereich //example.com/foo/.

Wir nennen Seiten, Worker und freigegebene Mitarbeiter clients. Ihr Service Worker kann nur Clients steuern, die unter die Richtlinie fallen. Sobald ein Client „gesteuert“ ist, durchlaufen seine Abrufe den Service Worker, der die Vorgaben erfüllt. Sie können feststellen, ob ein Client über navigator.serviceWorker.controller gesteuert wird, der null ist, oder eine Service Worker-Instanz.

Herunterladen, parsen und ausführen

Wenn Sie .register() aufrufen, wird Ihr allererster Service Worker heruntergeladen. Wenn Ihr Script nicht heruntergeladen oder geparst werden kann oder bei der ersten Ausführung einen Fehler ausgibt, wird das Register Promise abgelehnt und der Service Worker verworfen.

In den Entwicklertools von Chrome wird der Fehler in der Konsole und im Service Worker-Abschnitt des Anwendungs-Tabs angezeigt:

Auf dem Tab für die Service Worker-Entwicklertools wird ein Fehler angezeigt

Installieren

Das erste Ereignis, das ein Service Worker erhält, ist install. Sie wird ausgelöst, sobald der Worker ausgeführt wird, und wird nur einmal pro Service Worker aufgerufen. Wenn Sie Ihr Service Worker-Skript ändern, betrachtet der Browser es als anderen Service Worker und erhält ein eigenes install-Ereignis. Auf diese Neuerungen gehen wir später noch genauer ein.

Mit dem Ereignis install haben Sie die Möglichkeit, alles, was Sie benötigen, im Cache zu speichern, bevor Sie Clients steuern können. Das Versprechen, das du an event.waitUntil() übergibst, informiert den Browser darüber, wann die Installation abgeschlossen ist und ob die Installation erfolgreich war.

Wenn Ihr Versprechen ablehnt, signalisiert dies, dass die Installation fehlgeschlagen ist, und der Browser verwirft den Service Worker. Es hat niemals Kontrolle über Clients. Wir können uns also nicht darauf verlassen, dass cat.svg bei unseren fetch-Ereignissen im Cache vorhanden ist. Es ist eine Abhängigkeit.

Aktivieren

Sobald Ihr Service Worker Clients steuern und funktionale Ereignisse wie push und sync verarbeiten kann, erhalten Sie ein activate-Ereignis. Das bedeutet aber nicht, dass die Seite mit dem Namen .register() gesteuert wird.

Wenn Sie die Demo zum ersten Mal laden, wird die dog.svg zwar lange nach der Aktivierung durch den Service Worker angefordert, die Anfrage wird aber nicht verarbeitet und Sie sehen das Bild des Hundes. Die Standardeinstellung ist Consistency. Wenn Ihre Seite ohne Service Worker geladen wird, gilt das auch für die Unterressourcen. Wenn Sie die Demo ein zweites Mal laden, also die Seite aktualisieren, funktioniert das Ganze wieder. Sowohl die Seite als auch das Bild durchlaufen fetch-Ereignisse und Sie sehen stattdessen eine Katze.

clients.claim

Sie können nicht kontrollierte Clients übernehmen, indem Sie nach der Aktivierung in Ihrem Service Worker clients.claim() aufrufen.

Hier sehen Sie eine Variante der obigen Demo, bei der clients.claim() in ihrem activate-Ereignis aufgerufen wird. Du solltest eine Katze beim ersten Mal sehen. Ich sage „sollte“, weil das Timing entscheidend ist. Eine Katze wird nur angezeigt, wenn der Service Worker aktiviert wird und clients.claim() wirksam wird, bevor das Bild geladen wird.

Wenn Sie mit Ihrem Service Worker Seiten anders laden als über das Netzwerk, kann clients.claim() lästig sein, da Ihr Service Worker einige Clients steuert, die ohne den Dienst geladen werden.

Service Worker aktualisieren

Kurz gesagt bedeutet das:

  • In folgenden Fällen wird eine Aktualisierung ausgelöst:
    • Eine Navigation zu einer Seite, die unter die Vorgaben fällt.
    • Funktionelle Ereignisse wie push und sync, es sei denn, in den letzten 24 Stunden wurde auf Updates geprüft.
    • .register() wird nur dann aufgerufen, wenn sich die Service Worker-URL geändert hat. Sie sollten es jedoch vermeiden, die Worker-URL zu ändern.
  • In den meisten Browsern, einschließlich Chrome 68 und höher, werden Caching-Header bei der Suche nach Updates des registrierten Service Worker-Skripts standardmäßig ignoriert. Sie berücksichtigen weiterhin Caching-Header, wenn Ressourcen abgerufen werden, die in einem Service Worker über importScripts() geladen wurden. Sie können dieses Standardverhalten überschreiben, indem Sie bei der Registrierung Ihres Service Workers die Option updateViaCache festlegen.
  • Ein Service Worker gilt als aktualisiert, wenn er in Byte von dem bereits im Browser vorhandenen Byte abweicht. (Wir weiten dies auf importierte Skripts/Module aus.)
  • Der aktualisierte Service Worker wird parallel zum vorhandenen gestartet und erhält ein eigenes install-Ereignis.
  • Wenn der Statuscode des neuen Workers nicht fehlerfrei ist (z. B. 404), kann er nicht geparst werden, gibt während der Ausführung einen Fehler aus oder lehnt er während der Installation ab, wird der neue Worker verworfen, der aktuelle Worker bleibt jedoch aktiv.
  • Nach der Installation führt der aktualisierte Worker den Status wait aus, bis der vorhandene Worker keine Clients mehr steuert. (Beachten Sie, dass sich Clients während einer Aktualisierung überschneiden.)
  • self.skipWaiting() verhindert das Warten. Das bedeutet, dass der Service Worker aktiviert wird, sobald die Installation abgeschlossen ist.

Nehmen wir an, wir haben unser Service Worker-Skript so geändert, dass als Antwort ein Bild von einem Pferd statt einer Katze angezeigt wird:

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

Demo zu den oben genannten Funktionen ansehen Du solltest immer noch das Bild einer Katze sehen. Warum...

Installieren

Der Cache-Name wurde von static-v1 in static-v2 geändert. Das bedeutet, dass ich den neuen Cache einrichten kann, ohne Elemente im aktuellen Cache zu überschreiben, den der alte Service Worker noch verwendet.

Mit diesem Muster werden versionsspezifische Caches erstellt, ähnlich wie Assets, die in einer nativen App mit einer ausführbaren Datei zusammengefasst werden. Möglicherweise haben Sie auch Caches, die nicht versionsspezifisch sind, z. B. avatars.

Warten

Nach der erfolgreichen Installation verzögert sich die Aktivierung des aktualisierten Service Workers, bis der vorhandene Service Worker keine Clients mehr steuert. Dieser Status wird als „Warten“ bezeichnet und sorgt dafür, dass immer nur eine Version Ihres Service Workers ausgeführt wird.

Wenn Sie die aktualisierte Demo ausgeführt haben, sollten Sie immer noch ein Bild einer Katze sehen, da der V2-Worker noch nicht aktiviert wurde. Sie können den neuen Service Worker in den Entwicklertools auf dem Tab „Application“ (Anwendungen) sehen:

Entwicklertools, die zeigen, dass ein neuer Service Worker wartet

Auch wenn Sie nur einen Tab für die Demo geöffnet haben, reicht die Aktualisierung der Seite nicht aus, um die neue Version übernehmen zu können. Dies liegt an der Funktionsweise der Browsernavigation. Beim Navigieren wird die aktuelle Seite erst geschlossen, wenn die Antwortheader empfangen wurden. Selbst wenn die aktuelle Seite einen Content-Disposition-Header enthält, bleibt die aktuelle Seite möglicherweise bestehen. Aufgrund dieser Überschneidung steuert der aktuelle Service Worker während einer Aktualisierung immer einen Client.

Schließen Sie alle Tabs, auf denen der aktuelle Service Worker verwendet wird, oder verlassen Sie sie, um das Update zu erhalten. Wenn Sie dann die Demo noch einmal aufrufen, sollte das Pferd zu sehen sein.

Dieses Muster ähnelt der Aktualisierung von Chrome. Updates für Chrome werden im Hintergrund heruntergeladen. Sie werden jedoch erst nach dem Neustart von Chrome angewendet. In der Zwischenzeit können Sie die aktuelle Version störungsfrei nutzen. Dies ist zwar in der Entwicklungsphase mühsam, aber die Entwicklertools bieten Möglichkeiten, dies zu vereinfachen. Darauf gehen wir weiter unten in diesem Artikel ein.

Aktivieren

Dieser wird ausgelöst, wenn der alte Service Worker nicht mehr verwendet wird und der neue Service Worker Clients steuern kann. Dies ist der ideale Zeitpunkt für Aktionen, die nicht ausgeführt werden konnten, während der alte Worker noch verwendet wurde, z. B. das Migrieren von Datenbanken und das Leeren von Caches.

In der Demo oben habe ich eine Liste der Caches, die dort voraussichtlich vorhanden sind. Beim activate-Ereignis lösche ich alle anderen, wodurch der alte static-v1-Cache entfernt wird.

Wenn du ein Promise an event.waitUntil() übergibst, werden funktionale Ereignisse (fetch, push, sync usw.) zwischengespeichert, bis das Promise aufgelöst wird. Die Aktivierung ist also vollständig abgeschlossen, wenn das fetch-Ereignis ausgelöst wird.

Wartephase überspringen

In der Wartezeit-Phase führen Sie nur eine Version Ihrer Website auf einmal aus. Sollten Sie diese Funktion jedoch nicht benötigen, können Sie Ihren neuen Service Worker früher aktivieren, indem Sie self.skipWaiting() aufrufen.

Dies führt dazu, dass Ihr Service Worker den aktiven Worker außer Kraft setzt und sich selbst aktiviert, sobald er in die Wartephase übertritt (oder sofort, wenn er sich bereits in der Wartephase befindet). Der Worker überspringt dadurch nicht die Installation, sondern wartet nur.

Es spielt keine Rolle, wann du skipWaiting() anrufst, solange dies während oder vor der Wartezeit erfolgt. Es ist üblich, es im install-Ereignis aufzurufen:

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

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

Sie können es aber als Ergebnis von postMessage() für den Service Worker aufrufen. Sie möchten z. B. nach einer Nutzerinteraktion skipWaiting().

In dieser Demo wird skipWaiting() verwendet. Sie sollten das Bild einer Kuh sehen, ohne die Website verlassen zu müssen. Wie bei clients.claim() ist auch hier ein Rennen. Sie sehen die Kuh also nur dann, wenn der neue Service Worker das Bild abruft, installiert und aktiviert, bevor die Seite versucht, das Bild zu laden.

Manuelle Updates

Wie bereits erwähnt, sucht der Browser nach Updates automatisch nach Updates und Funktionsereignissen, Sie können diese aber auch manuell auslösen:

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

Wenn Sie davon ausgehen, dass der Nutzer Ihre Website längere Zeit verwendet, ohne sie neu zu laden, sollten Sie update() in einem Intervall (z. B. stündlich) aufrufen.

Vermeiden Sie es, die URL Ihres Service Worker-Skripts zu ändern

Wenn Sie meinen Beitrag zu den Best Practices für das Caching gelesen haben, empfiehlt es sich, jeder Version Ihres Service Workers eine eigene URL zuzuweisen. Tun Sie das nicht! Dies ist für Service Worker normalerweise nicht empfehlenswert. Aktualisieren Sie einfach das Skript am aktuellen Speicherort.

Das kann zu folgendem Problem führen:

  1. index.html registriert sw-v1.js als Service Worker.
  2. sw-v1.js speichert index.html im Cache und stellt diese bereit, damit es offline funktioniert.
  3. Du aktualisierst index.html, damit dein neuer und glänzender sw-v2.js registriert wird.

Wenn Sie dies tun, erhält der Nutzer sw-v2.js nie, weil sw-v1.js die alte Version von index.html aus dem Cache bereitstellt. Sie befinden sich in einer Situation, in der Sie Ihren Service Worker aktualisieren müssen. Ih.

In der obigen Demo habe ich jedoch die URL des Service Workers geändert. Das heißt, für die Demo können Sie zwischen den Versionen wechseln. In der Produktion würde ich das nicht tun.

Einfache Entwicklung

Der Service Worker-Lebenszyklus ist auf den Nutzer ausgerichtet, aber während der Entwicklung ist dies etwas mühsam. Zum Glück gibt es ein paar hilfreiche Tools:

Beim Aktualisieren aktualisieren

Das ist mein Favorit.

Entwicklertools mit „Update beim Aktualisieren“

Dadurch wird der Lebenszyklus entwicklerfreundlich gestaltet. Für jede Navigation gilt Folgendes:

  1. Rufen Sie den Service Worker noch einmal ab.
  2. Installieren Sie sie als neue Version, auch wenn sie byteidentisch ist, was bedeutet, dass Ihr install-Ereignis ausgeführt wird und Ihre Caches aktualisiert werden.
  3. Überspringen Sie die Wartezeit, damit der neue Service Worker aktiviert wird.
  4. Auf der Seite navigieren

Das bedeutet, dass Sie Ihre Updates bei jeder Navigation (auch ohne Aktualisierung) erhalten, ohne zweimal neu laden oder den Tab schließen zu müssen.

Warten überspringen

Entwicklertools mit Anzeige „Warten überspringen“

Wenn ein Worker wartet, können Sie in den Entwicklertools auf „Warten überspringen“ klicken, um ihn sofort auf „Aktiv“ hochzustufen.

Umschalttaste-Neuladen

Wenn Sie das erneute Laden der Seite erzwingen („Umschalttaste/Neuladen“), wird der Service Worker vollständig umgangen. Es erfolgt keine Kontrolle. Diese Funktion ist in der Spezifikation enthalten und funktioniert daher auch in anderen Browsern, die Service Worker unterstützen.

Umgang mit Updates

Der Service Worker wurde als Teil des extensiblen Web entwickelt. Der Grundgedanke ist, dass wir als Browser-Entwickler anerkennen, dass wir in der Webentwicklung nicht besser sind als Webentwickler. Daher sollten wir keine eng gefassten, übergeordneten APIs zur Lösung eines bestimmten Problems mithilfe von Mustern bereitstellen, die wir mögen. Stattdessen sollten Sie Zugriff auf die Eingeweide des Browsers erhalten und es Ihnen so machen, wie Sie möchten, so, wie es für Ihre Nutzer am besten funktioniert.

Um so viele Muster wie möglich zu ermöglichen, lässt sich der gesamte Aktualisierungszyklus beobachten:

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

Der Lebenszyklus geht immer weiter

Wie Sie sehen, zahlt es sich aus, den Lebenszyklus des Service Workers zu verstehen – und mit diesem Verständnis sollte das Verhalten der Service Worker logischer und weniger mysteriös erscheinen. Dieses Wissen gibt Ihnen mehr Selbstvertrauen, wenn Sie Service Worker bereitstellen und aktualisieren.