Webkrauts Logo

Webkrauts Webkrauts Schriftzug

- für mehr Qualität im Web

At Your Service

Einführung in Service Worker

At Your Service

Mit Service Workern ist es möglich, per JavaScript bestimmte Dienste auch dann auszuführen, wenn der Nutzer die dazugehörige Website gar nicht geöffnet hat. Eine kleine Einführung in eine Technologie mit großem Potential.

Prognosen, insbesondere die Zukunft betreffend, sind ja bekanntermaßen so eine Sache. Dennoch ist zu konstatieren, dass in naher Zukunft alle modernen Browser eine Technologie unterstützen werden, die Websites und -applikationen mit großen Schritten in Richtung »nativer Applikation« führen wird: Service Worker. Diese

Hier ein paar Beispiele aus der Praxis:

Screenshot von trivago.de mit Hinweis:  Das Portal hält die Datenübertragung  zurück, bis das Netzwerk wieder verfügbar ist
trivago.de hält die Datenübertragung zurück, bis das Netzwerk wieder verfügbar ist.
Screenshot: Eigene Offline-Fehlermeldung auf matthiasott.com
Eigene Offline-Fehlermeldung auf matthiasott.com
Screenshot: Ein Beispiel für eine Push Notification aus einem Webkrauts-Artikel von 2015
Ein Beispiel für eine Push Notification

Service Worker gehören zur Familie der Web Worker und sind JavaScripts, die nicht auf das DOM zugreifen können und sollen. Stattdessen verwalten sie den Zwischenspeicher des Browsers (jedoch nicht den berühmten Browsercache), ausgehende Netzwerk-Requests und machen beide programmatisch steuerbar.

Verpflichtend ist neben einem modernen Browser allerdings eine https-Umgebung, da wir es mit dem Modifizieren von Netzwerk-Anfragen zu tun haben – nichts, was über eine ungesicherte Verbindung passieren sollte. Darüber hinaus braucht der geneigte Webschaffende Kontrolle über den Ablageort des Workers innerhalb der URL-Struktur der Website, denn dieser bestimmt seinen maximalen Einflussbereich (mehr zum Service Worker-»Scope« unten im Artikel).

Ein einfaches Beispielszenario

Stellt euch vor, ihr seid mit folgenden Anforderungen konfrontiert:

  • Assets (CSS, JavaScript, Bilder etc.) sollen in den Cache geladen werden, um grundsätzlich die Ladezeiten der Website zu verbessern und auf sie im Offline-Fall zurückzugreifen.
  • Weiterhin soll der Speicher ebenfalls alle besuchten Dokumente innerhalb des Service-Worker-Scopes aufnehmen.
  • Sofern der Benutzer offline ist und ein noch nicht gecachtes Dokument besucht, soll eine eigene Fehlerseite ausgegeben werden.

Um diese Aufgaben zu meistern, müsst ihr euch mit den wichtigsten Events im Lebenszyklus eines Service Worker vertraut machen: Registrierung und Installation, sowie das Abfangen von Netzwerk-Anfragen (Fetch). Darüber hinaus ist das Aktivierungs-Ereignis interessant – bietet es doch die Möglichkeit, veraltete Cache- oder Service-Worker-Versionen zu entfernen und sich somit beim Erneuern von z.B. CSS- oder JavaScript-Dateien nicht selbst zu überlisten.

Registrierung

Ein Promise-Objekt bildet eine asynchrone Berechnung ab und existiert in drei Zuständen: Zum Start seines Lebenszyklus ist ein Promise pending, also weder erfolgreich (fullfilled) noch gescheitert (rejected). Im Beispielcode dieses Artikels fangen wir den Erfolgsfall mit einem Callback als erstem Parameter von .then(), und den Misserfolgsfall mit einem Callback als erstem Parameter von .catch() ab. Vertiefende Informationen zu Promises bietet unter anderem MDN.

Zunächst fragt ihr ab, ob der Browser Service Worker überhaupt unterstützt. Ist das gegeben, wird das im Folgenden sw.js genannte Script registriert. Im Beispiel ist der Gültigkeitsbereich des Workers ausschließlich der Ordner /test, alle Dateien und Unterverzeichnisse darin. Beachtet hierbei, dass ihr die Angabe scope zwar weglassen könnt, ein Service Worker aber niemals in Ordnern »oberhalb« seines Ablageorts wirken kann.

Der Registrierungs-Vorgang liefert eine Promise (siehe Infokasten) zurück und macht den Erfolgs- und Misserfolgsfall handhabbar. Im Beispiel vermeldet die Konsole die gute oder schlechte Nachricht und schickt die Systemmeldungen gleich mit:

  1. if ('serviceWorker' in navigator) {
  2.   navigator.serviceWorker.register('/sw.js', { scope: '/test/' }).then(
  3.     function (reg) {
  4.       console.log('Service Worker registiert', reg)
  5.     },
  6.     function (err) {
  7.       console.log('Fehler: ', err)
  8.     }
  9.   );
  10. }

Installation

Ist das Script erfolgreich registriert und erkennt der Browser den Service Worker als neu (weil er der erste ist, oder weil er einen Unterschied zwischen dem aktiven und dem neuregistrierten feststellt), kann es mit dem nächsten Schritt, der Installation, weitergehen. Dies ist üblicherweise die Gelegenheit für das Befüllen des sogenannten »Static Cache«, beispielsweise:

  1. this.addEventListener('install', function(event) {
  2.   event.waitUntil(
  3.     caches.open('meinCacheVersion1').then(function(cache) {
  4.       return cache.addAll([
  5.         '/test/',
  6.         '/test/index.html',
  7.         '/test/style.css',
  8.         '/test/bundle.js',
  9.         '/test/logo.jpg',
  10.         '/test/sorry-offline.html',
  11.       ]);
  12.     })
  13.   );
  14. });

Nun stehen diese Assets theoretisch dem Website-Besucher selbst im Offline-Fall zur Verfügung. Praktisch muss aber noch die Netzwerkanfrage abgefangen und Inhalte aus meinCacheVersion1 zurückgeliefert werden. Das passiert mit dem -Event.

Fetch

In einer idealen Welt befindet sich die angeforderte Ressource in unserem Static Cache:

  1. this.addEventListener('fetch', function(event) {
  2.   // Reagiere auf Anfragen mit passenden Ressourcen aus dem Cache
  3.   event.respondWith(
  4.     caches.match(event.request)
  5.   );
  6. });

In der Praxis ist aber mit einer komplexeren Situation zu rechnen: Manche Anfragen können aus dem Cache bedient werden, manche nicht. Da caches.match() einen Promise zurückliefert und diese als rejected zurückkommt, wenn die angeforderte Ressource im Cache nicht gefunden wird, fangt ihr diesen Fall folgendermaßen ab und leitet die Anfrage an das Netzwerk weiter:

  1. this.addEventListener('fetch', function(event) {
  2.   // Reagiere auf Anfragen mit passenden Ressourcen aus dem Cache
  3.   event.respondWith(
  4.     caches.match(event.request)
  5.         .catch(function() {
  6.         // Wenn das nicht möglich ist, leite sie an das Netzwerk weiter
  7.             return fetch(event.request);
  8.         })
  9.   );
  10. });

Damit seid ihr eigentlich mit einer einfachen Implementierung eines Service Workers fertig. Aber euer Szenario stellt noch weitere Anforderungen – da das fetch-Event erkennt, was an Dateien und Dokumente angefordert wird, können ihr diese Ressourcen nun gleich »im Gallopp« im Cache ergänzen:

  1. this.addEventListener('fetch', function(event) {
  2.   event.respondWith(
  3.     // Reagiere auf Anfragen mit passenden Ressourcen aus dem Cache
  4.     caches.match(event.request).catch(function() {
  5.       // Wenn das nicht möglich ist, leite sie an das Netzwerk weiter
  6.       return fetch(event.request).then(function(response) {
  7.         // Sobald das geschehen ist, befülle 'meinCacheVersion1' mit den angefragten Ressourcen
  8.         return caches.open('meinCacheVersion1').then(function(cache) {
  9.           cache.put(event.request, response.clone());
  10.           return response;
  11.         });  
  12.       });
  13.     })
  14.   );
  15. });

Wenn sich eine Ressource weder im Cache befindet, noch gerade eine Verbindung zu einem Netzwerk besteht, dann verdient dies ein eigenes Handling. Im Installationsschritt haben wir eine Seite namens sorry-offline.html im Cache abgelegt. Und diese lässt sich für dieses Worst Case Scenario ausgeben – beispielsweise so:

  1. this.addEventListener('fetch', function(event) {
  2.   event.respondWith(
  3.     // Reagiere auf Anfragen mit passenden Ressourcen aus dem Cache
  4.     caches.match(event.request).catch(function() {
  5.       // Wenn das nicht möglich ist, leite sie an das Netzwerk weiter
  6.       return fetch(event.request).then(function(response) {
  7.         // Sobald das geschehen ist, befülle 'meinCacheVersion1' mit den angefragten Ressourcen
  8.         return caches.open('meinCacheVersion1').then(function(cache) {
  9.           cache.put(event.request, response.clone());
  10.           return response;
  11.         });  
  12.       }).catch(function() {
  13.         // Scheitert die Cache- und Netzwerkanfrage, liefere sorry-offline.html
  14.         return caches.match('/test/sorry-offline.html');
  15.       });
  16.     })
  17.   );
  18. });

Aktivierung

Dass Website-Assets einmal hochgeladen und danach nie wieder angefasst werden, ist völlig unrealistisch. Entsprechend gibt es ein Verfahren, Service Worker (bzw. Caches) zu löschen und zu aktualisieren. Dazu aktualisiert ihr euer install-Event mit den neuen oder geänderten Daten und nutzt das activate-Event, indem ihr alle Caches außer dem aktuellen löscht (hier 'meinCacheVersion2' genannt):

  1. this.addEventListener('activate', function(event) {
  2.   let cacheWhitelist = ['meinCacheVersion2'];
  3.  
  4.   // Lösche alle Caches, die nicht in der Whitelist genannt sind
  5.   event.waitUntil(
  6.     caches.keys().then(function(keyList) {
  7.       return Promise.all(keyList.map(function(key) {
  8.         if (cacheWhitelist.indexOf(key) === -1) {
  9.           return caches.delete(key);
  10.         }
  11.       }));
  12.     })
  13.   );
  14. });

Hiermit sind die wesentlichen Vorraussetzungen für ein besseres Offline-Handling einer handelsüblichen Website geschaffen und alle Anforderungen aus Eurem Beispiel-Szenario erfüllt. Service Worker können aber noch mehr, beispielsweise:

  • Anfragen an das Netzwerk so lange zurückhalten, bis ein Netzwerk wieder verfügbar ist (Background Sync).
  • Selbst dann Push-Benachrichtigungen ausführen, wenn das Browserfenster geschlossen ist (Push Manager).
  • Mit der Channel Messaging API kann ein Service Worker mit anderen installierten Workern unabhängig vom Browserkontext (und damit z.B. in zwei iFrames) kommunizieren.

Wie bei allen neuen Technologien stellt sich die Frage nach der Browserunterstützung. Hierzu gibt es aber zwei gute Nachrichten:

Erstens unterstützen Firefox und Chrome Service Worker bereits seit 2015 (ab Version 44, respektive 45). Microsoft hat die volle Unterstützung für Edge 17 angekündigt und stellt eine Vorschau mit dem Windows Insider Release 17063 zur Verfügung. Auch Apple aktiviert mit der Safari Technology Preview 46 seine Service-Worker-Implementierung, sodass mit einem zeitnahen Eintreffen auf den Standardbrowsern von macOS und iOS zu treffen ist. Eine komplette Übersicht hält wie immer caniuse bereit.

Zweitens lassen sich Service Worker ganz im Sinne des Progressive Enhancements einsetzen: Wenn Browser moderne Features nicht unterstützen, ist das nicht weiter tragisch. Benutzer »fähiger« Browser wiederum werden belohnt.

Spätestens, wenn diese Technologie die iOS-Geräte der Entscheider erreicht, wird sich »Service Worker« also vollends zu den beliebten Online-Marketing-Buzzwords »One Pager«, »Responsive« und »Parallax« gesellen.

Wer durch diese – zugegebenermaßen sehr an der Oberfläche kratzende – Vorstellung neugierig auf den steuerbaren Netzwerk-Proxy geworden ist, dem sei der umfassende deutschsprachige Artikel bei MDN Web Docs ans Herz gelegt – sowie die zahlreichen englischsprachigen Quellen und Tutorials zum Thema. Einen winzigen Auschnitt daraus findet ihr hier:

Neuen Kommentar schreiben

Bitte beachtet unsere Hausregeln fürs Kommentieren. Die Kommentare werden nach sechs Wochen geschlossen.