Service Worker i TypeScript

Service Worker i TypeScript

Denna artikel förklarar Service Workers i TypeScript.

Vi kommer att förklara Service Workers i TypeScript, inklusive praktiska exempel.

YouTube Video

Service Worker i TypeScript

En Service Worker är en 'begäran-proxy' som ligger mellan webbläsaren och nätverket. Den möjliggör fångst av hämtningar, cachekontroll, offline-stöd och bakgrundsbehandling (synkronisering och push). Att använda TypeScript ger typsäkerhet och ökar underhållbarheten.

Konfigurera TypeScript

tsconfig.json (Aktivera WebWorker-typer)

Låt oss titta på ett exempel på hur man aktiverar WebWorker-typ 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}
  • Genom att lägga till WebWorker till lib-arrayen kan du använda typer som ServiceWorkerGlobalScope.
  • DOM och WebWorker har olika typer, så det är vanligt att separera inställningarna av tsconfig.json för webbläsaren (huvudapplikationen) och Service Worker.
  • Service Worker-filerna hamnar slutligen på en sökväg som matchar scope (vanligtvis webbplatsens rot, /sw.js).
  • Av säkerhetsskäl körs Service Workers endast över HTTPS (eller på localhost).

Registreringskod på webbläsarsidan

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    );
  • Den här processen registrerar en Service Worker. scope syftar på de sökvägar som Service Worker kan kontrollera. Till exempel, om du placerar /sw.js direkt under roten och sätter scope till rotkatalogen (/), kan du kontrollera alla resurser på hela webbplatsen. Å andra sidan, om du anger en särskild katalog, till exempel /app/, kommer endast innehållet under den katalogen att kontrolleras.
1    // If there's a waiting worker, notify the user.
2    if (registration.waiting) {
3      promptUserToUpdate(registration);
4    }
  • waiting anger det tillstånd där en ny Service Worker har installerats och väntar på att aktiveras. I det här skedet kontrolleras befintliga sidor fortfarande av den gamla Service Worker, så det är vanligt att be användaren om bekräftelse och därefter anropa skipWaiting() för att genast aktivera den nya Service Worker. Detta gör att du kan återspegla de senaste processerna utan att vänta på nästa omladdning av sidan.
 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 utlöses när installationen av en ny Service Worker har påbörjats. När denna händelse äger rum sätts en ny worker i registration.installing, så genom att lyssna på dess statechange kan du upptäcka när installationen har slutförts (installed). Vidare, om navigator.serviceWorker.controller finns, betyder det att en äldre Service Worker redan kontrollerar sidan, så det här är ett tillfälle att meddela användaren om att det finns 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}
  • Händelsen controllerchange utlöses i det ögonblick den nya Service Worker börjar kontrollera den aktuella sidan. Om sidan laddas om vid denna tidpunkt kommer nya cache-strategier och processer att tillämpas omedelbart. Automatisk omladdning kan dock försämra användarupplevelsen, så det är att föredra att ladda om först efter att ha fått användarens godkännande.
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();
  • Genom att låta Service Worker ta emot ett postMessage({ type: 'SKIP_WAITING' }) från klienten och sedan anropa self.skipWaiting(), kan du påskynda en uppdatering.

Scope-deklaration i sw.ts

Låt oss nu titta på ett typiskt exempel på Service Worker som implementerar app shell-cachelagring.

När du använder Service Workers i TypeScript är det användbart att tilldela rätt typ till self.

1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
  • I TypeScript behandlas self som any som standard, så utan extra typning får du varken typkomplettering eller typkontroll för Service Worker-specifika API:er som skipWaiting() eller clients.
  • Genom att ange ServiceWorkerGlobalScope möjliggörs autokomplettering, förhindrar felanvändning och ger en säkrare utveckling separerad från vanliga DOM-skript.

Grundläggande Service Worker (Install/Activate/Fetch)

Det visar enkel cacheversionshantering, förcachelagring vid installation, borttagning av gamla cacheminnen vid aktivering och cache-strategier vid hämtning (cache-first för statiska resurser, network-first för API:er).

sw.ts (Minimal uppsättning + 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});
  • Under install-händelsen blir appens statisk resurser (App Shell) förlagrade i cache. Genom att anropa self.skipWaiting() aktiveras den nya Service Worker omedelbart, vilket gör den senaste cachen tillgänglig utan att behöva vänta till nästa åtkomst.
 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});
  • Vid activate-händelsen raderas gamla versioner av cacher, och Service Worker hålls uppdaterad. Dessutom kan nya Service Worker genom att anropa self.clients.claim() styra alla klienter utan att behöva vänta på att sidan ska laddas om.
 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 fånga upp begäranden och kontrollera svaret. Du kan implementera strategier som cache-first eller network-first, vilket är användbart för offlinestöd och prestanda.
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});
  • Om SKIP_WAITING tas emot tillåter anropet av self.skipWaiting() att du omedelbart aktiverar den väntande Service Worker. Som ett resultat kommer den nya versionen att tillämpas från nästa begäran utan behov av att ladda om sidan.

Översikt över praktiska cache-strategier

cache-first

Cache-first kontrollerar cacheminnet först och returnerar svaret omedelbart om det finns. Om inte, hämtar den från nätverket och cachelagrar resultatet. Detta passar statiska 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}
  • Denna kod visar en cache-first-implementering. Om det finns i cacheminnet returneras det; om inte, hämtas det från nätverket och sparas i cacheminnet. Det är lämpligt för statiska resurser som sällan ändras, såsom bilder eller CSS.

network-first

Network-first försöker med nätverket först och återgår till cacheminnet vid fel. Detta är lämpligt för API:er där färskhet är viktigt.

 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}
  • Denna kod visar en network-first-implementering. Om ett nätverkssvar tas emot sparas det i cacheminnet; om det misslyckas returneras den cachelagrade versionen. Det passar för resurser som kräver färsk data, som nyhetsartiklar eller API-svar.

stale-while-revalidate

stale-while-revalidate returnerar först cachead data och uppdaterar den samtidigt i bakgrunden från nätverket. Detta balanserar svarshastighet och färskhet.

 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}
  • Denna kod returnerar cacheminnet omedelbart om det finns, samtidigt som den hämtar ny data från nätverket i bakgrunden för att uppdatera cacheminnet. Det ger snabba svar till användare och använder uppdaterat innehåll vid nästa åtkomst, vilket gör det lämpligt för användargränssnitt eller lätt dataleverans.

Optimera uppdateringsflödet (uppdateringsavisering och säker omladdning)

Service Worker-uppdateringar sker inte omedelbart; den nya versionen förblir väntande tills befintliga flikar stängs.

Här implementerar vi ett system för att meddela klienten när den nya versionen är redo och ladda om sidan säkert baserat på användarens åtgärd.

Meddela klienten från Service Worker-sidan när den nya versionen är redo.

 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 denna kod anropas notifyClientsUpdated i slutet av activate-händelsen för att meddela alla anslutna klienter att den nya versionen är redo. clients.claim() är en metod som omedelbart placerar öppna sidor (klienter) under kontroll av den nyaktiverade Service Worker. Normalt börjar en Service Worker styra sidan först vid nästa omladdning, men med clients.claim() kan du ta kontroll över sidan omedelbart utan omladdning.

Visa uppdateringsgränssnitt på klienten och ladda om vid användaråtgärd

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 tar emot SW_UPDATED via message-händelsen och visar ett uppdateringsmeddelande i användargränssnittet. När användaren väljer att ladda om sidan körs window.location.reload(), vilket uppdaterar gammal HTML, CSS och andra resurser på sidan till den senaste versionen. Detta säkerställer att cachen och kontrollen från Service Worker med clients.claim() återspeglas på hela sidan.

Offline-backup

Förbered /offline.html för kritisk navigering och tillhandahåll ett minimalt gränssnitt som förmedlar mening även utan bilder eller typsnitt. Om ett API-anrop misslyckas, visa det senast cachelagrade tillståndet om möjligt och försök att hämta igen i bakgrunden för att förbättra användarupplevelsen.

Implementeringsexempel

 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});
  • Förcachelagra /offline.html under install-händelsen så att du kan återge åtminstone en minimal sida när nätverket är otillgängligt.
  • Under fetch-händelsen kan du observera navigeringsförfrågningar med request.mode === 'navigate' och rikta in dig specifikt på sidövergångar.
  • Falla tillbaka på /offline.html när nätverket misslyckas, vilket säkerställer att den visas även offline.

Meddelandeutbyte mellan klienten och Service Worker

Eftersom Service Worker fungerar oberoende av sidans livscykel är bidirektionell meddelandehantering viktig för att meddela tillstånd och utföra kommandon. Genom att ange typer för meddelanden undviks felaktig meddelandehantering, möjliggör kodkomplettering och gör din implementering mer robust.

Kodexempel

  1. Definition av meddelandetyper
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 är typen av meddelande som skickas från Service Worker till klient.
  • ClientToSw är typen av meddelande som skickas från klient till Service Worker.
  • Detta gör det möjligt att förtydliga vilken typ av händelser som kan utbytas genom tvåvägskommunikation.
  1. Behandling på Service Worker-sidan
 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 Workern tar emot meddelanden från klienten och förgrenar behandlingen baserat på typ.
  • För CLEAR_CACHE raderas cacheminnet och alla klienter informeras med CACHE_CLEARED.
  • För PING svarar den ursprungliga klienten med ett PING-meddelande inklusive tidsstämpel.
  1. Meddela alla klienter från 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}
  • Använd clients.matchAll för att hämta alla fönsterflikar.
  • Genom att skicka postMessage till varje kan du sända ut meddelanden.
  • Detta kan användas för uppdateringsaviseringar (som SW_UPDATED) och felmeddelanden.
  1. Behandling på klientsidan
1navigator.serviceWorker.controller?.postMessage({
2  type: 'PING',
3  ts: Date.now()
4} as ClientToSw);
  • Genom att skicka en PING från klienten och ta emot ett svar från Service Worker kan du bekräfta att den tvåvägskommunikationen fungerar korrekt. Detta gör det enklare att testa anslutningstillstånd och meddelandehantering.
 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 の更新やリロード、通知表示などを行います。^}

Fördelar med typade meddelanden

  • Genom att använda typade meddelanden blir det tydligt vilka meddelanden som kan skickas och tas emot, och autokomplettering samt typkontroll förbättrar säkerheten.
  • postMessage möjliggör en-till-en kommunikation och broadcast möjliggör en-till-många kommunikation.
  • Du kan enkelt implementera viktiga funktioner som uppdateringsaviseringar (SW_UPDATED), cachehantering (CACHE_CLEARED) och hälsokontroller (PING).

Sammanfattning

  • Att använda TypeScript lägger till typsäkerhet till Service Worker API-anrop och meddelanden, vilket kraftigt förbättrar utvecklingseffektiviteten och underhållbarheten.
  • Att förstå livscykelhändelserna install, activate och fetch, samt att välja rätt cachestrategi (såsom cache-first eller network-first) för varje situation leder till en bättre användarupplevelse.
  • För drift är det viktigt att förstå cacheversionshantering och uppdateringsflöden (updatefound, waiting, SKIP_WAITING, etc.).
  • Genom att använda typade meddelanden för kommunikationen mellan klient och Service Worker kan du undvika felimplementationer och skapa ett system som är lätt att bygga ut och underhålla på lång sikt.

Du kan följa med i artikeln ovan med hjälp av Visual Studio Code på vår YouTube-kanal. Vänligen kolla även in YouTube-kanalen.

YouTube Video