Service Worker en TypeScript
Cet article explique les Service Workers en TypeScript.
Nous allons expliquer les Service Workers en TypeScript, avec des exemples concrets.
YouTube Video
Service Worker en TypeScript
Un Service Worker agit comme un « proxy de requêtes » qui se situe entre le navigateur et le réseau. Il permet l'interception des requêtes, le contrôle du cache, le support hors ligne et le traitement en arrière-plan (synchronisation et push). L'utilisation de TypeScript garantit la sécurité des types et améliore la maintenabilité.
Configuration de TypeScript
tsconfig.json
(Activer les types WebWorker)
Voyons un exemple d’activation du type WebWorker dans 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}
- En ajoutant
WebWorker
au tableaulib
, vous pouvez utiliser des types commeServiceWorkerGlobalScope
. - Le
DOM
et leWebWorker
ont des types différents, il est donc courant de séparer les paramètrestsconfig.json
pour le navigateur (application principale) et leService Worker
. - Les fichiers du
Service Worker
sont finalement générés dans un chemin correspondant au scope (généralement à la racine du site/sw.js
). - Pour des raisons de sécurité, les Service Workers ne fonctionnent que sur HTTPS (ou sur
localhost
).
Code d’enregistrement côté navigateur
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 );
- Ce processus enregistre un
Service Worker
. Lescope
fait référence à l'ensemble des chemins que leService Worker
peut contrôler. Par exemple, si vous placez/sw.js
à la racine et définissez lescope
au dossier racine (/
), vous pouvez contrôler toutes les ressources du site entier. En revanche, si vous spécifiez un répertoire particulier comme/app/
, seuls les contenus de ce répertoire seront contrôlés.
1 // If there's a waiting worker, notify the user.
2 if (registration.waiting) {
3 promptUserToUpdate(registration);
4 }
waiting
indique l'état où un nouveau Service Worker a été installé et attend d'être activé. À ce stade, les pages existantes sont encore contrôlées par l'ancienService Worker
, il est donc courant de demander la confirmation à l'utilisateur puis d'appelerskipWaiting()
après avoir obtenu son accord afin d'activer immédiatement le nouveauService Worker
. Cela permet de refléter les derniers changements sans attendre le prochain rechargement de la page.
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
est déclenché lorsque l'installation d'un nouveau Service Worker commence. Lorsque cet événement se produit, un nouveau worker est défini dansregistration.installing
, ainsi, en surveillant sonstatechange
, vous pouvez détecter quand l'installation est terminée (installed
). De plus, sinavigator.serviceWorker.controller
existe, cela signifie qu’un ancien Service Worker contrôle déjà la page. C'est donc l'occasion d'informer l'utilisateur qu'une nouvelle version est disponible.
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'événement
controllerchange
est déclenché au moment où le nouveau Service Worker commence à contrôler la page actuelle. Recharger à ce stade permettra d'appliquer immédiatement les nouvelles stratégies de cache et de traitement. Cependant, le rechargement automatique peut dégrader l'expérience utilisateur, il est donc préférable de recharger après avoir obtenu le consentement de l'utilisateur.
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();
- En faisant recevoir au
Service Worker
unpostMessage({ type: 'SKIP_WAITING' })
du client puis en appelantself.skipWaiting()
, vous pouvez déclencher une mise à jour.
Déclaration du scope dans sw.ts
Voyons maintenant un exemple typique de Service Worker qui met en œuvre la mise en cache du shell de l’application.
Lorsque vous utilisez les Service Workers en TypeScript, il est utile d’assigner le bon type à self
.
1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
- En TypeScript,
self
est de typeany
par défaut ; sans typage supplémentaire, vous ne bénéficierez ni de la complétion de type ni de la vérification de type pour les API spécifiques au Service Worker commeskipWaiting()
ouclients
. - Spécifier
ServiceWorkerGlobalScope
permet l’autocomplétion, évite les erreurs, et permet un développement plus sûr, séparé des scripts DOM classiques.
Service Worker de base (Install/Activate/Fetch)
Il présente une gestion simple des versions du cache, le pré-cache à l’installation, la suppression des anciens caches à l’activation, et des stratégies de cache lors des fetch (cache-first pour les fichiers statiques, network-first pour les APIs).
sw.ts
(Configuration minimale + squelette de 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});
- Lors de l’événement
install
, les ressources statiques (App Shell) de l’application sont pré-mises en cache. En appelantself.skipWaiting()
, le nouveauService Worker
est activé immédiatement, rendant ainsi le dernier cache disponible sans attendre le prochain accès.
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});
- Lors de l’événement
activate
, les anciennes versions des caches sont supprimées et leService Worker
est maintenu à jour. De plus, en appelantself.clients.claim()
, le nouveauService Worker
peut contrôler tous les clients sans attendre que la page soit rechargée.
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});
- Dans
fetch
, vous pouvez intercepter les requêtes et contrôler la réponse. Vous pouvez mettre en œuvre des stratégies comme cache-first ou network-first, utiles pour le support hors-ligne et la performance.
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});
- Si
SKIP_WAITING
est reçu, l'appel àself.skipWaiting()
permet d'activer instantanément le Service Worker en attente. Par conséquent, la nouvelle version sera appliquée à partir de la prochaine requête, sans avoir besoin de recharger la page.
Résumé des stratégies de cache pratiques
cache-first
Cache-first vérifie d’abord le cache et fournit la réponse immédiatement si elle est disponible. Sinon, il récupère depuis le réseau et met le résultat en cache. Ceci est adapté aux fichiers statiques.
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}
- Ce code montre une implémentation cache-first. S’il existe une entrée en cache, elle est retournée ; sinon, la ressource est récupérée sur le réseau et stockée dans le cache. C’est adapté aux ressources statiques qui changent rarement, comme les images ou les fichiers CSS.
network-first
Network-first tente d’abord le réseau, puis se replie sur le cache en cas d’échec. C’est approprié pour les APIs où la fraîcheur des données est 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}
- Ce code montre une implémentation network-first. Si une réponse réseau est reçue, elle est enregistrée dans le cache ; sinon, la version en cache est retournée. C’est adapté aux ressources nécessitant des données fraîches, comme des articles d’actualité ou des réponses d’API.
stale-while-revalidate
stale-while-revalidate renvoie d'abord le cache puis le met à jour en arrière-plan à partir du réseau en même temps. Cela équilibre la rapidité de réponse et la fraîcheur des données.
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}
- Ce code retourne immédiatement le cache si disponible, et récupère en arrière-plan de nouvelles données sur le réseau pour mettre à jour le cache. Cela offre des réponses rapides aux utilisateurs et du contenu mis à jour au prochain accès, ce qui le rend adapté à l’UI ou à la diffusion de données légères.
Optimiser la mise à jour (notification et rechargement sécurisé)
Les mises à jour du Service Worker
ne sont pas immédiates ; la nouvelle version reste en attente jusqu'à la fermeture des onglets existants.
Ici, nous mettons en place un système pour avertir le client quand la nouvelle version est prête, puis recharger la page de façon sécurisée selon l’action de l’utilisateur.
Notifiez le client depuis le Service Worker
lorsqu'une nouvelle version est prête.
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});
- Dans ce code,
notifyClientsUpdated
est appelé à la fin de l’événementactivate
pour notifier tous les clients connectés que la nouvelle version est prête.clients.claim()
est une méthode qui place immédiatement les pages ouvertes (clients) sous le contrôle du Service Worker nouvellement activé. Normalement, unService Worker
ne commence à contrôler la page qu'au prochain chargement, mais grâce àclients.claim()
, la page passe immédiatement sous contrôle sans rechargement.
Afficher une interface de mise à jour côté client, puis recharger après action de l’utilisateur
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});
- Le client reçoit
SW_UPDATED
via l’événementmessage
et affiche une notification de mise à jour dans l’interface. Lorsque l’utilisateur choisit de recharger,window.location.reload()
est exécuté, mettant à jour l’ancien HTML, CSS et les autres ressources de la page vers la version la plus récente. Cela garantit que le cache et la prise de contrôle duService Worker
viaclients.claim()
sont reflétés sur l’ensemble de la page.
Secours hors-ligne (Offline Fallback)
Préparez /offline.html
pour la navigation critique, et fournissez une interface minimale qui reste compréhensible même sans image ou police. Si un appel API échoue, montrez le dernier état en cache si possible et essayez de le rafraîchir en arrière-plan pour améliorer l’expérience utilisateur.
Exemple d’implémentation
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});
- Pré-cachez
/offline.html
lors de l’événementinstall
afin de pouvoir fournir au minimum une page basique en cas d’indisponibilité réseau. - Lors de l’événement
fetch
, vous pouvez surveiller les requêtes de navigation avecrequest.mode === 'navigate'
et cibler spécifiquement les transitions de page. - Basculer vers
/offline.html
si le réseau échoue, garantissant l’affichage même hors ligne.
La communication entre le client et le Service Worker
Comme le Service Worker
fonctionne indépendamment du cycle de vie de la page, une messagerie bidirectionnelle est importante pour notifier les états et exécuter des commandes. Spécifier des types pour les messages aide à éviter les envois incorrects, permet la complétion du code et rend votre implémentation plus robuste.
Exemple de code
- Définition des types de messages
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
désigne le type des messages envoyés du Service Worker vers le client.ClientToSw
est le type des messages envoyés du client vers le Service Worker.- Cela vous permet de clarifier les types d'événements pouvant être échangés via une communication bidirectionnelle.
- Traitement côté 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});
- Le Service Worker reçoit les messages du client et effectue un traitement différent selon leur type.
- Pour
CLEAR_CACHE
, il supprime le cache puis avertit tous les clients avecCACHE_CLEARED
. - Pour
PING
, il répond au client initial avec un messagePING
incluant un timestamp.
- Notifier tous les clients depuis le 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}
- Utilisez
clients.matchAll
pour récupérer tous les onglets ouverts. - En envoyant
postMessage
à chacun, vous pouvez diffuser un message. - Cela peut servir à notifier les mises à jour (ex :
SW_UPDATED
) ou les erreurs.
- Traitement côté client
1navigator.serviceWorker.controller?.postMessage({
2 type: 'PING',
3 ts: Date.now()
4} as ClientToSw);
- En envoyant un
PING
depuis le client et en recevant une réponse duService Worker
, vous pouvez vérifier que la communication bidirectionnelle fonctionne correctement. Cela facilite le test des états de connexion et la gestion des messages.
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 の更新やリロード、通知表示などを行います。^}
Avantages de la messagerie typée
- En utilisant des messages typés, il est clair quels messages peuvent être envoyés/reçus, et l’autocomplétion ainsi que la vérification des types augmentent la sécurité.
postMessage
permet une communication un-à-un, etbroadcast
une communication un-à-plusieurs.- Vous pouvez facilement mettre en œuvre des fonctions essentielles comme les notifications de mise à jour (
SW_UPDATED
), la gestion du cache (CACHE_CLEARED
), ou la vérification d’état (PING
).
Résumé
- L’utilisation de TypeScript apporte la sécurité des types aux appels API et à la messagerie des Service Workers, améliorant grandement l’efficacité et la maintenabilité du développement.
- Comprendre les événements du cycle de vie
install
,activate
etfetch
, et choisir la bonne stratégie de cache (cache-first, network-first, etc.) selon la situation permet d’offrir une meilleure expérience utilisateur. - Pour l’exploitation, comprendre la gestion des versions de cache et les flux de mise à jour (
updatefound
,waiting
,SKIP_WAITING
, etc.) est essentiel. - En adoptant des messages typés pour la communication entre le client et le
Service Worker
, vous pouvez prévenir les erreurs d'implémentation et établir un système facile à étendre et à maintenir à long terme.
Vous pouvez suivre l'article ci-dessus avec Visual Studio Code sur notre chaîne YouTube. Veuillez également consulter la chaîne YouTube.