Service Worker i TypeScript
Denne artikel forklarer Service Workers i TypeScript.
Vi vil forklare Service Workers i TypeScript, inklusive praktiske eksempler.
YouTube Video
Service Worker i TypeScript
En Service Worker er en “anmodnings-proxy”, der sidder mellem browseren og netværket. Den muliggør opsnapning af forespørgsler, cache-kontrol, offline-understøttelse og baggrundsprocesser (synkronisering og push-meddelelser). Brug af TypeScript giver typesikkerhed og øger vedligeholdelsen.
Opsætning af TypeScript
tsconfig.json
(Aktivér WebWorker-typer)
Lad os se på et eksempel på at aktivere WebWorker-typen i tsconfig.json
.
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}
- Ved at tilføje
WebWorker
tillib
-arrayet kan du bruge typer somServiceWorkerGlobalScope
. DOM
ogWebWorker
har forskellige typer, så det er almindelig praksis at adskilletsconfig.json
-indstillingerne for browseren (hovedappen) ogService Worker
.Service Worker
-filer gemmes til sidst på en sti, der matcher scopet (normalt websitets rod/sw.js
).- Af sikkerhedsmæssige årsager kører Service Workers kun over HTTPS (eller på
localhost
).
Registreringskode på browsersiden
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 );
- Denne proces registrerer en
Service Worker
.scope
refererer til rækken af stier, somService Worker
kan kontrollere. For eksempel, hvis du placerer/sw.js
direkte under roden og angiverscope
til rodmappen (/
), kan du kontrollere alle ressourcer på hele sitet. Omvendt, hvis du angiver en specifik mappe, såsom/app/
, vil kun indholdet under den pågældende mappe blive kontrolleret.
1 // If there's a waiting worker, notify the user.
2 if (registration.waiting) {
3 promptUserToUpdate(registration);
4 }
waiting
angiver tilstanden hvor en ny Service Worker er blevet installeret og venter på at blive aktiveret. På dette trin kontrolleres eksisterende sider stadig af den gamleService Worker
, så det er almindeligt at bede brugeren om bekræftelse, og efter godkendelse kaldeskipWaiting()
for straks at aktivere den nyeService Worker
. Dette gør det muligt at anvende de nyeste processer uden at vente på, at siden genindlæses.
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
udløses når installationen af en ny Service Worker er påbegyndt. Når denne begivenhed opstår, sættes en ny worker iregistration.installing
, så ved at overvåge densstatechange
, kan du opdage hvornår installationen er fuldført (installed
). Hvisnavigator.serviceWorker.controller
findes, betyder det yderligere, at en gammel Service Worker allerede styrer siden, og dette er en mulighed for at informere brugeren om, at der findes en ny version.
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}
controllerchange
-begivenheden afsendes i det øjeblik, hvor den nye Service Worker begynder at kontrollere den aktuelle side. Genindlæsning på dette tidspunkt vil straks anvende de nye cache-strategier og processer. Dog kan automatisk genindlæsning forværre brugeroplevelsen, så det er at foretrække at indlæse siden igen efter brugerens samtykke.
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();
- Ved at lade
Service Worker
modtage enpostMessage({ type: 'SKIP_WAITING' })
fra klienten og derefter kaldeself.skipWaiting()
, kan du igangsætte en opdatering.
Scope-deklaration i sw.ts
Lad os derefter se på et typisk eksempel på en Service Worker, der implementerer app shell-caching.
Når du bruger Service Workers i TypeScript, er det nyttigt at tildele den korrekte type til self
.
1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
- I TypeScript behandles
self
som standard somany
, så uden yderligere typning får du hverken typefuldførelse eller typekontrol for Service Worker-specifikke API'er somskipWaiting()
ellerclients
. - Hvis du angiver
ServiceWorkerGlobalScope
, aktiverer det autoudfyldning, forhindrer misbrug og muliggør mere sikker udvikling adskilt fra almindelige DOM-scripts.
Grundlæggende Service Worker (Installér/Aktivér/Hent)
Det demonstrerer enkel cache-versionsstyring, forudindlæsning ved installation, sletning af gamle caches ved aktivering og cache-strategier ved hentning (cache-first for statiske aktiver, network-first for API'er).
sw.ts
(Minimal opsætning + Cache-stel)
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});
- Under
install
-begivenheden bliver appens statisk ressourcer (App Shell) gemt på forhånd i cachen. Ved at kaldeself.skipWaiting()
aktiveres den nyeService Worker
med det samme, så den nyeste cache er tilgængelig uden at vente på næste adgang.
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});
- I
activate
-begivenheden slettes gamle versioner af caches, ogService Worker
holdes opdateret. Yderligere, ved at kaldeself.clients.claim()
, kan den nyeService Worker
styre alle klienter uden at vente på at siden genindlæses.
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});
- I
fetch
kan du opsnappe anmodninger og kontrollere svaret. Du kan implementere strategier som cache-first eller network-first, hvilket er nyttigt for offline-understøttelse og ydeevne.
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});
- Hvis
SKIP_WAITING
modtages, gør et kald tilself.skipWaiting()
det muligt at aktivere den ventende Service Worker med det samme. Som følge heraf vil den nye version blive anvendt fra næste forespørgsel uden behov for at genindlæse siden.
Oversigt over praktiske cache-strategier
cache-first
Cache-first tjekker først cachen og returnerer straks svaret, hvis det er tilgængeligt. Hvis ikke, hentes det fra netværket og resultatet caches. Dette egner sig til statiske filer.
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}
- Denne kode demonstrerer en cache-first-implementering. Hvis der er en cache, returneres den; hvis ikke, hentes den fra netværket og gemmes i cachen. Det er velegnet til statiske ressourcer, der sjældent ændres, såsom billeder eller CSS.
network-first
Network-first forsøger først netværket og falder tilbage på cachen, hvis det mislykkes. Dette passer til API'er, hvor det er vigtigt med friske data.
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}
- Denne kode demonstrerer en network-first-implementering. Hvis der modtages et svar fra netværket, gemmes det i cachen; hvis ikke, returneres den cachede version. Det egner sig til ressourcer, der kræver friske data, såsom nyhedsartikler eller API-svar.
stale-while-revalidate
stale-while-revalidate returnerer først cachen og opdaterer den samtidig fra netværket i baggrunden. Dette balancerer responshastighed og friskhed.
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}
- Denne kode returnerer straks cachen, hvis den er tilgængelig, mens nye data hentes fra netværket i baggrunden for at opdatere cachen. Det giver hurtige svar til brugerne og bruger opdateret indhold ved næste adgang, hvilket gør det velegnet til UI eller letvægts data-levering.
Optimering af opdateringsforløbet (opdateringsnotifikation og sikker genindlæsning)
Service Worker
-opdateringer sker ikke med det samme; den nye version vil forblive ventende, indtil alle eksisterende faner er lukket.
Her implementerer vi et system, der giver klienten besked, når den nye version er klar, og genindlæser siden sikkert baseret på brugerhandling.
Giv besked til klienten fra Service Worker
-siden, når den nye version er klar.
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});
- I denne kode kaldes
notifyClientsUpdated
i slutningen afactivate
-begivenheden for at informere alle tilsluttede klienter om, at den nye version er klar.clients.claim()
er en metode, der straks bringer de aktuelt åbne sider (klienter) under kontrol af den nyaktiverede Service Worker. Normalt begynder enService Worker
først at kontrollere siden ved næste indlæsning, men ved at anvendeclients.claim()
kan du straks bringe siden under kontrol uden genindlæsning.
Vis opdaterings-UI på klienten, og genindlæs ved brugerhandling
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});
- Klienten modtager
SW_UPDATED
viamessage
-begivenheden og viser en opdateringsnotifikation i UI'et. Når brugeren vælger at genindlæse, køreswindow.location.reload()
, hvilket opdaterer gammel HTML, CSS og andre ressourcer på siden til den nyeste version. Dette sikrer, at cache og styring fraService Worker
, der blev ændret medclients.claim()
, bliver reflekteret over hele siden.
Offline-fallback
Forbered /offline.html
til kritisk navigation, og giv en minimalistisk UI, der giver mening, selv uden billeder eller skrifttyper. Hvis et API-kald fejler, vis den sidst cachede tilstand hvis muligt, og prøv at hente i baggrunden for at forbedre UX.
Implementeringseksempel
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});
- Forudindlæs
/offline.html
underinstall
-begivenheden, så du kan returnere mindst en minimal side, når netværket ikke er tilgængeligt. - I
fetch
-begivenheden kan du overvåge navigationsanmodninger medrequest.mode === 'navigate'
og specifikt målrette sideovergange. - Fald tilbage til
/offline.html
, når netværket fejler, så siden vises, selv når der er offline.
Beskedudveksling mellem klienten og Service Worker
Da Service Worker
fungerer uafhængigt af sidens livscyklus, er tovejskommunikation vigtig for at underrette om tilstande og udføre kommandoer. At specificere typer for beskeder hjælper med at forhindre forkerte beskeder, muliggør kodefuldførelse og gør din implementering mere robust.
Kodeeksempel
- Definition af beskedtyper
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
er beskedtypen, der sendes fra Service Worker til klienten.ClientToSw
er beskedtypen, der sendes fra klienten til Service Worker.- Dette gør det muligt at præcisere de typer af begivenheder, der kan udveksles gennem tovejskommunikation.
- Håndtering på Service Worker-siden
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});
- Service Worker modtager beskeder fra klienten og forgrener behandlingen baseret på typen.
- For
CLEAR_CACHE
sletter den cachen og underretter derefter alle klienter medCACHE_CLEARED
. - For
PING
svarer den til den oprindelige klient med enPING
-besked inklusive et tidsstempel.
- Underret alle klienter fra Service Worker
1async function broadcast(msg: SwToClient) {
2 const clients = await self.clients.matchAll({ includeUncontrolled: true });
3 for (const c of clients) c.postMessage(msg);
4}
- Brug
clients.matchAll
for at hente alle vinduesfaner. - Ved at sende
postMessage
til hver, kan du udsende beskeder. - Dette kan bruges til opdateringsnotifikationer (såsom
SW_UPDATED
) og fejlnotifikationer.
- Håndtering på klientsiden
1navigator.serviceWorker.controller?.postMessage({
2 type: 'PING',
3 ts: Date.now()
4} as ClientToSw);
- Ved at sende en
PING
fra klienten og modtage et svar fraService Worker
, kan du bekræfte, at tovejskommunikation fungerer korrekt. Dette gør det nemmere at teste forbindelsestilstande og beskedhåndtering.
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 の更新やリロード、通知表示などを行います。^}
Fordele ved typede beskeder
- Brug af typede beskeder tydeliggør, hvilke beskeder der kan sendes og modtages, og autoudfyldelse og typekontrol øger sikkerheden.
postMessage
muliggør en-til-en kommunikation ogbroadcast
muliggør en-til-mange kommunikation.- Du kan nemt implementere essentielle funktioner som opdateringsnotifikationer (
SW_UPDATED
), cachehåndtering (CACHE_CLEARED
) og sundhedstjek (PING
).
Sammendrag
- Brug af TypeScript tilføjer typesikkerhed til Service Worker API-kald og beskedudveksling, hvilket forbedrer udviklingseffektiviteten og vedligeholdelsen betydeligt.
- Forståelse af
install
-,activate
- ogfetch
-livscyklusbegivenhederne og valg af den rette cache-strategi (såsom cache-first eller network-first) til hver situation giver en bedre brugeroplevelse. - For driften er det vigtigt at forstå cache-versionsstyring og opdateringsforløb (
updatefound
,waiting
,SKIP_WAITING
osv.). - Ved at anvende typede beskeder til kommunikation mellem klient og
Service Worker
, kan du forhindre fejlimplementering og etablere et system, som er let at udvide og vedligeholde på længere sigt.
Du kan følge med i ovenstående artikel ved hjælp af Visual Studio Code på vores YouTube-kanal. Husk også at tjekke YouTube-kanalen.