PWA + Service Worker

16. März 2019

Wer sich etwas mit Webentwicklung beschäftigt, der wird in der nahen Vergangenheit die Abkürzung PWA öfter gelesen haben.

Was ist PWA?

PWA steht für Progressive Web App und ist eine Art der Entwicklung von Webseiten, dass diese sich am Ende ähnlich wie native Anwendungen verhalten. Dazu gehören folgende Merkmale:

  • Offline-Funktionalität
  • Push-Notifications
  • „Installierbar“ und Icon auf dem Desktop bzw. bei Tables und Smartphones auf dem Homescreen.

Gerade ist die Version 73 von Chrome erschienen, mit der dieser jetzt auf allen Desktop-Betriebssystemen (also Windows, Mac und Linux) die o.g. Funktionen bietet. Auf Android-Geräten sind diese ebenfalls bereits vollumfänglich vorhanden.

Ein kleiner Ausreißer stellt iOS dar. Apple war lange Zeit etwas zögerlich, was PWAs angeht, erwirtschaften sie doch einen erheblichen Teil des Gewinnes über ihren App-Store. Eine PWA benötigt keinen App-Store und kann direkt von einer Seite aus „installiert“ werden. Seit Version 11.2 oder 11.3 hat sich allerdings auch unter iOS einiges verbessert, auch wenn es noch nicht alle Funktionen gibt (z.B. Push-Notifications).

Auf die Themen Push-Notifications möchte ich in diesem Beitrag nicht näher eingehen (vielleicht in Zukunft) und mich auf das Thema Offline-Funktionalität beschränken. Und genau hier kommt der Service Worker ins Spiel.

Was ist ein Service Worker?

Die meiner Meinung nach treffendste Beschreibung eines Service Workers ist, dass dieser einen Proxy zwischen der Webseite und dem Webserver darstellt. Dies bedeutet, dass Requests von einer Webseite nicht mehr direkt vom Browser zum Webserver gehen, sondern vom Browser an den Service Worker, der dann entscheidet, ob er den Request an den Webserver weiterleitet oder die Daten, die er bereits im Cache hat, zurück liefert.

Nebenbei ist der Service Worker eine wichtige Komponente wenn es um Push-Notifications geht, aber darauf will ich jetzt ja nicht eingehen 😉.

Etwas wichtiges Vorweg: Service Worker funktionieren nur in Kombination mit https oder localhost! Let’s Encrypt sei dank, sollte dies allerdings kein großes Problem mehr darstellen.

Beim Öffnen einer Seite muss der Service Worker installiert werden:

<script>
  if (navigator.serviceWorker) {
    navigator.serviceWorker.register("/service-worker.js");
  }
</script>

Ja, mehr ist es nicht (OK, etwas Fehlerbehandlung könnte nicht schaden) 🤷‍♂️😀.

Im Code des Service Workers gibt es drei wichtige Events, die behandelt werden sollten:

install

Dieses Event wird beim Installieren des Service-Workers aufgerufen. Hierbei ist wichtig zu erwähnen, dass ein Service-Worker min. alle 24 Stunden erneuert wird (Verbindung vorausgesetzt). Ein manuelles Update durch den Benutzer ist ebenfalls möglich, indem die Methode update() auf dem ServiceWorkerRegistration-Objekt, welches bei navigator.serviceWorker.register zurückgegeben wird, aufgerufen wird.

Dieses Event hat die Aufgabe, den Cache vorzubereiten und zu befüllen.

const CACHE_VERSION = 1;
const CACHE_PREFIX = "prefetch-cache-v";
const CURRENT_CACHE = CACHE_PREFIX + CACHE_VERSION;

self.addEventListener("install", async (event) => {
  const preCache = async () => {
    const cache = await caches.open(CURRENT_CACHE);
    await cache.addAll([
      "/",
      "/index.html",
      "css/style.css",
      "js/app.js",
      "https://www.example.com/this/that/image.gif"
    ]);
  }

  event.waitUntil(preCache());
});

activate

Activate wird aufgerufen, wenn install durchgeführt wurde. Hier können alte Caches bereinigt/gelöscht werden.

self.addEventListener("activate", (event) => {
  const clearCache = async () => {
    const keys = await caches.keys();

    for (let key of keys) {
      if (!key.startsWith(CACHE_PREFIX)) {
        continue;
      }

      const v = parseInt(key.substr(CACHE_PREFIX.length));
      if (v < CACHE_VERSION) {
        await caches.delete(key);
      }
    }
  }

  event.waitUntil(clearCache());
});

fetch

Wenn von einer Webseite eine Anforderung an den Webserver passiert, dann wird dies – ganz in aller Proxy Manier, durch dieses Event geschleust. Dieses entscheidet dann, ob das Ergebnis aus dem Cache oder vom Webserver geholt werden soll.

self.addEventListener("fetch", async (event) => {
  const eval = () => {
    try {
      return caches.match(event.request, {
        cacheName: CURRENT_CACHE
      });
    }
    catch {
      return fetch(event.request);
    }
  };

  event.respondWith(eval());
});

Mein Beispielcode soll einen kleinen Einblick geben, wie diese Events zu verwenden sind. Für eine Produktiveinsatz sind sie vermutlich nicht geeignet 😉.

Nachdem bei der Programmierung mit Service Workers sehr viel falsch gemacht werden kann und sich sehr viel in den Projekten wiederholt (Boilerplate), hat Google ein Projekt namens Workbox erstellt. Mit Hilfe von Workbox wird die ganze Sache erheblich vereinfacht. Ein entsprechender Beitrag dazu ist in Planung 💪.