Bidirektionale Kommunikation mit Service Workern

Andrew Guan
Andrew Guan
Demián Renzulli
Demián Renzulli

In einigen Fällen muss eine Webanwendung möglicherweise einen zweiseitigen Kommunikationskanal zwischen der Seite und dem Dienst-Worker herstellen.

Beispiel: In einer Podcast-PWA könnte eine Funktion implementiert werden, mit der Nutzer Folgen zum Offline-Abspielen herunterladen und dem Dienst-Worker ermöglichen, die Seite regelmäßig über den Fortschritt zu informieren, damit die Hauptschleife die Benutzeroberfläche aktualisieren kann.

In diesem Leitfaden werden die verschiedenen Möglichkeiten zur Implementierung einer zweiseitigen Kommunikation zwischen dem Window- und dem Service Worker-Kontext erläutert. Dazu werden verschiedene APIs, die Workbox-Bibliothek sowie einige erweiterte Anwendungsfälle vorgestellt.

Diagramm, das einen Service Worker und die Seite zeigt, die Nachrichten austauschen

Workbox verwenden

workbox-window ist eine Reihe von Modulen der Workbox-Bibliothek, die im Fensterkontext ausgeführt werden sollen. Die Klasse Workbox bietet die Methode messageSW(), mit der eine Nachricht an den registrierten Dienst-Worker der Instanz gesendet und auf eine Antwort gewartet werden kann.

Im folgenden Seitencode wird eine neue Workbox-Instanz erstellt und eine Nachricht an den Service Worker gesendet, um die Version abzurufen:

const wb = new Workbox('/sw.js');
wb.register();

const swVersion = await wb.messageSW({type: 'GET_VERSION'});
console.log('Service Worker version:', swVersion);

Der Dienst-Worker implementiert einen Nachrichtenempfänger am anderen Ende und antwortet auf den registrierten Dienst-Worker:

const SW_VERSION = '1.0.0';

self.addEventListener('message', (event) => {
  if (event.data.type === 'GET_VERSION') {
    event.ports[0].postMessage(SW_VERSION);
  }
});

Im Hintergrund verwendet die Bibliothek eine Browser-API, die wir im nächsten Abschnitt Message Channel näher betrachten. Viele Implementierungsdetails werden jedoch abstrahiert, was die Verwendung erleichtert und gleichzeitig die breite Browserunterstützung dieser API nutzt.

Diagramm, das die bidirektionale Kommunikation zwischen Seite und Service Worker mithilfe des Workbox-Fensters zeigt

Browser-APIs verwenden

Wenn die Workbox-Bibliothek nicht Ihren Anforderungen entspricht, gibt es mehrere APIs auf niedrigerem Niveau, mit denen Sie eine „zweiseitige“ Kommunikation zwischen Seiten und Service Workern implementieren können. Sie haben einige Gemeinsamkeiten und Unterschiede:

Gemeinsamkeiten:

  • In allen Fällen beginnt die Kommunikation an einem Ende über die postMessage()-Schnittstelle und wird am anderen Ende durch Implementieren eines message-Handlers empfangen.
  • In der Praxis können wir mit allen verfügbaren APIs dieselben Anwendungsfälle implementieren. Einige von ihnen können die Entwicklung in einigen Szenarien jedoch vereinfachen.

Unterschiede:

  • Sie haben unterschiedliche Möglichkeiten, die andere Seite der Kommunikation zu identifizieren: Einige verwenden einen expliziten Verweis auf den anderen Kontext, während andere implizit über ein Proxyobjekt kommunizieren können, das auf jeder Seite instanziiert wird.
  • Die Browserunterstützung variiert.
Diagramm, das die Zwei-Wege-Kommunikation zwischen Seite und Service Worker sowie die verfügbaren Browser-APIs zeigt

Broadcast Channel API

Unterstützte Browser

  • Chrome: 54.
  • Edge: 79.
  • Firefox: 38.
  • Safari: 15.4.

Quelle

Die Broadcast Channel API ermöglicht die grundlegende Kommunikation zwischen Browserkontexten über BroadcastChannel-Objekte.

Dazu muss in jedem Kontext zuerst ein BroadcastChannel-Objekt mit derselben ID instanziiert und Nachrichten von ihm gesendet und empfangen werden:

const broadcast = new BroadcastChannel('channel-123');

Das BroadcastChannel-Objekt stellt eine postMessage()-Schnittstelle bereit, um eine Nachricht an jeden wartenden Kontext zu senden:

//send message
broadcast.postMessage({ type: 'MSG_ID', });

Jeder Browserkontext kann über die Methode onmessage des BroadcastChannel-Objekts auf Nachrichten lauschen:

//listen to messages
broadcast.onmessage = (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //process message...
  }
};

Wie Sie sehen, gibt es keinen expliziten Verweis auf einen bestimmten Kontext. Daher ist es nicht erforderlich, zuerst eine Referenz auf den Service Worker oder einen bestimmten Client abzurufen.

Diagramm, das die bidirektionale Kommunikation zwischen Seite und Service Worker mithilfe eines Broadcast Channel-Objekts zeigt

Der Nachteil ist, dass die API derzeit von Chrome, Firefox und Edge unterstützt wird, aber von anderen Browsern wie Safari noch nicht.

Client API

Unterstützte Browser

  • Chrome: 40.
  • Edge: 17.
  • Firefox: 44.
  • Safari: 11.1.

Quelle

Über die Client API können Sie eine Referenz auf alle WindowClient-Objekte abrufen, die die aktiven Tabs darstellen, die vom Dienstarbeiter gesteuert werden.

Da die Seite von einem einzelnen Service Worker gesteuert wird, überwacht und sendet sie Nachrichten direkt über die serviceWorker-Schnittstelle an den aktiven Service Worker:

//send message
navigator.serviceWorker.controller.postMessage({
  type: 'MSG_ID',
});

//listen to messages
navigator.serviceWorker.onmessage = (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //process response
  }
};

Ähnlich überwacht der Dienst-Worker Nachrichten, indem er einen onmessage-Listener implementiert:

//listen to messages
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //Process message
  }
});

Um mit seinen Clients zu kommunizieren, ruft der Dienst-Worker Methoden wie Clients.matchAll() und Clients.get() auf, um ein Array von WindowClient-Objekten zu erhalten. Dann kann es postMessage()eine der folgenden Aktionen ausführen:

//Obtain an array of Window client objects
self.clients.matchAll(options).then(function (clients) {
  if (clients && clients.length) {
    //Respond to last focused tab
    clients[0].postMessage({type: 'MSG_ID'});
  }
});
Diagramm, das einen Dienstarbeiter zeigt, der mit einer Reihe von Clients kommuniziert

Client API ist eine gute Option, um relativ einfach mit allen aktiven Tabs von einem Service Worker zu kommunizieren. Die API wird von allen gängigen Browsern unterstützt. Möglicherweise sind jedoch nicht alle Methoden verfügbar. Prüfen Sie daher vor der Implementierung auf Ihrer Website, ob der Browser die API unterstützt.

Kanal für Nachrichten

Unterstützte Browser

  • Chrome: 2.
  • Edge: 12.
  • Firefox: 41.
  • Safari: 5.

Quelle

Für Message Channel muss ein Port definiert und von einem Kontext an einen anderen übergeben werden, um einen zweiseitigen Kommunikationskanal herzustellen.

Um den Kanal zu initialisieren, instanziiert die Seite ein MessageChannel-Objekt und sendet damit einen Port an den registrierten Dienst-Worker. Auf der Seite wird auch ein onmessage-Listener implementiert, um Nachrichten aus dem anderen Kontext zu empfangen:

const messageChannel = new MessageChannel();

//Init port
navigator.serviceWorker.controller.postMessage({type: 'PORT_INITIALIZATION'}, [
  messageChannel.port2,
]);

//Listen to messages
messageChannel.port1.onmessage = (event) => {
  // Process message
};
Diagramm, das eine Seite zeigt, die einen Port an einen Service Worker weitergibt, um eine bidirektionale Kommunikation herzustellen.

Der Service Worker empfängt den Port, speichert eine Referenz darauf und verwendet sie, um eine Nachricht an die andere Seite zu senden:

let communicationPort;

//Save reference to port
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'PORT_INITIALIZATION') {
    communicationPort = event.ports[0];
  }
});

//Send messages
communicationPort.postMessage({type: 'MSG_ID'});

MessageChannel wird derzeit von allen gängigen Browsern unterstützt.

Erweiterte APIs: Hintergrundsynchronisierung und Hintergrundabruf

In diesem Leitfaden haben wir Möglichkeiten zur Implementierung von Zwei-Wege-Kommunikationstechniken für relativ einfache Fälle untersucht, z. B. das Übergeben einer Stringnachricht, die den auszuführenden Vorgang beschreibt, oder einer Liste von URLs, die zwischen zwei Kontexten im Cache gespeichert werden sollen. In diesem Abschnitt werden zwei APIs vorgestellt, die für bestimmte Szenarien geeignet sind: fehlende Verbindung und lange Downloads.

Hintergrundsynchronisierung

Unterstützte Browser

  • Chrome: 49.
  • Edge: 79.
  • Firefox: Nicht unterstützt.
  • Safari: Nicht unterstützt.

Quelle

Eine Chat-App sollte dafür sorgen, dass Nachrichten aufgrund einer schlechten Verbindung nicht verloren gehen. Mit der Background Sync API können Sie Aktionen verschieben, damit sie wiederholt werden, wenn der Nutzer eine stabile Verbindung hat. So wird sichergestellt, dass alles, was der Nutzer senden möchte, auch tatsächlich gesendet wird.

Anstelle der postMessage()-Schnittstelle registriert die Seite eine sync:

navigator.serviceWorker.ready.then(function (swRegistration) {
  return swRegistration.sync.register('myFirstSync');
});

Der Service Worker überwacht dann das Ereignis sync, um die Nachricht zu verarbeiten:

self.addEventListener('sync', function (event) {
  if (event.tag == 'myFirstSync') {
    event.waitUntil(doSomeStuff());
  }
});

Die Funktion doSomeStuff() sollte ein Versprechen zurückgeben, das den Erfolg oder Misserfolg des jeweiligen Vorgangs angibt. Wenn das Versprechen erfüllt ist, ist die Synchronisierung abgeschlossen. Falls die Synchronisierung fehlschlägt, wird ein neuer Synchronisierungsversuch geplant. Auch bei wiederholten Synchronisierungen wird auf eine Verbindung gewartet und ein exponentielles Backoff verwendet.

Sobald der Vorgang ausgeführt wurde, kann der Dienst-Worker mithilfe einer der zuvor beschriebenen Kommunikations-APIs mit der Seite kommunizieren, um die Benutzeroberfläche zu aktualisieren.

Die Google Suche verwendet die Hintergrundsynchronisierung, um fehlgeschlagene Abfragen aufgrund einer schlechten Verbindung zu speichern und später noch einmal zu versuchen, wenn der Nutzer online ist. Nach Abschluss des Vorgangs wird der Nutzer über eine Web-Push-Benachrichtigung über das Ergebnis informiert:

Diagramm, das eine Seite zeigt, die einen Port an einen Service Worker weitergibt, um eine bidirektionale Kommunikation herzustellen.

Hintergrundabruf

Unterstützte Browser

  • Chrome: 74.
  • Edge: 79.
  • Firefox: Nicht unterstützt.
  • Safari: Nicht unterstützt.

Quelle

Für relativ kurze Aufgaben wie das Senden einer Nachricht oder einer Liste von URLs, die im Cache gespeichert werden sollen, sind die bisher beschriebenen Optionen eine gute Wahl. Wenn die Aufgabe zu lange dauert, beendet der Browser den Dienstarbeiter. Andernfalls besteht ein Risiko für die Privatsphäre und den Akku des Nutzers.

Mit der Background Fetch API können Sie eine langwierige Aufgabe an einen Service Worker auslagern, z. B. den Download von Filmen, Podcasts oder Levels eines Spiels.

Verwenden Sie backgroundFetch.fetch anstelle von postMessage(), um über die Seite mit dem Dienst-Worker zu kommunizieren:

navigator.serviceWorker.ready.then(async (swReg) => {
  const bgFetch = await swReg.backgroundFetch.fetch(
    'my-fetch',
    ['/ep-5.mp3', 'ep-5-artwork.jpg'],
    {
      title: 'Episode 5: Interesting things.',
      icons: [
        {
          sizes: '300x300',
          src: '/ep-5-icon.png',
          type: 'image/png',
        },
      ],
      downloadTotal: 60 * 1024 * 1024,
    },
  );
});

Mit dem BackgroundFetchRegistration-Objekt kann die Seite auf das Ereignis progress warten, um den Fortschritt des Downloads zu verfolgen:

bgFetch.addEventListener('progress', () => {
  // If we didn't provide a total, we can't provide a %.
  if (!bgFetch.downloadTotal) return;

  const percent = Math.round(
    (bgFetch.downloaded / bgFetch.downloadTotal) * 100,
  );
  console.log(`Download progress: ${percent}%`);
});
Diagramm, das eine Seite zeigt, die einen Port an einen Service Worker weitergibt, um eine bidirektionale Kommunikation herzustellen.
Die Benutzeroberfläche wird aktualisiert, um den Fortschritt eines Downloads anzuzeigen (links). Dank Service Worker kann der Vorgang auch dann fortgesetzt werden, wenn alle Tabs geschlossen wurden (rechts).

Nächste Schritte

In diesem Leitfaden haben wir den allgemeinsten Fall der Kommunikation zwischen Seiten- und Dienst-Workern (d. h. die bidirektionale Kommunikation) untersucht.

Oft ist nur ein Kontext erforderlich, um mit dem anderen zu kommunizieren, ohne eine Antwort zu erhalten. In den folgenden Anleitungen finden Sie Informationen dazu, wie Sie unidirektionale Techniken auf Ihren Seiten vom und zum Service Worker implementieren. Außerdem finden Sie Anwendungsfälle und Produktionsbeispiele:

  • Imperatives Caching: Ein Service Worker wird von der Seite aufgerufen, um Ressourcen im Voraus im Cache zu speichern (z.B. in Prefetching-Szenarien).
  • Updates übertragen: Die Seite wird vom Service Worker aufgerufen, um über wichtige Updates zu informieren, z.B. dass eine neue Version der Webanwendung verfügbar ist.