Service Worker-Lebenszyklus

Jake Archibald
Jake Archibald

Der Lebenszyklus des Dienstarbeiters ist der komplizierteste Teil. Wenn Sie nicht wissen, was es bewirken soll und welche Vorteile es bietet, kann es sich so anfühlen, als würde es Ihnen im Weg stehen. Sobald Sie jedoch wissen, wie es funktioniert, können Sie Nutzern nahtlose, unaufdringliche Updates anbieten und das Beste aus Web- 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 Lebenszyklus soll Folgendes ermöglichen:

  • Offline-first-Funktion ermöglichen
  • 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 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. Das 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 gibt die Dauer und den Erfolg oder Misserfolg der Installation an.
  • Ein Dienstworker empfängt Ereignisse wie fetch und push erst, 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 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'));
  }
});

Es speichert ein Bild einer Katze im Cache und gibt es aus, wenn eine Anfrage für /dog.svg erfolgt. Wenn Sie jedoch das Beispiel oben ausführen, sehen Sie beim ersten Laden der Seite einen Hund. 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 erkennen, ob ein Client über navigator.serviceWorker.controller gesteuert wird, was null oder eine Dienstarbeiterinstanz sein kann.

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 Dienstarbeiter 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, die .register() aufgerufen hat, kontrolliert 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 werden fetch-Ereignisse durchlaufen und Sie sehen 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 Dienstarbeiter verwenden, um Seiten anders zu laden, als sie über das Netzwerk geladen würden, kann clients.claim() zu Problemen führen, da Ihr Dienstarbeiter einige Clients steuert, die ohne ihn geladen wurden.

Dienst-Worker aktualisieren

Kurz gesagt bedeutet das:

  • Eine Aktualisierung wird ausgelöst, wenn eines der folgenden Ereignisse eintritt:
    • Eine Navigation zu einer in den Geltungsbereich fallenden Seite.
    • Funktionsereignisse wie push und sync, es sei denn, es gab innerhalb der letzten 24 Stunden eine Updateprüfung.
    • .register() wird nur aufgerufen, wenn sich die Service Worker-URL geändert hat. 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 Dienstworker 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: Bei einer Aktualisierung überschneiden sich die Clients.
  • 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 Sie sollten trotzdem ein Katzenbild sehen. Das hat folgende Gründe:

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 erfolgreichen Installation wird der aktualisierte Dienstarbeiter erst aktiviert, wenn der vorhandene Dienstarbeiter keine Clients 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 weiterhin 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. Das liegt an der Funktionsweise der Browsernavigation. Wenn Sie die Seite wechseln, wird die aktuelle Seite erst ausgeblendet, 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 die Demo noch einmal aufrufen, sollte das Pferd zu sehen sein.

Dieses Muster ähnelt dem Aktualisierungsvorgang 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. Das ist während der Entwicklung jedoch sehr mühsam. Mit den DevTools können Sie sich das aber erleichtern, wie ich weiter unten in diesem Artikel erläutern werde.

Aktivieren

Dieser wird ausgelöst, sobald der alte Service Worker nicht mehr vorhanden 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 vorhanden sein sollten. Im activate-Ereignis lösche ich alle anderen, wodurch der alte static-v1-Cache entfernt wird.

Wenn Sie event.waitUntil() ein Versprechen übergeben, werden funktionale Ereignisse (fetch, push, sync usw.) bis zur Auflösung des Versprechens zwischengespeichert. 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. Häufig wird sie im Ereignis install aufgerufen:

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

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

Sie können ihn aber als Ergebnis eines postMessage() an den Dienstarbeiter aufrufen. Sie möchten also skipWaiting() nach einer Nutzerinteraktion.

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() handelt es sich um einen Wettlauf. Sie sehen die Kuh also nur, wenn der neue Service Worker abgerufen, installiert und aktiviert wird, 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 Sie davon ausgehen, dass Nutzer Ihre Website lange Zeit ohne Neuladen nutzen, sollten Sie 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. Igitt.

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. Das würde ich in der Produktion nicht tun.

Entwicklung vereinfachen

Der Lebenszyklus von Dienstmitarbeitern wurde für Nutzer entwickelt, ist aber während der Entwicklung 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 Dienst-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 Dienst-Worker aktiviert wird.
  4. Rufen Sie die Seite auf.

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

Wartezeit ü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 eng gefassten APIs bereitstellen, die ein bestimmtes Problem mithilfe von Mustern lösen, die uns gefallen. Stattdessen sollten wir Ihnen Zugriff auf das Herzstück des Browsers gewähren und Ihnen die Möglichkeit geben, es so zu tun, 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 geht weiter

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.