Ein Service Worker ist nur ein Worker und entsprechend sollte der Nachrichtenaustausch via postMessage()
und MessageEvent eine Kleinigkeit sein – möchte man meinen! Tatsächlich ist die ganze Angelegenheit beim Service Worker etwas weniger trivial als bei anderen Workern, was an ein paar besonderen Umständen liegt:
- Eine Seite/App wird immer von einem Service Worker kontrolliert …
- … der zu einem gegebenen Zeitpunkt ggf. noch inaktiv sein könnte …
- … und nicht der einzige vorhandene Worker sein muss …
- … aber seinerseits mehrere Seiten/Apps (Clients) kontrollieren könnte.
Diese Gemengelage führt zu einer etwas asymmetrischen API, die zwar logisch, aber nicht direkt offensichtlich ist.
Eine Seite/App kann immer nur einen Service Worker haben. Allerhöchstens könnte es verschiedene Versionen geben, wenn sich z.B. nach der Installation eines Updates ein neuer Worker inaktiv in Warteposition hinter dem aktuellen, aktiven Worker steht. Daher kann es für eine Seite/App nur eine Service-Worker-Message-Quelle geben, die unter navigator.serviceWorker
anzuzapfen ist:
navigator.serviceWorker.addEventListener("message", (evt) => { window.alert(`Nachricht vom SW: ${ evt.data }`); });
Es kann natürlich sein, dass gar kein Service Worker aktiv ist oder es überhaupt niemals einen Service Worker für die Seite/App geben wird, aber das ist für das Registrieren eines Event Listeners nicht relevant. Wenn es keine Message-Events gibt (entweder mangels Nachrichten oder mangels Senders), wird der Handler einfach nie aufgerufen. Es ist, als würde man an einen Briefkasten aufstellen – das kann man machen, auch wenn man niemals einen Brief erhalten wird.
Anders sieht es beim Senden von Nachrichten aus. Hier wird ein Ziel benötigt und dieses Ziel muss in der Lage sein, Nachrichten anzunehmen. Das heißt, dass wir darauf warten müssen dass ein Service Worker aktiv wird und dann auch explizit diesen als Adressaten auswählen müssen. In Code bedeutete das:
// Nachrichten zum SW senden // 1. warten bis der SW aktiv ist navigator.serviceWorker.ready .then( (registration) => { // 2. Zugriff auf aktiven SW erhalten if (registration.active) { // 3. Nachricht senden registration.active.postMessage(23); } });
Das Promise unter navigator.serviceWorker.ready
liefert eine ServiceWorkerRegistration, sobald es einen aktiven Service Worker gibt. Das ServiceWorkerRegistration-Objekt enthält Properties für Service Worker in verschiedenen Stadien: installing
, waiting
und active
. Eine Seite/App wird immer von exakt einem Worker kontrolliert, aber wenn sich neben dem aktivem Worker beispielsweise grade ein Update installiert, sind trotzdem zwei Worker vorhanden. Eine Message muss immer an einen Worker gehen, also rufen wir dort postMessage()
auf.
Natürlich spricht auch nichts dagegen, einem nicht-kontrollierenden Worker eine Nachricht zu senden oder mehrere Nachrichten an mehrere Worker zu verteilen:
// Nachrichten zum SW senden // 1. warten bis der SW aktiv ist navigator.serviceWorker.ready .then( (registration) => { // 2. Zugriff auf aktiven SW erhalten if (registration.active) { // 3. Nachricht senden registration.active.postMessage(23); } // 4. Zugriff auf SW für bereits installiertes Update erhalten if (registration.waiting) { // 5. Nachricht senden registration.waiting.postMessage(42); } });
Aufseiten des Workers ist das Empfangen von Nachrichten recht simpel: das message
-Event auf dem globalen Objekt fängt alle Meldungen aller verbundener Seiten/Apps ein. In der source
-Eigenschaft des Event-Objekts befindet sich das Client-Objekt, das die sendende Seiten/App repräsentiert. Ein solches Client-Objekt implementiert seinerseits postMessage()
, so dass sich Nachrichten leicht zurücksenden lassen:
// Nachrichten aus der Webseite empfangen self.addEventListener("message", (evt) => { const client = evt.source; client.postMessage(`Pong: ${ evt.data }`); });
Zugriff auf alle übrigen Clients gibt es über die asynchrone Methode self.clients.matchAll()
, so dass sich eine Nachricht von einem Client in den Worker an alle anderen Clients weiterverteilen lässt:
self.addEventListener("message", async (evt) => { const messageSource = evt.source; const clients = await self.clients.matchAll(); for (const client of clients) { if (client !== messageSource) { client.postMessage(`Message from ${ messageSource.id }: ${ evt.data }`); } } });
Und schon funktioniert der Nachrichtenaustausch zwischen Client(s) und Service Worker problemlos! Eigentlich ist das ganze Wirrwarr nur eine Konsequenz aus der grundsätzlichen Funktionsweise von Service Worker und somit hoffentlich nur auf den ersten Blick leicht irritierend.