Service Worker i TypeScript

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 til lib-arrayet kan du bruge typer som ServiceWorkerGlobalScope.
  • DOM og WebWorker har forskellige typer, så det er almindelig praksis at adskille tsconfig.json-indstillingerne for browseren (hovedappen) og Service 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, som Service Worker kan kontrollere. For eksempel, hvis du placerer /sw.js direkte under roden og angiver scope 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 gamle Service Worker, så det er almindeligt at bede brugeren om bekræftelse, og efter godkendelse kalde skipWaiting() for straks at aktivere den nye Service 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 i registration.installing, så ved at overvåge dens statechange, kan du opdage hvornår installationen er fuldført (installed). Hvis navigator.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 en postMessage({ type: 'SKIP_WAITING' }) fra klienten og derefter kalde self.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 som any, så uden yderligere typning får du hverken typefuldførelse eller typekontrol for Service Worker-specifikke API'er som skipWaiting() eller clients.
  • 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 kalde self.skipWaiting() aktiveres den nye Service 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, og Service Worker holdes opdateret. Yderligere, ved at kalde self.clients.claim(), kan den nye Service 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 til self.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 af activate-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 en Service Worker først at kontrollere siden ved næste indlæsning, men ved at anvende clients.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 via message-begivenheden og viser en opdateringsnotifikation i UI'et. Når brugeren vælger at genindlæse, køres window.location.reload(), hvilket opdaterer gammel HTML, CSS og andre ressourcer på siden til den nyeste version. Dette sikrer, at cache og styring fra Service Worker, der blev ændret med clients.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 under install-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 med request.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

  1. 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.
  1. 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 med CACHE_CLEARED.
  • For PING svarer den til den oprindelige klient med en PING-besked inklusive et tidsstempel.
  1. 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.
  1. 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 fra Service 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 og broadcast 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- og fetch-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.

YouTube Video