Service Worker-Lebenszyklus

Jake Archibald
Jake Archibald

Der Lebenszyklus des Dienstarbeiters ist der komplizierteste Teil. Wenn du nicht weißt, was damit erreicht werden soll und welche Vorteile es hat, kann es sich anfühlen, als ob es dich gegen dich ankämpft. Aber sobald du die Funktionsweise kennst, kannst du nahtlose, unaufdringliche Updates bereitstellen und das Beste aus Webmustern und nativen Mustern kombinieren.

Dieser Artikel ist sehr ausführlich, aber die Aufzählungspunkte zu Beginn jedes Abschnitts enthalten die wichtigsten Informationen.

Die Absicht

Der Zweck des Lebenszyklus ist:

  • Machen Sie Offline-First-Möglichkeiten.
  • Ermöglicht es einem neuen Dienstarbeiter, sich selbst vorzubereiten, ohne den aktuellen zu stören.
  • Achten Sie darauf, dass eine in den Geltungsbereich fallende Seite durch denselben Service Worker (oder keinen Service Worker) gesteuert wird.
  • Achten Sie darauf, dass jeweils nur eine Version Ihrer Website 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 Speicherplatz geht, kann es leicht passieren, dass zwei Tabs sehr unterschiedliche Ansichten darüber haben, wie der freigegebene Speicherplatz verwaltet werden soll. Dies kann zu Fehlern oder im schlimmsten Fall zu Datenverlusten führen.

Der erste Service Worker

Kurz gesagt bedeutet das:

  • Das install-Ereignis ist das erste Ereignis, das ein Service Worker erhält, und tritt nur einmal auf.
  • Ein an installEvent.waitUntil() übergebenes Versprechen signalisiert die Dauer und den Erfolg oder Misserfolg der Installation.
  • Ein Dienstworker empfängt erst dann Ereignisse wie fetch und push, wenn die Installation abgeschlossen ist und er den Status „aktiv“ hat.
  • Standardmäßig werden die Abrufe einer Seite nicht über einen Service Worker geleitet, es sei denn, die Seitenanfrage selbst wurde über einen Service Worker gesendet. 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 gesteuerte Seiten übernehmen.

Hier ist ein Beispiel für 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 Dienst-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'));
  }
});

Es speichert ein Bild einer Katze im Cache und gibt es aus, wenn eine Anfrage für /dog.svg erfolgt. Wenn Sie jedoch das obige Beispiel ausführen, wird beim ersten Laden der Seite ein Hund angezeigt. Aktualisieren Sie die Seite, um die Katze zu sehen.

Umfang und Kontrolle

Der Standardbereich einer Service Worker-Registrierung ist ./ relativ zur Script-URL. Wenn Sie also einen Service Worker unter //example.com/foo/bar.js registrieren, hat er standardmäßig den Bereich //example.com/foo/.

Seiten, Worker und freigegebene Worker werden als clients bezeichnet. Ihr Service Worker kann nur Clients steuern, die in den Geltungsbereich fallen. Sobald ein Client „kontrolliert“ wird, werden seine Abrufe über den entsprechenden Service Worker geleitet. Sie können feststellen, ob ein Client über navigator.serviceWorker.controller gesteuert wird, der null sein wird, oder über eine Service Worker-Instanz.

Herunterladen, parsen und ausführen

Ihr erster Service Worker wird heruntergeladen, wenn Sie .register() aufrufen. Wenn Ihr Script nicht heruntergeladen oder geparst werden kann oder bei der Erstausführung einen Fehler auftritt, wird das Registrierungsversprechen abgelehnt und der Dienst-Worker verworfen.

In den DevTools von Chrome wird der Fehler in der Konsole und im Bereich „Dienstworker“ auf dem Tab „Anwendung“ angezeigt:

Fehler auf dem Tab „DevTools“ des Dienst-Workers

Installieren

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

Mit dem Ereignis install kannst du alles im Cache speichern, was du benötigst, bevor du Clients steuern kannst. Über das Versprechen, das du an event.waitUntil() weitergibst, wird der Browser darüber informiert, wann die Installation abgeschlossen ist und ob sie erfolgreich war.

Wenn Ihr Versprechen abgelehnt wird, bedeutet das, dass die Installation fehlgeschlagen ist, und der Browser verwirft den Dienst-Worker. Sie steuert niemals Clients. Das bedeutet, dass wir davon ausgehen können, dass cat.svg in unseren fetch-Ereignissen im Cache vorhanden ist. Es ist eine Abhängigkeit.

Aktivieren

Sobald Ihr Service Worker bereit ist, Clients zu steuern und Funktionsereignisse wie push und sync zu verarbeiten, erhalten Sie ein activate-Ereignis. Das bedeutet jedoch nicht, dass die Seite mit dem Namen .register() gesteuert wird.

Wenn Sie die Demo zum ersten Mal laden, wird dog.svg zwar lange nach der Aktivierung des Dienstarbeiters angefordert, aber nicht verarbeitet. Sie sehen weiterhin das Bild des Hundes. Der Standardwert ist consistency (Konsistenz). Wenn Ihre Seite ohne Service Worker geladen wird, werden auch ihre Unterressourcen nicht geladen. Wenn Sie die Demo ein zweites Mal laden (d. h. die Seite aktualisieren), wird sie gesteuert. Sowohl die Seite als auch das Bild durchlaufen fetch-Ereignisse. Du siehst stattdessen eine Katze.

clients.claim

Sie können die Kontrolle über nicht gesteuerte Clients übernehmen, indem Sie clients.claim() in Ihrem Service Worker aufrufen, sobald er aktiviert ist.

Hier ist eine Variante der Demo oben, bei der clients.claim() im Ereignis activate aufgerufen wird. Sie sollten beim ersten Mal eine Katze sehen. Ich sage „sollten“, weil das Timing entscheidend ist. Sie sehen nur eine Katze, wenn der Dienst-Worker aktiviert wird und clients.claim() in Kraft tritt, bevor versucht wird, das Bild zu laden.

Wenn Sie Ihren Service Worker verwenden, um Seiten anders als über das Netzwerk zu laden, kann clients.claim() lästig sein, da Ihr Service Worker einige Clients steuert, die ohne ihn geladen werden.

Service Worker aktualisieren

Kurz gesagt bedeutet das:

  • Eine Aktualisierung wird in folgenden Fällen ausgelöst:
    • Eine Navigation zu einer Seite, die unter die Vorgaben fällt.
    • Funktionsspezifische Ereignisse wie push und sync, es sei denn, in den letzten 24 Stunden wurde eine Updateüberprüfung durchgeführt.
    • .register() wird nur aufgerufen, wenn die Service Worker-URL geändert wurde. Vermeiden Sie es jedoch, die Worker-URL zu ändern.
  • Die meisten Browser, einschließlich Chrome 68 und höher, ignorieren standardmäßig Cache-Header, wenn nach Updates des registrierten Service Worker-Scripts gesucht wird. Caching-Header werden weiterhin beim Abrufen von Ressourcen berücksichtigt, die über importScripts() in einen Service Worker geladen wurden. Sie können dieses Standardverhalten überschreiben, indem Sie beim Registrieren Ihres Service Workers die Option updateViaCache festlegen.
  • Ihr Service Worker gilt als aktualisiert, wenn er sich byteweise vom bereits im Browser vorhandenen unterscheidet. Diese Maßnahme wird auch auf importierte Scripts/Module ausgeweitet.
  • Der aktualisierte Service Worker wird zusammen mit dem vorhandenen gestartet und erhält ein eigenes install-Ereignis.
  • Wenn der neue Worker einen nicht ordnungsgemäßen Statuscode hat (z. B. 404), nicht geparst werden kann, während der Ausführung einen Fehler anzeigt oder bei der Installation abgelehnt wird, wird er verworfen, der aktuelle Worker bleibt jedoch aktiv.
  • Nach der erfolgreichen Installation wait der aktualisierte Worker, bis der vorhandene Worker keine Clients mehr steuert. Hinweis: Während einer Aktualisierung überschneiden sich die Kunden.
  • self.skipWaiting() verhindert das Warten, d. h. der Dienst-Worker wird aktiviert, sobald die Installation abgeschlossen ist.

Angenommen, wir haben unser Service Worker-Script so geändert, dass es mit einem Bild eines Pferdes antwortet, anstatt mit einem Bild einer Katze:

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 ansehen Du solltest immer noch ein Bild einer Katze sehen. Warum...

Installieren

Ich habe den Cachenamen von static-v1 in static-v2 geändert. So kann ich den neuen Cache einrichten, ohne Daten im aktuellen Cache zu überschreiben, den der alte Service Worker noch verwendet.

Dadurch werden versionsspezifische Caches erstellt, ähnlich wie Assets, die eine native App mit ihrer ausführbaren Datei bündelt. Möglicherweise haben Sie auch nicht versionspezifische Caches, z. B. avatars.

Warten

Nach der Installation verzögert der aktualisierte Service Worker die Aktivierung, bis der vorhandene Service Worker die Clients nicht mehr steuert. Dieser Status wird als „Warten“ bezeichnet. So sorgt der Browser dafür, dass immer nur eine Version Ihres Service Workers ausgeführt wird.

Wenn Sie die aktualisierte Demo ausgeführt haben, sollte immer noch ein Bild einer Katze angezeigt werden, da der V2-Worker noch nicht aktiviert ist. Sie sehen den neuen Service Worker auf dem Tab „Anwendung“ in den DevTools:

In den DevTools wird angezeigt, dass ein neuer Dienstarbeiter wartet

Auch wenn Sie nur einen Tab mit der Demo geöffnet haben, reicht es nicht aus, die Seite zu aktualisieren, damit die neue Version übernommen wird. Dies liegt an der Funktionsweise der Browsernavigation. Wenn Sie die Seite wechseln, wird die aktuelle Seite erst entfernt, wenn die Antwortheader empfangen wurden. Selbst dann bleibt die aktuelle Seite möglicherweise erhalten, wenn die Antwort einen Content-Disposition-Header enthält. Aufgrund dieser Überschneidung steuert der aktuelle Service Worker immer einen Client während einer Aktualisierung.

Wenn Sie das Update erhalten möchten, schließen Sie alle Tabs, die den aktuellen Dienstarbeiter verwenden, oder wechseln Sie zu einem anderen Tab. Wenn Sie dann die Demo noch einmal aufrufen, sollten Sie das Pferd sehen.

Dieses Muster ähnelt der Aktualisierung von Chrome. Chrome-Updates werden im Hintergrund heruntergeladen, aber erst nach dem Neustart von Chrome angewendet. In der Zwischenzeit können Sie die aktuelle Version weiter verwenden. Dies ist zwar ein Problem bei der Entwicklung, aber die Entwicklertools bieten Möglichkeiten, dies zu vereinfachen. Auf das wird später in diesem Artikel noch genauer eingegangen.

Aktivieren

Dieses Ereignis wird ausgelöst, sobald der alte Service Worker nicht mehr aktiv ist und der neue Service Worker Clients steuern kann. Dies ist der ideale Zeitpunkt, um Aufgaben auszuführen, die nicht möglich waren, während der alte Worker noch verwendet wurde, z. B. Datenbankmigrationen und Cache-Löschvorgänge.

In der Demo oben verwalte ich eine Liste der Caches, die ich erwarte, und im activate-Ereignis entferne ich alle anderen. Dadurch wird der alte static-v1-Cache entfernt.

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

Wartephase überspringen

Während der Wartephase wird nur eine Version Ihrer Website gleichzeitig ausgeführt. Wenn Sie diese Funktion nicht benötigen, können Sie Ihren neuen Service Worker durch Aufrufen von self.skipWaiting() früher aktivieren.

Dadurch wird der aktuelle aktive Worker vom Service Worker verdrängt und der Service Worker selbst aktiviert, sobald er in die Wartephase eintritt (oder sofort, wenn er sich bereits in der Wartephase befindet). Die Installation wird dadurch nicht übersprungen, sondern nur pausiert.

Es spielt keine Rolle, wann Sie skipWaiting() anrufen, solange Sie es während oder vor der Wartezeit tun. Es ist üblich, sie im Ereignis install aufzurufen:

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

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

Sie können dies jedoch als Ergebnis einer postMessage() für den Service Worker aufrufen. Sie möchten skipWaiting() einer Nutzerinteraktion folgen.

Hier ist eine Demo, in der skipWaiting() verwendet wird. Sie sollten ein Bild einer Kuh sehen, ohne dass Sie die Seite wechseln müssen. Wie bei clients.claim() ist es ein Rennen. Sie sehen also nur dann die Kuh, 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 Navigationen und Funktionsereignissen automatisch nach Updates. Sie können sie aber auch manuell auslösen:

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

Wenn du davon ausgehst, dass der Nutzer deine Website über einen längeren Zeitraum ohne Neuladen verwendet, kannst du update() in einem bestimmten Intervall aufrufen (z. B. stündlich).

URL des Service Worker-Scripts nicht ändern

Wenn Sie meinen Beitrag zu Best Practices für das Caching gelesen haben, sollten Sie jeder Version Ihres Service Workers eine eindeutige URL zuweisen. Tu das nicht! Dies ist normalerweise nicht empfehlenswert. Aktualisieren Sie das Script einfach an seinem aktuellen Speicherort.

Das kann zu einem Problem wie diesem führen:

  1. index.html registriert sw-v1.js als Service Worker.
  2. sw-v1.js speichert und liefert index.html im Cache, sodass es zuerst offline funktioniert.
  3. Sie aktualisieren index.html, damit Ihr neues sw-v2.js registriert wird.

In diesem Fall erhält der Nutzer nie sw-v2.js, da sw-v1.js die alte Version von index.html aus seinem Cache ausliefert. Sie müssen Ihren Service Worker aktualisieren, um Ihren Service Worker zu aktualisieren. Oh.

Für die Demo oben habe ich jedoch die URL des Dienstarbeiters geändert. So können Sie für die Demo zwischen den Versionen wechseln. In der Produktion würde ich das nicht machen.

Entwicklung vereinfachen

Der Lebenszyklus des Service Workers wurde im Hinblick auf die Nutzer entwickelt, aber in der Entwicklung ist es etwas mühsam. Zum Glück gibt es einige Tools, die dir dabei helfen können:

Beim Aktualisieren aktualisieren

Das ist mein Favorit.

In den Entwicklertools wird „Update beim Aktualisieren“ angezeigt

Dadurch wird der Lebenszyklus für Entwickler nutzerfreundlicher. Jede Navigation hat folgende Eigenschaften:

  1. Rufen Sie den Service Worker noch einmal ab.
  2. Installieren Sie es als neue Version, auch wenn es byteweise identisch ist. Das install-Ereignis wird dann ausgeführt und die Caches werden aktualisiert.
  3. Überspringen Sie die Wartephase, damit der neue Service Worker aktiviert wird.
  4. Rufen Sie die Seite auf.

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

Warten überspringen

In den DevTools wird „Warten überspringen“ angezeigt

Wenn ein Worker wartet, können Sie in den DevTools auf „Warten überspringen“ klicken, um ihn sofort in den Status „Aktiv“ zu versetzen.

Umschalttaste + Aktualisieren

Wenn Sie die Seite erzwungen neu laden (Umschalttaste + Aktualisieren), wird der Dienst-Worker vollständig umgangen. Es wird nicht kontrolliert. Diese Funktion ist in der Spezifikation enthalten und funktioniert daher in anderen Browsern, die Service Worker unterstützen.

Umgang mit Updates

Der Dienst-Worker wurde als Teil des extensible Web entwickelt. Wir als Browserentwickler sind uns bewusst, dass wir nicht besser in der Webentwicklung sind als Webentwickler. Daher sollten wir keine übergeordneten APIs anbieten, die ein bestimmtes Problem mithilfe von Mustern lösen, die uns gefallen. Stattdessen sollten wir Ihnen einen Einblick in den Browser geben, wie Sie es möchten, und zwar so, wie es für Ihre Nutzer am besten funktioniert.

Um möglichst viele Muster zu ermöglichen, wird der gesamte Updatezyklus beobachtet:

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 ist endlos

Wie Sie sehen, lohnt es sich, den Lebenszyklus von Dienstprogrammen zu verstehen. Wenn Sie das tun, sollte das Verhalten von Dienstprogrammen logischer und weniger mysteriös erscheinen. Dieses Wissen gibt Ihnen mehr Sicherheit beim Bereitstellen und Aktualisieren von Service Workern.