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
zumlib
-Array können Sie Typen wieServiceWorkerGlobalScope
verwenden. DOM
undWebWorker
haben unterschiedliche Typen, daher ist es gängige Praxis, dietsconfig.json
-Einstellungen für den Browser (Hauptanwendung) und denService 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 derService Worker
kontrollieren kann. Wenn Sie zum Beispiel/sw.js
direkt im Stammverzeichnis ablegen und denscope
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 altenService Worker
kontrolliert. Es ist üblich, den Benutzer um eine Bestätigung zu bitten und nach ZustimmungskipWaiting()
aufzurufen, um den neuenService 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 inregistration.installing
gesetzt. Durch Überwachen von dessenstatechange
können Sie erkennen, wann die Installation abgeschlossen ist (installed
). Wenn außerdemnavigator.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 einepostMessage({ type: 'SKIP_WAITING' })
erhält und dannself.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 alsany
behandelt. Ohne zusätzliche Typisierung gibt es also keine Typvervollständigung oder Typprüfung für Service Worker-spezifische APIs wieskipWaiting()
oderclients
. - 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 vonself.skipWaiting()
wird der neueService 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 derService Worker
aktuell gehalten. Außerdem kann der neueService Worker
durch den Aufruf vonself.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 vonself.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 desactivate
-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 einService Worker
die Kontrolle über die Seite erst beim nächsten Laden, aber mitclients.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
permessage
-Event und zeigt eine Update-Benachrichtigung im UI an. Wenn der Benutzer das Neuladen auswählt, wirdwindow.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 denService Worker
, der mitclients.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 desinstall
-Events vor, damit Sie bei Netzwerkausfall zumindest eine Minimalseite zurückgeben können. - Im
fetch
-Event können Sie Navigationsanfragen mitrequest.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
- 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.
- 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 mitCACHE_CLEARED
benachrichtigt. - Bei
PING
antwortet er dem ursprünglichen Client mit einerPING
-Nachricht inklusive Zeitstempel.
- 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.
- 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 vomService 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
undfetch
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.