PWA und Offline-Betrieb
Eine Progressive Web App, kurz PWA, ist eine Web-App, die sich wie eine installierte App verhält. Zwei Dateien machen den Unterschied. Das Manifest beschreibt Name, Icons und Farben der App, damit Browser und Betriebssystem sie installieren und mit eigenem Fenster starten können. Der Service Worker ist ein Skript, das zwischen App und Netz sitzt. Er beantwortet Anfragen aus einem Cache, wenn das Netz fehlt.
In deinem Projekt
Abschnitt betitelt „In deinem Projekt“Laut Projektauftrag bringt eine PWA mit Offline-Betrieb einen Bonuspunkt. Sie lohnt sich für eine App, die unterwegs gebraucht wird, etwa eine Liste, die man im Keller ohne Empfang öffnet. Offline heisst dabei meist, dass die App startet und die zuletzt geladenen Daten zeigt. Neue Daten zu senden, während das Netz fehlt, ist deutlich aufwendiger. Ein Service Worker läuft nur in einem sicheren Kontext, also über HTTPS oder auf localhost, und GitHub Pages liefert HTTPS.
Ein kleines Beispiel
Abschnitt betitelt „Ein kleines Beispiel“Das Beispiel unter optional/pwa/de ist ein kleiner Katalog mit Vite. Manifest, Service Worker und Icons liegen im Ordner public, damit Vite sie unverändert in den Build kopiert. Die Adressen im Manifest sind relativ, so funktioniert die App auch unter einem Basispfad.
{ "id": "./", "name": "Lorraine Commons, Katalog", "short_name": "Lorraine Commons", "lang": "de-CH", "start_url": "./", "scope": "./", "display": "standalone", "background_color": "#ffffff", "theme_color": "#1f6b4f", "icons": [ { "src": "icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }, { "src": "icon.svg", "sizes": "any", "type": "image/svg+xml" } ]}Der Service Worker legt beim Installieren die Hülle der App in den Cache, also index.html und das Manifest. Beim Aktivieren löscht er seine eigenen Caches aus einer älteren Version.
self.addEventListener('install', (event) => { event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(SHELL)))})
// A new version removes the old caches of this app and leaves those of other apps aloneself.addEventListener('activate', (event) => { event.waitUntil( caches .keys() .then((keys) => Promise.all(keys.filter((key) => key.startsWith(PREFIX) && key !== CACHE).map((key) => caches.delete(key)))), )})Jede Anfrage der App läuft danach durch den Listener für fetch. Seiten, also Anfragen mit dem Modus navigate, holt er zuerst aus dem Netz und offline aus dem Cache. Weil eine SPA für jede Adresse dieselbe index.html braucht, nimmt er offline immer diese Datei. Daten unter /api/ holt er ebenfalls zuerst aus dem Netz, damit sie aktuell sind. Die Dateien der App ändern sich nur mit einem neuen Build, sie kommen deshalb zuerst aus dem Cache.
self.addEventListener('fetch', (event) => { const { request } = event if (request.method !== 'GET' || new URL(request.url).origin !== self.location.origin) return
// Every address of the SPA is answered by index.html, offline from the cache if (request.mode === 'navigate') { event.respondWith(networkFirst(request, './index.html')) } else if (new URL(request.url).pathname.includes('/api/')) { event.respondWith(networkFirst(request)) } else { event.respondWith(cacheFirst(request)) }})Die App registriert den Service Worker nur im Build. Während der Entwicklung würde er alte Dateien aus dem Cache liefern, und Änderungen wären nicht zu sehen. Probier das Beispiel deshalb mit npm run build und npm run preview.
// The built app registers the service worker below its base path. During development it would// keep old files in the cache, so npm run dev leaves it out.export function registerServiceWorker() { if (!import.meta.env.PROD || !('serviceWorker' in navigator)) return window.addEventListener('load', () => { navigator.serviceWorker.register(`${import.meta.env.BASE_URL}sw.js`, { scope: import.meta.env.BASE_URL }) })}Die Tests unter tests/sw.test.js führen den Service Worker in Node aus, mit kleinen Attrappen für self, caches und fetch. So prüfen sie ohne Browser, dass eine Adresse offline index.html bekommt und Dateien nur einmal aus dem Netz kommen.
Chrome und Edge bieten die Installation an, wenn das Manifest einen Namen, Icons mit 192 und 512 Pixeln, start_url und display enthält und die Seite über HTTPS kommt. Safari legt jede Website auf Wunsch ins Dock des Mac oder auf den Home-Bildschirm des iPhone, und Firefox installiert auf dem Desktop keine PWAs aus einem Manifest. Alle Apps desselben Origins teilen sich ausserdem die Caches, etwa alle Projekte unter einem Konto von GitHub Pages. Der Service Worker erkennt seine eigenen Caches deshalb am Präfix catalog- und löscht nur diese. Wer viele Dateien vorab in den Cache legen will, lässt den Service Worker von vite-plugin-pwa erzeugen, das auf Workbox aufbaut.
Offizielle Links
Abschnitt betitelt „Offizielle Links“Texte und Abbildungen stehen unter CC BY-SA 4.0, der Code unter der MIT-Lizenz und die Daten des Kurses unter CC0 1.0. Die Schrift der Abbildungen ist Kalam unter der SIL Open Font License 1.1. Die Quellen liegen auf GitHub.