Service Worker i TypeScript

Service Worker i TypeScript

Denne artikkelen forklarer Service Workers i TypeScript.

Vi vil forklare Service Workers i TypeScript, inkludert praktiske eksempler.

YouTube Video

Service Worker i TypeScript

En Service Worker er en “forespørsels-proxy” som sitter mellom nettleseren og nettverket. Den muliggjør avskjæring av forespørsler, cache-kontroll, frakoblet støtte og bakgrunnsprosessering (synk og push). Å bruke TypeScript gir typesikkerhet og øker vedlikeholdbarheten.

Sette opp TypeScript

tsconfig.json (Aktiver WebWorker-typer)

La oss se på et eksempel på hvordan WebWorker-typen aktiveres 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 å legge til WebWorker i lib-arrayet kan du bruke typer som ServiceWorkerGlobalScope.
  • DOM og WebWorker har ulike typer, så det er vanlig praksis å skille tsconfig.json-innstillingene for nettleseren (hovedapplikasjonen) og Service Worker.
  • Service Worker-filer blir til slutt lagt ut på en sti som samsvarer med omfanget (vanligvis nettstedets rot /sw.js).
  • Av sikkerhetsgrunner kjører Service Workers kun over HTTPS (eller på localhost).

Registreringskode på nettlesersiden

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 prosessen registrerer en Service Worker. scope refererer til området av stier som Service Worker kan kontrollere. For eksempel, hvis du plasserer /sw.js direkte under roten og setter scope til rotkatalogen (/), kan du kontrollere alle ressursene på hele nettstedet. På den annen side, hvis du spesifiserer en bestemt katalog som /app/, vil kun innholdet under denne katalogen bli kontrollert.
1    // If there's a waiting worker, notify the user.
2    if (registration.waiting) {
3      promptUserToUpdate(registration);
4    }
  • waiting indikerer tilstanden hvor en ny Service Worker er installert og venter på å bli aktivert. På dette stadiet kontrolleres eksisterende sider fortsatt av den gamle Service Worker, så det er vanlig å be brukeren om bekreftelse, og etter godkjenning kalle skipWaiting() for å umiddelbart aktivere den nye Service Worker. Dette gjør at du kan bruke de siste endringene uten å vente på neste sideoppdatering.
 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 trigges når installasjonen av en ny Service Worker har startet. Når denne hendelsen oppstår, settes en ny worker i registration.installing, så ved å overvåke dens statechange kan du oppdage når installasjonen er fullført (installed). Videre, hvis navigator.serviceWorker.controller finnes, betyr det at en gammel Service Worker allerede kontrollerer siden, så dette er en mulighet til å varsle brukeren om at en ny versjon finnes.
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-hendelsen utløses i det øyeblikket den nye Service Workeren begynner å kontrollere den nåværende siden. Å laste siden på nytt på dette tidspunktet vil umiddelbart ta i bruk de nye cache-strategiene og prosessene. Automatisk omlasting kan imidlertid forringe brukeropplevelsen, så det er bedre å laste siden på nytt etter å ha fått brukerens 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 å la Service Workeren motta en postMessage({ type: 'SKIP_WAITING' }) fra klienten og deretter kalle self.skipWaiting(), kan du fremtvinge en oppdatering.

Områdeerklæring i sw.ts

La oss deretter se på et vanlig Service Worker-eksempel som implementerer app-shell-caching.

Når du bruker Service Workers i TypeScript, er det nyttig å tildele riktig type til self.

1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
  • I TypeScript blir self behandlet som any som standard, så uten ekstra typifisering får du verken autofullføring eller typekontroll for Service Worker-spesifikke API-er som skipWaiting() eller clients.
  • Ved å spesifisere ServiceWorkerGlobalScope får du autoutfylling, unngår feilbruk, og utviklingen blir tryggere og mer atskilt fra vanlige DOM-skript.

Grunnleggende Service Worker (Installer/Aktiver/Hent)

Eksemplet viser enkel versjonshåndtering av cache, forhåndslagring ved installering, sletting av gamle cacher ved aktivering og cachestrategier ved henting (for eksempel cache-first for statiske ressurser, network-first for API-er).

sw.ts (Minimal oppsett + Cache-skjelett)

 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-hendelsen blir appens statisk ressurser (App Shell) forhåndslagret i cache. Ved å kalle self.skipWaiting(), blir den nye Service Worker aktivert umiddelbart, slik at den nyeste cachen er tilgjengelig uten å vente på neste tilgang.
 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-hendelsen slettes gamle versjoner av cacher, og Service Worker holdes oppdatert. Videre, ved å kalle self.clients.claim(), kan den nye Service Worker kontrollere alle klienter uten å vente på at siden lastes på nytt.
 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 avskjære forespørsler og kontrollere responsen. Du kan implementere strategier som cache-first eller network-first, som er nyttige for frakoblet støtte og ytelse.
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 mottas, gjør et kall til self.skipWaiting() at du umiddelbart kan aktivere den ventende Service Workeren. Som resultat vil den nye versjonen brukes fra neste forespørsel uten behov for å laste inn siden på nytt.

Praktisk oversikt over cachestrategier

cache-first

Cache-first sjekker cachen først og returnerer responsen med en gang hvis den finnes. Hvis ikke, henter den fra nettverket og lagrer resultatet i cachen. Dette passer for 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}
  • Koden under viser en cache-first-implementasjon. Hvis det finnes en cache, returneres den; hvis ikke, hentes den fra nettverket og lagres i cachen. Dette er egnet for statiske ressurser som sjelden endres, som bilder eller CSS-filer.

network-first

Network-first forsøker nettverket først og faller tilbake til cachen hvis det mislykkes. Dette passer for API-er hvor ferskhet er viktig.

 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}
  • Koden under viser en network-first-implementasjon. Hvis nettverksrespons mottas, lagres den i cachen; hvis det feiler, returneres den bufrede versjonen. Det egner seg for ressurser hvor ferske data kreves, som nyhetsartikler eller API-responser.

stale-while-revalidate

stale-while-revalidate returnerer først cachen og oppdaterer den samtidig fra nettverket i bakgrunnen. Dette balanserer svartid og datatilgjengelighet.

 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 koden returnerer cachen umiddelbart hvis den finnes, mens nye data hentes fra nettverket i bakgrunnen for å oppdatere cachen. Dette gir raske svar til brukeren og oppdaterer innholdet for neste tilgang, noe som gjør det godt egnet for brukergrensesnitt eller lett datalevering.

Optimalisering av oppdateringsflyten (oppdateringsvarsel og sikker omlasting)

Service Worker-oppdateringer skjer ikke umiddelbart; den nye versjonen forblir ventende til eksisterende faner er lukket.

Her implementerer vi et system for å varsle klienten når den nye versjonen er klar og laste siden trygt på nytt basert på brukerens handling.

Varsle klienten fra Service Worker-siden når den nye versjonen 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 koden kalles notifyClientsUpdated på slutten av activate-hendelsen for å varsle alle tilkoblede klienter om at ny versjon er klar. clients.claim() er en metode som umiddelbart bringer åpne sider (klienter) under kontroll av den nylig aktiverte Service Worker. Normalt vil en Service Worker begynne å kontrollere siden først ved neste lasting, men ved å bruke clients.claim() kan du umiddelbart ta kontroll over siden uten å laste den inn på nytt.

Vis oppdateringsgrensesnitt på klienten, og last inn på nytt etter brukerens handling

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 mottar SW_UPDATED via message-hendelsen og viser et oppdateringsvarsel i brukergrensesnittet. Når brukeren velger å laste inn siden på nytt, kjøres window.location.reload(), og gamle HTML-, CSS- og andre ressurser på siden oppdateres til den nyeste versjonen. Dette sikrer at cache og kontroll med Service Worker, som er byttet med clients.claim(), blir gjeldende på hele siden.

Frakoblet reserve

Forbered /offline.html for kritisk navigasjon, og tilby et minimalt brukergrensesnitt som formidler mening selv uten bilder eller skrifttyper. Hvis et API-kall feiler, vis siste bufrede tilstand om mulig og prøv å hente på nytt i bakgrunnen for en bedre brukeropplevelse.

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});
  • Forhåndslagre /offline.html under install-hendelsen slik at du kan returnere minst en enkel side når nettverk ikke er tilgjengelig.
  • I fetch-hendelsen kan du overvåke navigasjonsforespørsler med request.mode === 'navigate' og spesifikt rette deg mot sideoverganger.
  • Gå tilbake til /offline.html når nettverket feiler, slik at det vises selv når brukeren er offline.

Meldingsutveksling mellom klienten og Service Worker

Siden Service Worker opererer uavhengig av sidelivssyklusen, er toveis kommunikasjon viktig for å varsle om tilstander og utføre kommandoer. Å spesifisere typer for meldinger hjelper til med å forhindre feil sending av meldinger, gjør det mulig med autofullføring av kode, og gjør implementeringen mer robust.

Kodeeksempel

  1. Definisjon av meldingstyper
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 meldingstypen sendt fra Service Worker til klienten.
  • ClientToSw er meldingstypen sendt fra klienten til Service Worker.
  • Dette gjør det mulig å tydeliggjøre hvilke typer hendelser som kan utveksles gjennom toveis kommunikasjon.
  1. Behandling 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 mottar meldinger fra klienten og behandler videre avhengig av meldingstype.
  • For CLEAR_CACHE slettes cachen og alle klienter varsles med CACHE_CLEARED.
  • For PING svarer den til opprinnelig klient med en PING-melding med tidsstempel.
  1. Varsle 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}
  • Bruk clients.matchAll for å hente alle åpne vinduer/faner.
  • Ved å sende postMessage til hver, kan du kringkaste meldinger.
  • Dette kan brukes til oppdateringsvarsler (som SW_UPDATED) og feilmeldinger.
  1. Behandling på klientsiden
1navigator.serviceWorker.controller?.postMessage({
2  type: 'PING',
3  ts: Date.now()
4} as ClientToSw);
  • Ved å sende en PING fra klienten og motta et svar fra Service Worker, kan du bekrefte at toveis kommunikasjon fungerer som den skal. Dette gjør det enklere å teste tilstand for tilkobling og håndtering av meldinger.
 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 の更新やリロード、通知表示などを行います。^}

Fordeler med typet meldingsutveksling

  • Typet meldingsutveksling gjør det tydelig hvilke meldinger som kan sendes og mottas, og autoutfylling og typekontroll øker sikkerheten.
  • postMessage muliggjør en-til-en kommunikasjon, mens broadcast gir en-til-mange kommunikasjon.
  • Du kan enkelt implementere viktige funksjoner som oppdateringsvarsler (SW_UPDATED), cache-håndtering (CACHE_CLEARED) og statuskontroll (PING).

Sammendrag

  • Ved å bruke TypeScript får du typesikkerhet ved både Service Worker API-kall og meldingsutveksling, noe som gir betydelig bedre utviklingseffektivitet og vedlikeholdbarhet.
  • Å forstå livssyklus-hendelsene install, activate og fetch, og å velge riktig cachestrategi (for eksempel cache-first eller network-first) for hver situasjon, gir bedre brukeropplevelse.
  • For drift er det viktig å forstå cache-versjonshåndtering og oppdateringsflyt (updatefound, waiting, SKIP_WAITING m.m.).
  • Ved å bruke typet meldingsutveksling for kommunikasjon mellom klient og Service Worker, kan du forhindre feilimplementering og etablere et system som er enkelt å utvide og vedlikeholde på lang sikt.

Du kan følge med på artikkelen ovenfor ved å bruke Visual Studio Code på vår YouTube-kanal. Vennligst sjekk ut YouTube-kanalen.

YouTube Video