Einfache Progressive Web App mit Service-Worker

von Oliver Richter

Worum gehts?

Wie lässt sich aus einer normalen Website eine einfache Progressive Web App (PWA) machen? Hier möchte ich zeigen, wo und wie man einen Service Worker einsetzt – ohne Framework, nur mit Vanilla JavaScript.

Einstieg

Eine PWA ist im Kern eine Website. Zusätzlich kann sie u. a. offline (oder zumindest eingeschränkt) funktionieren und sich unter bestimmten Voraussetzungen wie eine App auch installieren lassen. Dafür braucht man im Wesentlichen zwei Bausteine:
  1. ein Web App Manifest,
  2. einen Service Worker und die Registrierung des Service Workers in der Seite.
Wichtig vorab: Service Worker funktionieren nur über HTTPS. Für die lokale Entwicklung ist in der Regel auch localhost erlaubt.

Dateien und Ordnerstruktur im Beispiel

Für die hier gezeigte, einfache Variante reicht folgende Struktur im Root der Website:
 
  • index.html
  • offline.html
  • manifest.webmanifest
  • serviceworker.js
  • css/
  • styles.css
  • js/
    • app.js
  • icons/
    • icon-192.png
    • icon-512.png

Kurz zur Einordnung der oben genannten Dateien:

  • manifest.webmanifest – Name, Icons, Start-URL der PWA
  • serviceworker.js – der Service Worker (möglichst im Root, wegen des Scopes)
  • js/app.js – Registrierung des Service Workers (kann auch direkt in der HTML-Datei stehen)
  • offline.html – Fallback, wenn keine Netzverbindung besteht
  • icons/ – mindestens 192×192 und 512×512 Pixel

Die Pfade in den Beispielen weiter unten beziehen sich auf genau diese Struktur. Liegen Dateien bei Ihnen anders, müssen die Angaben in Manifest, Registrierung und weiteren Dateien entsprechend angepasst werden.

1. Web App Manifest

Das Manifest beschreibt, wie die App heißen soll und welche weiteren Eigenschaften wie Theme-Farben, Icons verwendet werden und wie sie gestartet wird. Eine sehr einfache Variante sieht so aus:

{
  "name": "Meine einfache PWA",
  "short_name": "MeinePWA",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#0a7a3e",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

Eingebunden wird das z. B. als manifest.webmanifest im Wurzelverzeichnis der Website. In der HTML-Datei wird sie im Head-Bereich der Seite wie folgt eingebunden:

<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#0a7a3e">

2. Den Service Worker registrieren

Was macht ein Serivce-Worker? Es ist ein Tool, das der Browser getrennt von der geöffneten Seite im Hintergrund ausführt. Er kann Netzwerk-Anfragen abfangen und zum Beispiel entscheiden, ob eine Datei aus dem Cache kommt oder neu aus dem Internet geladen wird. Genau deshalb ist er für eine PWA so wichtig: Ohne ihn gibt es kein sinnvolles Offline-Verhalten und auch keine App-ähnliche Zwischenspeicherung der Website.

Die Registrierung gehört in die Website selbst – also z. B. in eine `app.js` oder ans Ende der HTML-Datei. Der Service Worker wird dabei nicht einfach „mitgeladen“ wie ein normales Skriptsnippet, sondern beim Browser angemeldet:
if ("serviceWorker" in navigator) {
  window.addEventListener("load", function () {
    navigator.serviceWorker
      .register("/serviceworker.js")
      .then(function (registration) {
        console.log("Service Worker registriert:", registration.scope);
      })
      .catch(function (error) {
        console.error("Service Worker fehlgeschlagen:", error);
      });
  });
}
Sinnvoll ist, die Datei serviceworker.js möglichst im Root der Domain abzulegen. Der sogenannte Scope – also der Bereich der Website, den der Service Worker kontrollieren darf – hängt vom Speicherort der Datei ab. Liegt sie beispielsweise unter /js/serviceworker.js, kontrolliert sie oft nur Pfade unter /js/.

Ein einfacher Service Worker

Der Service Worker selbst läuft getrennt von der Seite im Hintergrund. Typischerweise sind dabei 3 Ereignisse vor allem relevant:

  1. install – der Worker wird installiert und kann Dateien in den Cache legen
  2. activate – der Worker übernimmt die Kontrolle und kann alte Caches aufräumen
  3. fetch – der Worker fängt Netzwerk-Anfragen ab und entscheidet, ob etwas aus dem Cache oder vom Netz kommt

Hier eine einfache Cache-First-Variante inklusive Offline-Fallback. Das heißt, es wird zuerst geprüft, ob die angeforderte Datei bereits im Cache liegt. Nur wenn dort nichts gefunden wird, geht die Anfrage ins Netz. Scheitert auch das – etwa weil keine Verbindung besteht –, liefert der Service Worker hilfsweise die Offline-Seite aus.
const CACHE_NAME = "pwa-cache-v1";
const ASSETS = [
  "/",
  "/index.html",
  "/offline.html",
  "/css/styles.css",
  "/js/app.js",
  "/icons/icon-192.png",
  "/icons/icon-512.png"
];

self.addEventListener("install", function (event) {
  event.waitUntil(
    caches.open(CACHE_NAME).then(function (cache) {
      return cache.addAll(ASSETS);
    })
  );
  self.skipWaiting();
});

self.addEventListener("activate", function (event) {
  event.waitUntil(
    caches.keys().then(function (keys) {
      return Promise.all(
        keys
          .filter(function (key) {
            return key !== CACHE_NAME;
          })
          .map(function (key) {
            return caches.delete(key);
          })
      );
    })
  );
  self.clients.claim();
});

self.addEventListener("fetch", function (event) {
  if (event.request.method !== "GET") {
    return;
  }

  event.respondWith(
    caches.match(event.request).then(function (cached) {
      if (cached) {
        return cached;
      }

      return fetch(event.request)
        .then(function (response) {
          var clone = response.clone();
          caches.open(CACHE_NAME).then(function (cache) {
            cache.put(event.request, clone);
          });
          return response;
        })
        .catch(function () {
          return caches.match("/offline.html");
        });
    })
  );
});
Beim Installieren werden die wichtigsten Dateien der Website vorgeladen. Beim Abruf wird zuerst der Cache geprüft. Ist nichts vorhanden, geht die Anfrage ins Netz. Schlägt auch das fehl, wird – sofern vorhanden – die Offline-Seite ausgeliefert.

Die Offline-Seite kann bewusst schlicht bleiben:
<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="utf-8">
  <title>Offline</title>
</head>
<body>
  <h1>Du bist offline</h1>
  <p>
  Diese Seite ist zwischengespeichert. Sobald wieder eine Verbindung besteht, 
  sollte die Website erneut erreichbar sein.
  </p>
</body>
</html>
Das ist nur eine sehr, sehr einfache Variante. Für Inhalte, die sich häufig ändern, wäre eher „Network First“ sinnvoll. Für eine erste PWA reicht die gezeigte Cache-First-Strategie aber aus.

Ein Hinweis aus der Praxis: `cache.addAll()` scheitert bereits, wenn eine einzige URL in der Liste einen Fehler (z. B. 404) liefert. Dann wird der gesamte Installationsschritt problematisch. Die Pfade in `ASSETS` sollten also wirklich alle existieren.

Cache-Versionierung

Ändern sich CSS, JavaScript oder andere vorgeladene Dateien, sollte man den Cache-Namen erhöhen, z. B. von `pwa-cache-v1` auf `pwa-cache-v2`. Im `activate`-Handler werden dann die alten Caches entfernt. Ohne Versionierung bleiben Besucher sonst leicht auf veraltetem Stand hängen.

Kurz prüfen

Zum Testen eignen sich die Entwicklertools im Browser (z. B. unter „Application“ bzw. „Anwendung“):

  1. Manifest und Icons prüfen
  2. Service Worker-Status prüfen (registriert / aktiv)
  3. Offline-Modus aktivieren und die Seite neu laden
  4. Cache-Inhalte kontrollieren

Wenn Manifest, gültiger Service Worker und HTTPS zusammenpassen, kann die Seite in unterstützten Browsern auch als App installierbar sein. Ob und wann ein Install-Hinweis erscheint, hängt allerdings vom Browser und ggf. weiteren Kriterien ab.

Mehr technische Details zum Service Worker finden Sie z. B. unter https://developer.mozilla.org/de/docs/Web/API/Service_Worker_API.

Zurück zur Newsübersicht

Kommentare

Einen Kommentar schreiben

Bitte rechnen Sie 4 plus 7.

Sie befinden Sich in der Detailansicht eines Blog-Eintrags.

Um alle Blog-Einträge zu sehen, wechseln Sie bitte zur Nachrichten-Übersicht