Service Worker in TypeScript
Questo articolo spiega i Service Worker in TypeScript.
Spiegheremo i Service Worker in TypeScript, includendo esempi pratici.
YouTube Video
Service Worker in TypeScript
Un Service Worker è un 'proxy di richiesta' che si trova tra il browser e la rete. Consente l'intercettazione delle richieste, il controllo della cache, il supporto offline e l'elaborazione in background (sincronizzazione e push). L'utilizzo di TypeScript garantisce la sicurezza dei tipi e aumenta la manutenibilità.
Configurazione di TypeScript
tsconfig.json
(Abilita i tipi WebWorker)
Vediamo un esempio di come abilitare il tipo WebWorker 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}
- Aggiungendo
WebWorker
all'arraylib
, puoi usare tipi comeServiceWorkerGlobalScope
. DOM
eWebWorker
hanno tipi diversi, quindi è pratica comune separare le impostazioni ditsconfig.json
per il browser (app principale) e per ilService Worker
.- I file del
Service Worker
vengono infine esportati in un percorso che corrisponde allo scope (di solito la root del sito/sw.js
). - Per motivi di sicurezza, i Service Worker vengono eseguiti solo su HTTPS (o su
localhost
).
Codice di registrazione lato browser
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 );
- Questo processo registra un
Service Worker
. Loscope
si riferisce all’intervallo di percorsi che ilService Worker
può controllare. Ad esempio, se posizioni/sw.js
direttamente nella root e imposti loscope
sulla directory root (/
), puoi controllare tutte le risorse dell’intero sito. Al contrario, se specifichi una directory particolare come/app/
, solo il contenuto di quella directory sarà controllato.
1 // If there's a waiting worker, notify the user.
2 if (registration.waiting) {
3 promptUserToUpdate(registration);
4 }
waiting
indica lo stato in cui un nuovo Service Worker è stato installato e sta aspettando di essere attivato. In questa fase, le pagine esistenti sono ancora controllate dal vecchioService Worker
, quindi è comune chiedere conferma all’utente e, dopo aver ottenuto l’approvazione, chiamareskipWaiting()
per attivare immediatamente il nuovoService Worker
. Questo ti consente di applicare i processi più recenti senza attendere il prossimo ricaricamento della pagina.
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
viene attivato quando inizia l’installazione di un nuovo Service Worker. Quando si verifica questo evento, un nuovo worker viene impostato inregistration.installing
, quindi monitorando il suostatechange
puoi rilevare quando l’installazione è stata completata (installed
). Inoltre, se esistenavigator.serviceWorker.controller
, significa che un vecchio Service Worker sta già controllando la pagina, quindi questa è un'occasione per informare l'utente dell'esistenza di una nuova versione.
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}
- L’evento
controllerchange
viene attivato nel momento in cui il nuovo Service Worker inizia a controllare la pagina attuale. Ricaricando in questo momento, verranno applicate immediatamente le nuove strategie di cache e di elaborazione. Tuttavia, il ricaricamento automatico può peggiorare l’esperienza dell’utente; quindi, è preferibile ricaricare dopo aver ottenuto il consenso dell’utente.
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();
- Facendo ricevere al
Service Worker
unpostMessage({ type: 'SKIP_WAITING' })
dal client e poi chiamandoself.skipWaiting()
, puoi forzare un aggiornamento.
Dichiarazione dello Scope in sw.ts
Successivamente, vediamo un esempio tipico di Service Worker che implementa la cache dell'app shell.
Quando si usano i Service Worker in TypeScript, è utile assegnare il tipo corretto a self
.
1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
- In TypeScript,
self
è trattato comeany
per impostazione predefinita, quindi senza tipizzazione aggiuntiva non otterrai il completamento o il controllo dei tipi per le API specifiche del Service Worker comeskipWaiting()
oclients
. - Specificare
ServiceWorkerGlobalScope
abilita il completamento automatico, previene errori e permette uno sviluppo più sicuro separato dagli script DOM normali.
Service Worker di base (Install/Activate/Fetch)
Dimostra una semplice gestione delle versioni della cache, pre-caching durante l'installazione, eliminazione delle vecchie cache all'attivazione e strategie di cache su fetch (cache-first per risorse statiche, network-first per le API).
sw.ts
(Impostazione minima + Scheletro della cache)
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});
- Durante l'evento
install
, le risorse statiche dell'app (App Shell) vengono pre-memorizzate nella cache. Chiamandoself.skipWaiting()
, il nuovoService Worker
viene attivato immediatamente, rendendo disponibile la cache più recente senza attendere il prossimo accesso.
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});
- Durante l'evento
activate
, le vecchie versioni delle cache vengono eliminate e ilService Worker
viene mantenuto aggiornato. Inoltre, chiamandoself.clients.claim()
, il nuovoService Worker
può controllare tutti i client senza attendere che la pagina venga ricaricata.
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
, puoi intercettare le richieste e controllare la risposta. Puoi implementare strategie come cache-first o network-first, utili per il supporto offline e le prestazioni.
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});
- Se viene ricevuto
SKIP_WAITING
, chiamareself.skipWaiting()
consente di attivare immediatamente il Service Worker in attesa. Di conseguenza, la nuova versione verrà applicata dalla richiesta successiva senza necessità di ricaricare la pagina.
Panoramica delle strategie di cache pratiche
cache-first
Cache-first controlla prima la cache e restituisce immediatamente la risposta se disponibile. In caso contrario, ottiene dalla rete e memorizza il risultato nella cache. Questo è adatto per i file statici.
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}
- Questo codice mostra una implementazione cache-first. Se esiste una cache, la restituisce; altrimenti, preleva dalla rete e lo salva nella cache. È adatto per risorse statiche che cambiano di rado, come immagini o CSS.
network-first
Network-first prova prima la rete e, in caso di errore, ricorre alla cache. Questo è adatto per le API dove la freschezza dei dati è importante.
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}
- Questo codice mostra una implementazione network-first. Se si riceve una risposta dalla rete, questa viene salvata nella cache; in caso contrario, restituisce la versione memorizzata nella cache. È adatto per risorse che richiedono dati aggiornati, come articoli di notizie o risposte API.
stale-while-revalidate
stale-while-revalidate restituisce prima la cache e la aggiorna contemporaneamente dalla rete in background. Questo bilancia velocità di risposta e freschezza.
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}
- Questo codice restituisce immediatamente la cache, se disponibile, mentre recupera i nuovi dati dalla rete in background per aggiornare la cache. Fornisce risposte rapide agli utenti e utilizza contenuti aggiornati per il prossimo accesso, rendendolo adatto a UI o alla consegna di dati leggeri.
Ottimizzare il flusso di aggiornamento (notifica di aggiornamento e ricarica sicura)
Gli aggiornamenti del Service Worker
non sono immediati; la nuova versione rimarrà in attesa finché le schede esistenti non saranno chiuse.
Qui implementiamo un sistema per notificare al client quando la nuova versione è pronta e ricaricare la pagina in sicurezza in base all'azione dell'utente.
Notifica il client dal lato Service Worker
quando la nuova versione è pronta.
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 questo codice,
notifyClientsUpdated
viene chiamata alla fine dell'eventoactivate
per notificare a tutti i client collegati che la nuova versione è pronta.clients.claim()
è un metodo che porta immediatamente le pagine (client) aperte sotto il controllo del nuovo Service Worker attivato. Normalmente, unService Worker
inizia a controllare la pagina solo al caricamento successivo, ma usandoclients.claim()
, puoi portare la pagina sotto il controllo immediatamente senza ricaricare.
Mostrare l'interfaccia di aggiornamento sul client e ricaricare con l'azione dell'utente
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});
- Il client riceve
SW_UPDATED
tramite l'eventomessage
e mostra una notifica di aggiornamento nell'interfaccia utente. Quando l'utente sceglie di ricaricare, viene eseguitowindow.location.reload()
, aggiornando HTML, CSS e altre risorse della pagina alla versione più recente. Questo garantisce che la cache e il controllo da parte delService Worker
, attivati conclients.claim()
, siano applicati a tutta la pagina.
Fallback Offline
Prepara /offline.html
per la navigazione critica e fornisci un'interfaccia minimale che dia significato anche senza immagini o font. Se una chiamata API fallisce, mostra lo stato memorizzato più recente se possibile e prova a ripetere la richiesta in background per migliorare l'esperienza utente.
Esempio di implementazione
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});
- Esegui il precaching di
/offline.html
durante l'eventoinstall
così da poter restituire almeno una pagina minima quando la rete non è disponibile. - Durante l'evento
fetch
, puoi monitorare le richieste di navigazione conrequest.mode === 'navigate'
e prendere di mira specificamente le transizioni di pagina. - Fai un fallback su
/offline.html
quando la rete fallisce, assicurandoti che venga visualizzato anche offline.
Messaggistica tra il client e il Service Worker
.
Poiché il Service Worker
opera indipendentemente dal ciclo di vita della pagina, la messaggistica bidirezionale è importante per notificare stati ed eseguire comandi. Specificare i tipi per i messaggi aiuta a prevenire l'invio errato di messaggi, abilita il completamento del codice e rende l'implementazione più robusta.
Esempio di codice
- Definizione dei tipi di messaggio
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
è il tipo di messaggio inviato dal Service Worker al client.ClientToSw
è il tipo di messaggio inviato dal client al Service Worker.- Questo permette di chiarire i tipi di eventi che possono essere scambiati tramite comunicazione bidirezionale.
- Elaborazione lato Service Worker
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});
- Il Service Worker riceve messaggi dal client e ramifica l'elaborazione in base al tipo.
- Per
CLEAR_CACHE
, elimina la cache e poi notifica tutti i client conCACHE_CLEARED
. - Per
PING
, risponde al client originale con un messaggioPING
contenente un timestamp.
- Notificare tutti i client dal 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}
- Utilizza
clients.matchAll
per ottenere tutte le finestre aperte. - Inviando
postMessage
a ciascuna, puoi trasmettere messaggi in broadcast. - Questo può essere utilizzato per notifiche di aggiornamento (come
SW_UPDATED
) e notifiche di errore.
- Elaborazione lato client
1navigator.serviceWorker.controller?.postMessage({
2 type: 'PING',
3 ts: Date.now()
4} as ClientToSw);
- Inviando un
PING
dal client e ricevendo una risposta dalService Worker
, puoi verificare che la comunicazione bidirezionale funzioni correttamente. Questo rende più semplice testare lo stato della connessione e la gestione dei messaggi.
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 の更新やリロード、通知表示などを行います。^}
Vantaggi della messaggistica tipizzata
- Usare messaggi tipizzati rende chiaro quali messaggi possono essere inviati e ricevuti e il completamento automatico e il controllo dei tipi migliorano la sicurezza.
postMessage
abilita la comunicazione uno-a-uno ebroadcast
abilita la comunicazione uno-a-molti.- Puoi facilmente implementare funzioni essenziali come notifiche di aggiornamento (
SW_UPDATED
), gestione della cache (CACHE_CLEARED
) e controlli di stato (PING
).
Riepilogo
- L'utilizzo di TypeScript aggiunge sicurezza dei tipi alle chiamate delle API e alla messaggistica dei Service Worker, migliorando notevolmente l'efficienza dello sviluppo e la manutenibilità.
- Comprendere gli eventi di ciclo di vita
install
,activate
efetch
e scegliere la strategia di caching più adatta (come cache-first o network-first) per ogni situazione porta a una migliore esperienza utente. - Per le operazioni, è essenziale comprendere la gestione delle versioni della cache e i flussi di aggiornamento (
updatefound
,waiting
,SKIP_WAITING
, ecc.). - Adottando una messaggistica tipizzata per la comunicazione tra client e
Service Worker
, puoi evitare errori di implementazione e creare un sistema facile da estendere e mantenere a lungo termine.
Puoi seguire l'articolo sopra utilizzando Visual Studio Code sul nostro canale YouTube. Controlla anche il nostro canale YouTube.