Service Worker in TypeScript

Service Worker in TypeScript

Dieser Artikel erklärt Service Worker in TypeScript.

Wir erklären Service Worker in TypeScript mit praktischen Beispielen.

YouTube Video

Service Worker in TypeScript

Ein Service Worker ist ein „Request Proxy“, der zwischen Browser und Netzwerk sitzt. Er ermöglicht das Abfangen von Requests (fetch), Cache-Kontrolle, Offline-Unterstützung und Hintergrundverarbeitung (Sync und Push). Die Verwendung von TypeScript bietet Typsicherheit und erhöht die Wartbarkeit.

Einrichtung von TypeScript

tsconfig.json (WebWorker-Typen aktivieren)

Sehen wir uns ein Beispiel an, wie man den WebWorker-Typ in tsconfig.json aktiviert.

 1{
 2  "compilerOptions": {
 3    "target": "ES2020",
 4    "module": "ES2020",
 5    "lib": ["ES2020", "WebWorker"],
 6    "moduleResolution": "Bundler",
 7    "strict": true,
 8    "noEmitOnError": true,
 9    "outDir": "out",
10    "skipLibCheck": true
11  },
12  "include": ["sw.ts"]
13}
  • Durch das Hinzufügen von WebWorker zum lib-Array können Sie Typen wie ServiceWorkerGlobalScope verwenden.
  • DOM und WebWorker haben unterschiedliche Typen, daher ist es gängige Praxis, die tsconfig.json-Einstellungen für den Browser (Hauptanwendung) und den Service Worker zu trennen.
  • Service Worker-Dateien werden letztendlich an einem Pfad ausgegeben, der dem Scope entspricht (in der Regel das Stammverzeichnis der Seite /sw.js).
  • Aus Sicherheitsgründen laufen Service Worker nur über HTTPS (oder auf localhost).

Registrierungscode auf der Browser-Seite

register-sw.ts

1// register-sw.ts
2async function registerServiceWorker() {
3  if (!('serviceWorker' in navigator)) return;
4
5  try {
6    const registration = await navigator.serviceWorker.register(
7      '/sw.js', { scope: '/' }
8    );
  • Dieser Vorgang registriert einen Service Worker. scope bezeichnet den Bereich der Pfade, die der Service Worker kontrollieren kann. Wenn Sie zum Beispiel /sw.js direkt im Stammverzeichnis ablegen und den scope auf das Stammverzeichnis (/) setzen, können Sie alle Ressourcen der gesamten Website kontrollieren. Wenn Sie hingegen ein bestimmtes Verzeichnis wie /app/ angeben, werden nur die Inhalte unter diesem Verzeichnis kontrolliert.
1    // If there's a waiting worker, notify the user.
2    if (registration.waiting) {
3      promptUserToUpdate(registration);
4    }
  • waiting zeigt den Zustand an, in dem ein neuer Service Worker installiert wurde und auf die Aktivierung wartet. In dieser Phase werden bestehende Seiten weiterhin vom alten Service Worker kontrolliert. Es ist üblich, den Benutzer um eine Bestätigung zu bitten und nach Zustimmung skipWaiting() aufzurufen, um den neuen Service Worker sofort zu aktivieren. Dies ermöglicht es Ihnen, die neuesten Prozesse anzuwenden, ohne auf das nächste Neuladen der Seite warten zu müssen.
 1    // When a new SW is installing, monitor its state changes
 2    registration.addEventListener('updatefound', () => {
 3      const newWorker = registration.installing;
 4      if (!newWorker) return;
 5      newWorker.addEventListener('statechange', () => {
 6        if (newWorker.state === 'installed' &&
 7            navigator.serviceWorker.controller) {
 8          // New content available, prompt the user
 9          promptUserToUpdate(registration);
10        }
11      });
12    });
  • updatefound wird ausgelöst, wenn die Installation eines neuen Service Workers begonnen hat. Wenn dieses Ereignis auftritt, wird ein neuer Worker in registration.installing gesetzt. Durch Überwachen von dessen statechange können Sie erkennen, wann die Installation abgeschlossen ist (installed). Wenn außerdem navigator.serviceWorker.controller existiert, bedeutet dies, dass ein alter Service Worker die Seite bereits steuert. Dies ist also eine Gelegenheit, den Benutzer über das Vorhandensein einer neuen Version zu informieren.
1    // When the active worker changes (e.g., after skipWaiting), reload if desired
2    navigator.serviceWorker.addEventListener('controllerchange', () => {
3      // Optionally reload to let the new SW take over
4      window.location.reload();
5    });
6  } catch (err) {
7    console.error('Service Worker registration failed: ', err);
8  }
9}
  • Das controllerchange-Ereignis wird in dem Moment ausgelöst, in dem der neue Service Worker die aktuelle Seite kontrolliert. Ein Neuladen der Seite zu diesem Zeitpunkt wendet sofort neue Cache-Strategien und Verarbeitungen an. Ein automatisches Neuladen kann jedoch das Benutzererlebnis verschlechtern, daher ist es besser, das Neuladen erst nach Zustimmung des Benutzers durchzuführen.
1function promptUserToUpdate(reg: ServiceWorkerRegistration) {
2  // Show UI to user. If user accepts:
3  if (reg.waiting) {
4    reg.waiting.postMessage({ type: 'SKIP_WAITING' });
5  }
6}
7
8registerServiceWorker();
  • Indem der Service Worker vom Client eine postMessage({ type: 'SKIP_WAITING' }) erhält und dann self.skipWaiting() aufruft, können Sie ein Update anstoßen.

Scope-Deklaration in sw.ts

Als Nächstes betrachten wir ein typisches Service-Worker-Beispiel, das Caching des App-Shells umsetzt.

Wenn Sie Service Worker in TypeScript verwenden, ist es sinnvoll, den korrekten Typ für self anzugeben.

1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
  • In TypeScript wird self standardmäßig als any behandelt. Ohne zusätzliche Typisierung gibt es also keine Typvervollständigung oder Typprüfung für Service Worker-spezifische APIs wie skipWaiting() oder clients.
  • Die Angabe von ServiceWorkerGlobalScope ermöglicht Autovervollständigung, verhindert Fehlgebrauch und fördert sicheres Entwickeln, getrennt von normalen DOM-Skripten.

Grundlegender Service Worker (Installieren/Aktivieren/Fetch)

Es zeigt einfaches Cache-Versionsmanagement, Pre-Caching beim Installieren, Löschen alter Caches beim Aktivieren und Cache-Strategien beim Fetch (Cache-First für statische Ressourcen, Network-First für APIs).

sw.ts (Minimale Einrichtung + Cache-Skelett)

 1const CACHE_NAME = 'app-shell-v1';
 2const STATIC_ASSETS = [
 3  '/',
 4  '/index.html',
 5  '/styles.css',
 6  '/main.js',
 7  '/fallback.png'
 8];
 9
10self.addEventListener('install', (event: ExtendableEvent) => {
11  // Pre-cache application shell
12  event.waitUntil(
13    caches.open(CACHE_NAME)
14      .then(cache => cache.addAll(STATIC_ASSETS))
15      // Activate immediately (optional: coordinate with client)
16      .then(() => self.skipWaiting())
17  );
18});
  • Während des install-Events werden die statischen Ressourcen (App Shell) der Anwendung vorab im Cache gespeichert. Durch den Aufruf von self.skipWaiting() wird der neue Service Worker sofort aktiviert und macht den neuesten Cache verfügbar, ohne auf den nächsten Zugriff zu warten.
 1self.addEventListener('activate', (event: ExtendableEvent) => {
 2  // Clean up old caches and take control of clients immediately
 3  event.waitUntil(
 4    caches.keys().then(keys =>
 5      Promise.all(keys
 6        .filter(key => key !== CACHE_NAME)
 7        .map(key => caches.delete(key)))
 8    ).then(() => self.clients.claim())
 9  );
10});
  • Beim activate-Event werden alte Cache-Versionen gelöscht und der Service Worker aktuell gehalten. Außerdem kann der neue Service Worker durch den Aufruf von self.clients.claim() alle Clients kontrollieren, ohne auf ein Neuladen der Seite warten zu müssen.
 1self.addEventListener('fetch', (event: FetchEvent) => {
 2  const request = event.request;
 3  const url = new URL(request.url);
 4
 5  // Navigation requests (SPA) -> network-first with fallback to cached index.html
 6  if (request.mode === 'navigate') {
 7    event.respondWith(
 8      fetch(request).catch(() => caches.match('/index.html') as Promise<Response>)
 9    );
10    return;
11  }
12
13  // Simple API routing: network-first for /api/
14  if (url.pathname.startsWith('/api/')) {
15    event.respondWith(networkFirst(request));
16    return;
17  }
18
19  // Static assets: cache-first
20  event.respondWith(cacheFirst(request));
21});
  • Im fetch-Event können Sie Anfragen abfangen und die Antwort steuern. Sie können Strategien wie cache-first oder network-first implementieren, die für Offline-Unterstützung und Performance hilfreich sind.
1self.addEventListener('message', (event: ExtendableMessageEvent) => {
2  const data = (event as any).data;
3  if (!data) return;
4
5  if (data.type === 'SKIP_WAITING') {
6    // Force the waiting service worker to become active
7    self.skipWaiting();
8  }
9});
  • Wenn SKIP_WAITING empfangen wird, können Sie durch den Aufruf von self.skipWaiting() den wartenden Service Worker sofort aktivieren. Infolgedessen wird die neue Version bereits mit der nächsten Anfrage angewendet, ohne dass die Seite neu geladen werden muss.

Praktischer Überblick zu Cache-Strategien

cache-first

Cache-first prüft zuerst den Cache und liefert, falls vorhanden, sofort die Antwort. Wenn nicht, wird aus dem Netzwerk geladen und das Ergebnis im Cache gespeichert. Dies eignet sich für statische Dateien.

 1async function cacheFirst(request: Request): Promise<Response> {
 2  const cache = await caches.open(CACHE_NAME);
 3  const cached = await cache.match(request);
 4  if (cached) {
 5    return cached;
 6  }
 7  const response = await fetch(request);
 8  if (response && response.ok) {
 9    cache.put(request, response.clone());
10  }
11  return response;
12}
  • Dieser Code zeigt eine Cache-First-Implementierung. Wenn ein Cache vorhanden ist, wird er zurückgegeben; sonst wird aus dem Netzwerk abgerufen und im Cache gespeichert. Es ist geeignet für statische Ressourcen wie Bilder oder CSS, die sich selten ändern.

network-first

Network-first versucht zuerst das Netzwerk und greift bei Misserfolg auf den Cache zurück. Dies eignet sich für APIs, bei denen Aktualität wichtig ist.

 1async function networkFirst(request: Request): Promise<Response> {
 2  const cache = await caches.open(CACHE_NAME);
 3  try {
 4    const response = await fetch(request);
 5    if (response && response.ok) {
 6      cache.put(request, response.clone());
 7    }
 8    return response;
 9  } catch (err) {
10    const cached = await cache.match(request);
11    if (cached) return cached;
12    return new Response(JSON.stringify({ error: 'offline' }), {
13      status: 503,
14      headers: { 'Content-Type': 'application/json' }
15    });
16  }
17}
  • Dieser Code zeigt eine Network-First-Implementierung. Wenn eine Netzwerkanwort empfangen wurde, wird diese im Cache gespeichert. Bei Fehlschlag wird die zwischengespeicherte Version zurückgegeben. Es ist geeignet für Ressourcen, die aktuelle Daten benötigen, z.B. Nachrichtenartikel oder API-Antworten.

stale-while-revalidate

Stale-while-revalidate liefert zuerst den Cache und aktualisiert ihn gleichzeitig im Hintergrund über das Netzwerk. Das balanciert Antwortgeschwindigkeit und Aktualität aus.

 1async function staleWhileRevalidate(request: Request, cacheName = CACHE_NAME): Promise<Response> {
 2  const cache = await caches.open(cacheName);
 3  const cachedResponse = await cache.match(request);
 4  const networkFetch = fetch(request).then(networkResponse => {
 5    if (networkResponse && networkResponse.ok) {
 6      cache.put(request, networkResponse.clone());
 7    }
 8    return networkResponse;
 9  }).catch(() => undefined);
10
11  // Return cached immediately if exists, otherwise wait network
12  return cachedResponse || (await networkFetch) || new Response('offline', { status: 503 });
13}
  • Dieser Code liefert sofort den Cache, falls verfügbar, und aktualisiert ihn im Hintergrund mit Daten aus dem Netzwerk. Es bietet schnelle Antworten für Nutzer und verwendet beim nächsten Zugriff aktualisierte Inhalte, was ideal für UI oder schlanke Datenzustellung ist.

Optimierung des Update-Ablaufs (Update-Benachrichtigung und sicheres Neuladen)

Service Worker-Updates sind nicht sofort wirksam; die neue Version bleibt in Wartestellung, bis alle bestehenden Tabs geschlossen sind.

Hier implementieren wir ein System, das den Client benachrichtigt, wenn die neue Version bereit ist und die Seite sicher basierend auf Nutzerinteraktion neu lädt.

Benachrichtigen Sie den Client auf Seite des Service Workers, wenn die neue Version bereit ist.

 1// In sw.ts: after 'activate' or when new version is ready, broadcast a message
 2async function notifyClientsUpdated() {
 3  const all = await self.clients.matchAll({ type: 'window' });
 4  for (const client of all) {
 5    client.postMessage({ type: 'SW_UPDATED' });
 6  }
 7}
 8
 9// e.g., call this at the end of 'activate'
10self.addEventListener('activate', (event) => {
11  event.waitUntil((async () => {
12    if ('navigationPreload' in self.registration) {
13      await self.registration.navigationPreload.enable();
14    }
15    // cache cleanup
16    const cacheNames = await caches.keys();
17    await Promise.all(
18      cacheNames.map((name) => {
19        if (name !== CACHE_NAME) {
20          return caches.delete(name);
21        }
22      })
23    );
24
25    await self.clients.claim();
26    await notifyClientsUpdated();
27  })());
28});
  • In diesem Code wird notifyClientsUpdated am Ende des activate-Events aufgerufen, um allen verbundenen Clients mitzuteilen, dass die neue Version bereit ist. clients.claim() ist eine Methode, mit der aktuell geöffnete Seiten (Clients) sofort unter die Kontrolle des neu aktivierten Service Workers gebracht werden. Normalerweise übernimmt ein Service Worker die Kontrolle über die Seite erst beim nächsten Laden, aber mit clients.claim() können Sie die Seite sofort unter Kontrolle bringen, ohne neu laden zu müssen.

Zeigen Sie eine Update-Benachrichtigung im Client-UI an und laden die Seite bei Nutzeraktion neu

1// in app startup
2navigator.serviceWorker.addEventListener('message', (e) => {
3  if (e.data?.type === 'SW_UPDATED') {
4    // Show a non-intrusive toast or banner: "New version available"
5    // When user clicks "Reload", call:
6    window.location.reload();
7  }
8});
  • Der Client empfängt SW_UPDATED per message-Event und zeigt eine Update-Benachrichtigung im UI an. Wenn der Benutzer das Neuladen auswählt, wird window.location.reload() ausgeführt und alte HTML-, CSS- und andere Ressourcen der Seite werden auf die neueste Version aktualisiert. Dadurch wird sichergestellt, dass der Cache und die Steuerung durch den Service Worker, der mit clients.claim() übernommen wurde, auf der gesamten Seite wirksam werden.

Offline-Fallback

Bereiten Sie /offline.html für kritische Navigationen vor und bieten Sie ein minimales UI, das auch ohne Bilder oder Schriftarten aussagekräftig ist. Bei Fehlschlag eines API-Requests zeigen Sie nach Möglichkeit den zuletzt gespeicherten Zustand an und versuchen, im Hintergrund erneut abzurufen, um die UX zu verbessern.

Implementierungsbeispiel

 1// sw.ts
 2const CACHE_NAME = 'app-cache-v1';
 3
 4// Cache offline.html during install
 5self.addEventListener('install', (event) => {
 6  event.waitUntil((async () => {
 7    const cache = await caches.open(CACHE_NAME);
 8    await cache.addAll(['/offline.html']);
 9  })());
10});
11
12// Handle fetch requests
13self.addEventListener('fetch', (event) => {
14  const request = event.request;
15
16  // Navigation requests (e.g., page transitions)
17  if (request.mode === 'navigate') {
18    event.respondWith((async () => {
19      try {
20        // Try to fetch from the network as usual
21        return await fetch(request);
22      } catch (err) {
23        // On failure, return offline fallback page
24        const cache = await caches.open(CACHE_NAME);
25        return await cache.match('/offline.html') as Response;
26      }
27    })());
28  }
29});
  • Cachen Sie /offline.html während des install-Events vor, damit Sie bei Netzwerkausfall zumindest eine Minimalseite zurückgeben können.
  • Im fetch-Event können Sie Navigationsanfragen mit request.mode === 'navigate' überwachen und gezielt Seitenwechsel ansprechen.
  • Fallen Sie auf /offline.html zurück, wenn das Netzwerk ausfällt, sodass die Seite auch offline angezeigt werden kann.

Nachrichtenübermittlung zwischen dem Client und dem Service Worker

Da der Service Worker unabhängig vom Lebenszyklus der Seite arbeitet, ist bidirektionale Kommunikation wichtig, um Status zu melden und Befehle auszuführen. Die Angabe von Typen für Nachrichten hilft, fehlerhaftes Versenden von Nachrichten zu verhindern, ermöglicht Codevervollständigung und macht die Implementierung robuster.

Codebeispiel

  1. Definition der Nachrichtentypen
1type SwToClient =
2  | { type: 'SW_READY' }
3  | { type: 'SW_UPDATED' }
4  | { type: 'CACHE_CLEARED' }
5  | { type: 'PING'; ts: number };
6
7type ClientToSw =
8  | { type: 'CLEAR_CACHE' }
9  | { type: 'PING'; ts: number };
  • SwToClient ist der Nachrichtentyp, der vom Service Worker an den Client gesendet wird.
  • ClientToSw ist der Nachrichtentyp, der vom Client an den Service Worker gesendet wird.
  • Dadurch können Sie die Arten von Ereignissen klarstellen, die über die bidirektionale Kommunikation ausgetauscht werden können.
  1. Verarbeitung auf Seite des Service Workers
 1self.addEventListener('message', (event) => {
 2  const data = event.data as ClientToSw;
 3  if (data?.type === 'CLEAR_CACHE') {
 4    event.waitUntil((async () => {
 5      const keys = await caches.keys();
 6      await Promise.all(keys.map((k) => caches.delete(k)));
 7      await broadcast({ type: 'CACHE_CLEARED' });
 8    })());
 9  } else if (data?.type === 'PING') {
10    event.source?.postMessage({ type: 'PING', ts: data.ts } as SwToClient);
11  }
12});
  • Der Service Worker empfängt Nachrichten vom Client und verarbeitet sie je nach Typ unterschiedlich.
  • Bei CLEAR_CACHE wird der Cache gelöscht und danach werden alle Clients mit CACHE_CLEARED benachrichtigt.
  • Bei PING antwortet er dem ursprünglichen Client mit einer PING-Nachricht inklusive Zeitstempel.
  1. Alle Clients vom Service Worker benachrichtigen
1async function broadcast(msg: SwToClient) {
2  const clients = await self.clients.matchAll({ includeUncontrolled: true });
3  for (const c of clients) c.postMessage(msg);
4}
  • Verwenden Sie clients.matchAll, um alle Fenster-Tabs zu erhalten.
  • Durch Senden von postMessage an jeden Client können Sie Nachrichten broadcasten.
  • Dies kann für Update-Benachrichtigungen (wie SW_UPDATED) und Fehlerbenachrichtigungen verwendet werden.
  1. Verarbeitung auf Client-Seite
1navigator.serviceWorker.controller?.postMessage({
2  type: 'PING',
3  ts: Date.now()
4} as ClientToSw);
  • Indem Sie einen PING vom Client senden und eine Antwort vom Service Worker erhalten, können Sie überprüfen, dass die bidirektionale Kommunikation korrekt funktioniert. Dadurch wird es einfacher, Verbindungsstatus und Nachrichtenverarbeitung zu testen.
 1navigator.serviceWorker.addEventListener('message', (e) => {
 2  const msg = e.data as SwToClient;
 3  switch (msg.type) {
 4    case 'SW_READY':
 5      console.log('Service Worker is ready');
 6      // Example: hide loading spinner or enable offline UI
 7      break;
 8    case 'SW_UPDATED':
 9      console.log('A new version of the Service Worker is available');
10      // Example: show update notification or reload prompt
11      const shouldReload = confirm('A new version is available. Reload now?');
12      if (shouldReload) {
13        window.location.reload();
14      }
15      break;
16    case 'CACHE_CLEARED':
17      console.log('Cache cleared');
18      // Example: show confirmation message to user
19      alert('Cache has been successfully cleared.');
20      break;
21    case 'PING':
22      console.log(`Received PING response, ts=${msg.ts}`);
23      break;
24  }
25});
  • {^ i18n_speak クライアント側では Service Worker から送信されるメッセージを受信し、種類に応じて処理を分岐します。SW_READY は初期化完了、SW_UPDATED は新バージョン検出、CACHE_CLEARED はキャッシュ削除完了、PING は通信確認を示します。各メッセージに応じて、UI の更新やリロード、通知表示などを行います。^}

Vorteile von typisierten Nachrichten

  • Durch typisierte Nachrichten wird klar ersichtlich, welche Nachrichten gesendet und empfangen werden können, und Autovervollständigung sowie Typprüfung erhöhen die Sicherheit.
  • postMessage ermöglicht Eins-zu-eins-Kommunikation, broadcast ermöglicht Eins-zu-viele-Kommunikation.
  • Sie können einfach essentielle Funktionen wie Update-Benachrichtigung (SW_UPDATED), Cache-Management (CACHE_CLEARED) und Health-Checks (PING) implementieren.

Zusammenfassung

  • Durch den Einsatz von TypeScript erhalten Service Worker API-Aufrufe und Nachrichten Typsicherheit, wodurch Entwicklungsaufwand und Wartbarkeit deutlich verbessert werden.
  • Das Verständnis der Lebenszyklusereignisse install, activate und fetch sowie die passende Caching-Strategie (wie cache-first oder network-first) je nach Situation führen zu einer besseren User Experience.
  • Für den Betrieb ist das Verständnis von Cache-Versionsmanagement und Update-Flows (updatefound, waiting, SKIP_WAITING usw.) essenziell.
  • Durch die Verwendung von typisierten Nachrichten für die Kommunikation zwischen Client und Service Worker können Sie Fehlimplementierungen verhindern und ein System schaffen, das langfristig leicht erweiterbar und wartbar ist.

Sie können den obigen Artikel mit Visual Studio Code auf unserem YouTube-Kanal verfolgen. Bitte schauen Sie sich auch den YouTube-Kanal an.

YouTube Video