Service Worker in TypeScript

Service Worker in TypeScript

Dit artikel legt Service Workers uit in TypeScript.

We leggen Service Workers in TypeScript uit, inclusief praktische voorbeelden.

YouTube Video

Service Worker in TypeScript

Een Service Worker is een 'verzoek-proxy' die zich tussen de browser en het netwerk bevindt. Het maakt het onderscheppen van fetch, cachebeheer, offline-ondersteuning en achtergrondverwerking (sync en push) mogelijk. Het gebruik van TypeScript biedt typeveiligheid en verhoogt het onderhoudsgemak.

TypeScript instellen

tsconfig.json (WebWorker-types inschakelen)

Laten we een voorbeeld bekijken van het inschakelen van het WebWorker-type in 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}
  • Door WebWorker toe te voegen aan de lib-array kun je types als ServiceWorkerGlobalScope gebruiken.
  • DOM en WebWorker hebben verschillende typen, daarom is het gebruikelijk om de tsconfig.json-instellingen voor de browser (hoofapp) en de Service Worker te scheiden.
  • Service Worker-bestanden worden uiteindelijk opgeslagen op een pad dat overeenkomt met de scope (meestal de site-root /sw.js).
  • Om veiligheidsredenen werken Service Workers alleen via HTTPS (of op localhost).

Registratiecode aan de browserzijde

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    );
  • Dit proces registreert een Service Worker. scope verwijst naar het bereik van paden die de Service Worker kan beheren. Als je bijvoorbeeld /sw.js direct onder de root plaatst en de scope instelt op de rootdirectory (/), kun je alle bronnen van de volledige site beheren. Aan de andere kant, als je een specifieke map zoals /app/ opgeeft, worden alleen de inhoud onder die map beheerd.
1    // If there's a waiting worker, notify the user.
2    if (registration.waiting) {
3      promptUserToUpdate(registration);
4    }
  • waiting geeft de status aan waarin een nieuwe Service Worker is geïnstalleerd en wacht op activatie. In deze fase worden bestaande pagina's nog steeds beheerd door de oude Service Worker, dus is het gebruikelijk om de gebruiker om bevestiging te vragen en na goedkeuring skipWaiting() aan te roepen om de nieuwe Service Worker direct te activeren. Hierdoor kun je de nieuwste processen toepassen zonder te wachten op de volgende paginavernieuwing.
 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 wordt geactiveerd wanneer de installatie van een nieuwe Service Worker is gestart. Wanneer dit evenement zich voordoet, wordt een nieuwe worker ingesteld in registration.installing. Door de statechange hiervan te volgen, kun je detecteren wanneer de installatie is voltooid (installed). Als bovendien navigator.serviceWorker.controller bestaat, betekent dit dat een oude Service Worker de pagina al controleert, dus dit is een kans om de gebruiker te informeren over het bestaan van een nieuwe versie.
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}
  • Het controllerchange-evenement wordt geactiveerd op het moment dat de nieuwe Service Worker de huidige pagina begint te beheren. Opnieuw laden op dit moment zal onmiddellijk de nieuwe cache-strategieën en verwerking toepassen. Automatisch herladen kan echter de gebruikerservaring verslechteren, dus het is beter om te herladen na het verkrijgen van toestemming van de gebruiker.
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();
  • Door de Service Worker een postMessage({ type: 'SKIP_WAITING' }) van de client te laten ontvangen en vervolgens self.skipWaiting() aan te roepen, kun je een update activeren.

Scope-verklaring in sw.ts

Laten we vervolgens een typisch Service Worker-voorbeeld bekijken dat app shell caching implementeert.

Bij het gebruik van Service Workers in TypeScript is het handig om het juiste type aan self toe te wijzen.

1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
  • In TypeScript wordt self standaard als any behandeld, dus zonder extra typing krijg je geen type-aanvulling of type-controle voor Service Worker-specifieke API's zoals skipWaiting() of clients.
  • Door ServiceWorkerGlobalScope te specificeren wordt autocompletion ingeschakeld, wordt verkeerd gebruik voorkomen en kan er veiliger worden ontwikkeld, gescheiden van reguliere DOM-scripts.

Basis Service Worker (Installeren/Activeren/Fetch)

Het demonstreert eenvoudig cacheversiebeheer, precaching bij installatie, oude caches verwijderen bij activeren en cachestrategieën bij fetch (cache-first voor statische middelen, network-first voor API's).

sw.ts (Minimale setup + cache-skelet)

 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});
  • Tijdens het install-event worden de statische bronnen (App Shell) van de app vooraf in de cache geplaatst. Door self.skipWaiting() aan te roepen, wordt de nieuwe Service Worker direct geactiveerd, waardoor de nieuwste cache beschikbaar is zonder te wachten op de volgende toegang.
 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});
  • Tijdens het activate-event worden oude versies van caches verwijderd en wordt de Service Worker up-to-date gehouden. Bovendien kan door het aanroepen van self.clients.claim(), de nieuwe Service Worker alle clients direct overnemen zonder te hoeven wachten op het herladen van de pagina.
 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});
  • In fetch kun je verzoeken onderscheppen en de respons beheren. Je kunt strategieën zoals cache-first of network-first toepassen, die nuttig zijn voor offline-ondersteuning en prestaties.
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});
  • Als SKIP_WAITING wordt ontvangen, kun je door het aanroepen van self.skipWaiting() de wachtende Service Worker direct activeren. Daardoor wordt de nieuwe versie toegepast vanaf het volgende verzoek zonder dat je de pagina hoeft te herladen.

Overzicht van praktische cache-strategieën

cache-first

Cache-first controleert eerst de cache en geeft direct antwoord als er iets beschikbaar is. Zo niet, dan wordt er iets van het netwerk gehaald en wordt het resultaat gecachet. Dit is geschikt voor statische bestanden.

 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}
  • Deze code demonstreert een cache-first-implementatie. Als er een cache is, wordt die teruggegeven; zo niet, dan wordt het van het netwerk opgehaald en opgeslagen in de cache. Het is geschikt voor statische bronnen die zelden veranderen, zoals afbeeldingen of CSS.

network-first

Network-first probeert eerst het netwerk en valt terug op de cache als dat faalt. Dit is geschikt voor API's waarbij actualiteit belangrijk is.

 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}
  • Deze code toont een network-first-implementatie. Als er een netwerkrespons is, wordt deze opgeslagen in de cache; als dat mislukt, wordt de gecachete versie teruggegeven. Het is geschikt voor bronnen die actuele gegevens vereisen, zoals nieuwsartikelen of API-responses.

stale-while-revalidate

stale-while-revalidate levert eerst de cache op en werkt deze gelijktijdig bij vanuit het netwerk op de achtergrond. Dit balanceert reactiesnelheid en versheid.

 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}
  • Deze code geeft de cache direct terug als die aanwezig is, terwijl er op de achtergrond nieuwe data van het netwerk opgehaald wordt om de cache te bijwerken. Het biedt snelle reacties aan gebruikers en gebruikt bijgewerkte inhoud bij het volgende bezoek, waardoor het geschikt is voor de gebruikersinterface of lichte datalevering.

Het optimaliseren van het updateproces (update-melding en veilig herladen)

Service Worker-updates zijn niet direct; de nieuwe versie blijft wachten totdat bestaande tabbladen zijn gesloten.

Hier implementeren we een systeem om de client te melden wanneer de nieuwe versie klaar is en de pagina veilig te herladen op basis van een gebruikersactie.

Meld de client vanaf de Service Worker-zijde wanneer de nieuwe versie klaar is.

 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 deze code wordt notifyClientsUpdated aan het einde van het activate-event aangeroepen om alle verbonden clients te informeren dat de nieuwe versie klaar is. clients.claim() is een methode die de momenteel geopende pagina's (clients) onmiddellijk onder controle brengt van de nieuw geactiveerde Service Worker. Normaal gesproken begint een Service Worker de pagina pas bij de volgende laadbeurt te beheren, maar met clients.claim() kun je de pagina onmiddellijk onder controle brengen zonder te herladen.

Toon een update-UI aan de client en herlaad op gebruikersactie

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});
  • De client ontvangt SW_UPDATED via het message-event en toont een update-melding in de gebruikersinterface. Wanneer de gebruiker ervoor kiest om te herladen, wordt window.location.reload() uitgevoerd, waarbij oude HTML, CSS en andere bronnen op de pagina bijgewerkt worden naar de nieuwste versie. Dit zorgt ervoor dat de cache en de controle door de Service Worker, overgenomen met clients.claim(), over de gehele pagina effect hebben.

Offline-fallback

Voorzie in /offline.html voor kritieke navigatie en bied een minimale UI die zelfs zonder afbeeldingen of lettertypen betekenis overbrengt. Als een API-aanroep mislukt, toon dan indien mogelijk de laatste gecachete status en probeer op de achtergrond opnieuw op te halen om de gebruikerservaring te verbeteren.

Voorbeeld van implementatie

 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});
  • Precach /offline.html tijdens het install-event zodat je ten minste een minimale pagina kunt teruggeven wanneer het netwerk niet beschikbaar is.
  • Tijdens het fetch-event kun je navigatieverzoeken monitoren met request.mode === 'navigate' en specifiek paginawisselingen targeten.
  • Val terug op /offline.html wanneer het netwerk faalt, zodat deze ook offline wordt getoond.

Berichtenuitwisseling tussen de client en de Service Worker

Omdat de Service Worker onafhankelijk van de paginacyclus opereert, is tweerichtingscommunicatie belangrijk voor het melden van statussen en het uitvoeren van opdrachten. Het specificeren van typen voor berichten helpt verkeerde berichtverzending te voorkomen, maakt code-completion mogelijk en maakt je implementatie robuuster.

Codevoorbeeld

  1. Definitie van berichttypen
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 is het type bericht dat van de Service Worker naar de client wordt gestuurd.
  • ClientToSw is het type bericht dat van de client naar de Service Worker wordt gestuurd.
  • Hiermee kun je de soorten gebeurtenissen verduidelijken die via bidirectionele communicatie kunnen worden uitgewisseld.
  1. Verwerking aan de Service Worker-zijde
 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});
  • De Service Worker ontvangt berichten van de client en bepaalt de verwerking op basis van het type.
  • Bij CLEAR_CACHE wordt de cache verwijderd en worden alle clients op de hoogte gebracht met CACHE_CLEARED.
  • Bij PING antwoordt hij de oorspronkelijke client met een PING-bericht inclusief tijdstempel.
  1. Alle clients vanuit de Service Worker informeren
1async function broadcast(msg: SwToClient) {
2  const clients = await self.clients.matchAll({ includeUncontrolled: true });
3  for (const c of clients) c.postMessage(msg);
4}
  • Gebruik clients.matchAll om alle venstertabbladen op te halen.
  • Door postMessage naar elk te sturen, kun je berichten uitzenden.
  • Dit kan gebruikt worden voor update-meldingen (zoals SW_UPDATED) en foutmeldingen.
  1. Verwerking aan de clientzijde
1navigator.serviceWorker.controller?.postMessage({
2  type: 'PING',
3  ts: Date.now()
4} as ClientToSw);
  • Door een PING vanuit de client te sturen en een antwoord van de Service Worker te ontvangen, kun je verifiëren dat de tweerichtingscommunicatie goed werkt. Dit maakt het eenvoudiger om verbindingsstatussen en de afhandeling van berichten te 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 の更新やリロード、通知表示などを行います。^}

Voordelen van getypte berichtgeving

  • Door getypte berichten is duidelijk welke berichten verzonden en ontvangen kunnen worden, en autocompletion en typecontrole verhogen de veiligheid.
  • postMessage maakt een-op-een communicatie mogelijk en broadcast maakt een-op-veel communicatie mogelijk.
  • Je kunt eenvoudig essentiële functies implementeren zoals update-meldingen (SW_UPDATED), cachebeheer (CACHE_CLEARED) en health checks (PING).

Samenvatting

  • Het gebruik van TypeScript voegt typeveiligheid toe aan Service Worker API-aanroepen en berichtverkeer, wat de efficiëntie en het onderhoudsgemak aanzienlijk verbetert.
  • Begrijpen van de install, activate en fetch-levenscyclusgebeurtenissen, en het kiezen van de juiste cachestrategie (zoals cache-first of network-first) voor elke situatie leidt tot een betere gebruikerservaring.
  • Voor operaties is begrip van cacheversiebeheer en update-flow (updatefound, waiting, SKIP_WAITING, enz.) essentieel.
  • Door gebruik te maken van getypeerde berichten voor communicatie tussen client en Service Worker, kun je verkeerde implementaties voorkomen en een systeem opzetten dat op de lange termijn eenvoudig uit te breiden en te onderhouden is.

Je kunt het bovenstaande artikel volgen met Visual Studio Code op ons YouTube-kanaal. Bekijk ook het YouTube-kanaal.

YouTube Video