Service Worker-Lebenszyklus

Jake Archibald
Jake Archibald

Der Lebenszyklus des Service Workers ist der kompliziertste 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.

Dies ist eine tiefgründige Betrachtung, aber die Stichpunkte am Anfang jedes Abschnitts decken die meisten Dinge ab, die Sie wissen müssen.

Die Absicht

Der Zweck des Lebenszyklus ist:

  • Machen Sie Offline-First-Möglichkeiten.
  • Einem neuen Service Worker erlauben, sich vorzubereiten, ohne den aktuellen Service Worker zu unterbrechen.
  • Stellen Sie sicher, dass eine Seite, die unter die Vorgaben fällt, durchgehend von demselben Service Worker (oder keinem Service Worker) gesteuert wird.
  • 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 dies in Ordnung, aber wenn es um Speicherplatz geht, kann es leicht passieren, dass zwei Tabs unterschiedliche Meinungen dazu haben, wie der gemeinsame Speicher verwaltet werden soll. Dies kann zu Fehlern oder, noch schlimmer, zum Datenverlust führen.

Der erste Service Worker

Kurz gesagt bedeutet das:

  • Das install-Ereignis ist das erste Ereignis, das ein Service Worker erhält. Es kommt nur einmal vor.
  • Ein Promise, das an installEvent.waitUntil() weitergegeben wird, signalisiert die Dauer und den Erfolg oder Misserfolg der Installation.
  • Ein Service Worker erhält erst Ereignisse wie fetch und push, wenn die Installation erfolgreich abgeschlossen wurde und er „aktiv“ wird.
  • Standardmäßig werden Seitenabrufe nicht durch einen Service Worker geleitet, es sei denn, die Seitenanfrage selbst wurde durch einen Service Worker geleitet. Sie müssen daher 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.

Nehmen wir folgenden 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>

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

Ein Katzenbild wird im Cache gespeichert und bei einer Anfrage für /dog.svg bereitgestellt. Wenn Sie jedoch das obige Beispiel ausführen, sehen Sie beim ersten Laden der Seite einen Hund. Klicke auf „Aktualisieren“ und du siehst die Katze.

Umfang und Kontrolle

Der Standardbereich 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 Worker clients. Ihr Service Worker kann nur Clients steuern, die unter die Vorgaben fallen. Sobald ein Client "gesteuert" ist, durchlaufen seine Abrufe den Service Worker, der unter die Vorgaben fällt. 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 allererster Service Worker wird heruntergeladen, wenn Sie .register() aufrufen. Wenn Ihr Skript nicht heruntergeladen oder geparst werden kann oder bei der ersten Ausführung einen Fehler auslöst, wird das Register Promise abgelehnt und der Service Worker verworfen.

Die Entwicklertools von Chrome zeigen den Fehler in der Konsole und im Service Worker-Bereich des Anwendungs-Tabs an:

Fehler, der auf dem Tab mit den Service Worker-Entwicklertools angezeigt wird

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. Wenn Sie Ihr Service Worker-Skript ändern, betrachtet der Browser es als anderen Service Worker und erhält ein eigenes install-Ereignis. Auf Aktualisierungen werde ich später noch eingehen.

Das install-Ereignis ist Ihre Chance, alles im Cache zu speichern, bevor Sie Clients steuern können. Das Versprechen, das du an event.waitUntil() übergibst, informiert den Browser, wenn die Installation abgeschlossen ist und ob die Installation erfolgreich war.

Wenn Ihr Promise abgelehnt wird, signalisiert dies, dass die Installation fehlgeschlagen ist und der Browser den Service Worker verwirft. Es wird niemals Clients kontrollieren. Das bedeutet, dass wir uns darauf verlassen können, dass cat.svg im Cache unserer fetch-Ereignisse vorhanden ist. Es ist eine Abhängigkeit.

Aktivieren

Sobald der Service Worker bereit ist, Clients zu steuern und funktionale Ereignisse 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, verarbeitet der Service Worker die Anfrage nicht und Sie sehen immer noch das Bild des Hundes, obwohl dog.svg lange nach der Aktivierung des Service Workers angefordert wird. Die Standardeinstellung ist Consistency. Wenn Ihre Seite ohne Service Worker geladen wird, gilt das Gleiche für ihre Unterressourcen. Wenn Sie die Demo ein zweites Mal laden, also die Seite aktualisieren, erfolgt die Steuerung. Sowohl die Seite als auch das Bild durchlaufen fetch-Ereignisse. Du siehst stattdessen eine Katze.

clients.claim

Sie können die Kontrolle über unkontrollierte Clients übernehmen, indem Sie nach der Aktivierung clients.claim() innerhalb Ihres Service Workers aufrufen.

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

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. Sie sollten es jedoch vermeiden, die Worker-URL zu ändern.
  • Die meisten Browser, einschließlich Chrome 68 und höher, ignorieren standardmäßig Caching-Header, wenn nach Aktualisierungen für das registrierte Service Worker-Skript gesucht wird. Sie berücksichtigen jedoch weiterhin Caching-Header, wenn Ressourcen, die in einem Service Worker über importScripts() geladen werden, abgerufen werden. Sie können dieses Standardverhalten überschreiben, indem Sie bei der Registrierung Ihres Service Workers die Option updateViaCache festlegen.
  • Ihr Service Worker gilt als aktualisiert, wenn er sich von dem im Browser bereits vorhandenen unterscheidet. (Wir weiten dies auf importierte Skripts/Module aus.)
  • Der aktualisierte Service Worker wird zusammen mit dem vorhandenen gestartet und erhält ein eigenes install-Ereignis.
  • Wenn der neue Worker einen fehlerhaften Statuscode hat (z. B. 404), nicht geparst werden kann, während der Ausführung ein Fehler ausgegeben wird oder der neue Worker während der Installation abgelehnt wird, wird der neue Worker verworfen, der aktuelle Worker bleibt jedoch aktiv.
  • Nach der Installation führt der aktualisierte Worker den wait so lange aus, bis der vorhandene Worker keine Clients steuert. (Hinweis: Bei einer Aktualisierung überschneiden sich die Clients.)
  • self.skipWaiting() verhindert Wartezeiten, d. h., der Service Worker wird aktiviert, sobald die Installation abgeschlossen ist.

Nehmen wir an, wir haben unser Service Worker-Skript so geändert, dass als Antwort ein Bild eines Pferdes 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 der oben aufgeführten Funktionen Du solltest immer noch ein Bild einer Katze sehen. Warum...

Installieren

Beachten Sie, dass ich den Cache-Namen von static-v1 in static-v2 geändert habe. 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 eine native App mit der ausführbaren Datei bündeln würde. Möglicherweise sind auch nicht versionsspezifische Caches vorhanden, z. B. avatars.

Wartet

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 und sorgt dafür, dass im Browser jeweils nur eine Version Ihres Service Workers ausgeführt wird.

Wenn Sie die aktualisierte Demo ausgeführt haben, sollten Sie immer noch das Bild einer Katze sehen, da der V2-Worker noch nicht aktiviert wurde. Du kannst den neuen Service Worker auf dem Tab „Application“ (Anwendung) der Entwicklertools sehen:

Entwicklertools mit wartenden neuen Service Workern

Selbst wenn Sie nur einen Tab für die 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. Während der Navigation wird die aktuelle Seite erst ausgeblendet, wenn die Antwortheader empfangen wurden. Selbst dann bleibt die aktuelle Seite möglicherweise bestehen, wenn die Antwort einen Content-Disposition-Header enthält. Aufgrund dieser Überschneidung steuert der aktuelle Service Worker einen Client während einer Aktualisierung immer.

Schließen Sie alle Tabs oder verwenden Sie den aktuellen Service Worker, um das Update zu erhalten. Wenn Sie dann die Demo noch einmal aufrufen, sollten Sie das Pferd sehen.

Dieses Muster ähnelt der Aktualisierung von Chrome. Updates für Chrome werden im Hintergrund heruntergeladen, gelten jedoch erst nach dem Neustart von Chrome. In der Zwischenzeit können Sie die aktuelle Version ohne Unterbrechung weiterverwenden. 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 zu erledigen, die nicht möglich waren, während der alte Worker noch verwendet wurde, z. B. Datenbanken migrieren und Caches löschen.

In der Demo oben verwalte ich eine Liste der Caches, von denen ich erwarte, dass sie dort vorhanden sind. Im 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. Wenn also das fetch-Ereignis ausgelöst wird, ist die Aktivierung vollständig abgeschlossen.

Wartephase überspringen

In der Wartezeit wird nur eine Version deiner Website auf einmal ausgeführt. Wenn du diese Funktion jedoch nicht benötigst, kannst du die Aktivierung des neuen Service Workers beschleunigen, indem du self.skipWaiting() aufrufst.

Dadurch wird der aktuelle aktive Worker vom Service Worker entfernt und aktiviert, sobald er in die Wartephase wechselt (oder sofort, wenn er sich bereits in der Wartephase befindet). Ihr Worker muss die Installation nicht überspringen, sondern nur warten.

Es spielt keine Rolle, wann du skipWaiting() anrufst, solange dies während oder vor dem Warten passiert. Üblicherweise wird sie im Ereignis install aufgerufen:

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. Wie in, Sie möchten nach einer Nutzerinteraktion skipWaiting().

Hier ist eine Demo, in der skipWaiting() verwendet wird. Du solltest das Bild einer Kuh sehen, ohne wegzugehen. 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 diese 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 Aktualisierung verwendet, kannst du update() in einem bestimmten Intervall aufrufen (z. B. stündlich).

Ändern Sie die URL Ihres Service Worker-Skripts nicht

Wenn Sie meinen Beitrag zu den Best Practices für das Caching gelesen haben, können Sie jeder Version Ihres Service Workers eine eindeutige URL zuweisen. Tun Sie das nicht! Dies ist in der Regel nicht empfehlenswert für Service Worker. Aktualisieren Sie einfach das Skript am aktuellen Speicherort.

Dies kann zu einem Problem wie dem folgenden führen:

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

Wenn du dies oben tust, erhält der Nutzer nie sw-v2.js, weil sw-v1.js die alte Version von index.html aus seinem Cache bereitstellt. Sie müssen Ihren Service Worker aktualisieren, um ihn ebenfalls aktualisieren zu können. Oh.

In der obigen Demo habe ich jedoch die URL des Service Workers geändert. In der Demo können Sie also zwischen den Versionen wechseln. In der Produktion würde ich das nicht machen.

Entwicklung leicht gemacht

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

Beim Aktualisieren aktualisieren

Das ist mein Favorit.

In den Entwicklertools wird „Update beim Aktualisieren“ angezeigt

Dadurch ist der Lebenszyklus entwicklerfreundlich. Bei jeder Navigation:

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

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

Warten überspringen

Entwicklertools mit Anzeige von „Warten überspringen“

Wenn ein Worker wartet, kannst du in den Entwicklertools auf „Warten überspringen“ klicken, um ihn sofort auf „aktiv“ zu setzen.

Umschalt-Neu laden

Wenn Sie ein erneutes Laden der Seite erzwingen (Shift-Neuladen), wird der Service Worker vollständig umgangen. Es ist unkontrolliert. Diese Funktion ist in den Spezifikationen enthalten und funktioniert daher auch in anderen Browsern, die Service Worker unterstützen.

Updates verarbeiten

Der Service Worker wurde als Teil des Expandable-Web entwickelt. Die Idee ist, dass wir als Browserentwickler anerkennen, dass wir in der Webentwicklung nicht besser 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 so viele Muster wie möglich zu aktivieren, ist der gesamte Aktualisierungszyklus zu 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 ist endlos

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