Développement Web

Service Workers avec Workbox 7 : tutoriel complet pas à pas

13 دقائق للقراءة

📍 Article principal : Application web installable hors-ligne : architecture complète pas à pas
Ce tutoriel approfondit la configuration du service worker. Pour la vue d’ensemble de l’architecture, consulter d’abord l’article principal.

Pourquoi Workbox plutôt qu’un service worker écrit à la main

Un service worker manuel pour un site moyen, c’est entre trois cents et huit cents lignes de JavaScript répétitif : déclaration des caches, gestion des versions, écriture de chaque stratégie, traitement des expirations, nettoyage des anciens caches. La logique est bien connue, les bugs aussi : oublier de supprimer un ancien cache à l’activation, mal gérer une réponse opaque, ne pas exclure correctement les requêtes POST du fallback. Workbox, maintenu par l’équipe Chrome de Google, encapsule ces patrons dans une bibliothèque modulaire dont la version stable est la 7.4.1. Le code généré reste lisible et débogable, mais les pièges classiques sont neutralisés par défaut.

Trois modes d’utilisation cohabitent. Le mode Workbox CLI génère un fichier sw.js complet à partir d’une configuration déclarative — idéal pour un site statique. Le mode Workbox Build s’intègre dans Webpack, Vite ou Rollup pour précacher automatiquement les actifs du bundle. Le mode injectManifest donne la main au développeur sur le squelette du service worker tout en injectant la liste précalculée des actifs à précacher. Ce tutoriel couvre les deux premiers, qui couvrent 80 % des besoins.

Prérequis

  • Node.js 20 ou plus (Workbox CLI exige cette version minimale)
  • Un terminal POSIX ou PowerShell
  • Un site statique ou bundlé déjà servi en HTTPS (ou en local sur localhost)
  • Niveau intermédiaire en JavaScript ES modules
  • Temps estimé : 40 minutes pour une première intégration complète

Étape 1 — Installer Workbox CLI

L’outil en ligne de commande est le point d’entrée le plus rapide. Il s’installe globalement ou comme dépendance de développement. La version 7 impose Node.js 20 minimum, vérifiez votre installation avant de continuer.

node --version
# Devrait afficher v20.x.x ou supérieur

npm install --save-dev workbox-cli@7.4.1

L’installation locale plutôt que globale est recommandée pour figer la version dans package.json et garantir la reproductibilité entre développeurs. Une fois l’installation terminée, lancez npx workbox --version pour confirmer que le binaire est exécutable. La version affichée doit correspondre à celle installée.

Étape 2 — Générer une configuration interactive

Workbox CLI propose un assistant qui pose les questions essentielles et écrit un fichier de configuration JavaScript adapté. Lancez la commande depuis la racine de votre projet, là où se trouve déjà votre index.html.

npx workbox wizard

Six questions vous attendent : le répertoire racine de votre site (typiquement ./dist, ./build ou ./public), les patrons globaux des fichiers à précacher (cocher HTML, CSS, JS, SVG, PNG), le fichier de sortie du service worker (dist/sw.js), le fichier de configuration à créer (workbox-config.cjs), si vous souhaitez ajouter des URLs supplémentaires hors patrons, et la taille maximale d’un fichier précachable. Acceptez les valeurs par défaut pour une première fois — vous ajusterez plus tard.

Le résultat est un fichier workbox-config.cjs qui ressemble à ceci :

module.exports = {
  globDirectory: 'dist/',
  globPatterns: ['**/*.{html,js,css,svg,png,woff2}'],
  swDest: 'dist/sw.js',
  maximumFileSizeToCacheInBytes: 5 * 1024 * 1024
};

Ce fichier déclare ce que Workbox doit précacher au moment de l’installation du service worker. Le glob couvre les types courants ; ajoutez jpg ou webp si vos images ne sont pas en PNG. La limite de 5 Mo par fichier évite de précacher accidentellement une vidéo qui ferait exploser le cache.

Étape 3 — Générer le service worker

La génération elle-même est instantanée. Workbox calcule un hash de chaque fichier listé et l’inclut dans le manifeste de précache, ce qui garantit qu’une modification du contenu invalide automatiquement le cache associé.

npx workbox generateSW workbox-config.cjs

La sortie console liste chaque fichier ajouté au précache avec son hash et sa taille, puis indique le total : nombre de fichiers, taille cumulée. Si vous voyez « 0 files » alors que votre dist/ contient bien des fichiers, vérifiez les permissions et le chemin globDirectory. Le fichier dist/sw.js est maintenant prêt — il fait environ 25 ko minifié et contient tout le runtime Workbox plus le manifeste de précache.

Étape 4 — Enregistrer le service worker depuis la page

Le service worker doit être enregistré par la page qui veut bénéficier de son interception. Cet enregistrement se fait habituellement au chargement de la page, après un test de support pour ne pas casser l’application sur les navigateurs trop anciens (un cas qui devient rare mais reste prudent).

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(reg => console.log('SW enregistré, scope:', reg.scope))
      .catch(err => console.error('Échec enregistrement SW:', err));
  });
}

Le chemin /sw.js est important : il détermine la portée du service worker. Servi depuis la racine, le service worker contrôle tout le domaine. Servi depuis /app/sw.js, il ne contrôle que /app/ et ses sous-chemins. Pour étendre la portée d’un fichier qui n’est pas à la racine, l’en-tête HTTP Service-Worker-Allowed doit être configuré côté serveur — chose qu’on évite quand on le peut.

Étape 5 — Vérifier dans le navigateur

Ouvrez l’application en HTTPS (ou sur http://localhost) puis les DevTools de Chrome. Onglet Application, sous-onglet Service Workers : la liste affiche le service worker installé avec son statut (activated and is running), son scope, sa source. Cochez Update on reload pour développer plus confortablement — chaque rechargement force la mise à jour du worker au lieu d’attendre le cycle normal.

Dans le sous-onglet Cache Storage, vous voyez les caches créés par Workbox. Le précache porte un nom du type workbox-precache-v2-https://votre.site/. Cliquez dessus pour inspecter les entrées : chaque ressource est enregistrée avec son URL et un identifiant de version. Onglet Network, rechargez la page avec la case Offline cochée : la page doit se charger entièrement depuis le service worker. Si une requête échoue, ouvrez le détail pour identifier laquelle.

Étape 6 — Ajouter des stratégies pour le runtime

Le précache couvre les actifs statiques connus à la compilation. Pour les ressources demandées dynamiquement (appels API, images uploadées par les utilisateurs, polices distantes), il faut ajouter des règles de cache au runtime. Workbox les exprime de façon déclarative dans la configuration.

module.exports = {
  globDirectory: 'dist/',
  globPatterns: ['**/*.{html,js,css,svg,png,woff2}'],
  swDest: 'dist/sw.js',
  maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
  runtimeCaching: [
    {
      urlPattern: /\.(?:png|jpg|jpeg|webp|avif)$/,
      handler: 'CacheFirst',
      options: {
        cacheName: 'images-v1',
        expiration: {
          maxEntries: 60,
          maxAgeSeconds: 30 * 24 * 60 * 60
        }
      }
    },
    {
      urlPattern: /^https:\/\/api\.votre-domaine\.com\//,
      handler: 'NetworkFirst',
      options: {
        cacheName: 'api-v1',
        networkTimeoutSeconds: 3,
        expiration: {
          maxEntries: 100,
          maxAgeSeconds: 24 * 60 * 60
        }
      }
    }
  ]
};

Chaque entrée runtimeCaching est évaluée dans l’ordre lors de chaque requête qui passe par le service worker. Le premier urlPattern qui matche détermine la stratégie. CacheFirst est utilisé ici pour les images qui changent rarement, avec une expiration à 30 jours et un plafond de 60 entrées pour borner la taille. NetworkFirst est utilisé pour l’API avec un délai réseau de 3 secondes au-delà duquel le cache prend le relais. Le détail conceptuel de ces stratégies est traité dans le tutoriel sur les stratégies de cache.

Étape 7 — Intégration avec un bundler

Les projets sérieux utilisent Vite, Webpack ou Rollup. Workbox propose des plugins pour chacun. Pour Vite, le plugin officiel s’appelle vite-plugin-pwa et embarque Workbox.

npm install --save-dev vite-plugin-pwa workbox-window

La configuration Vite se complète comme suit :

// vite.config.js
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    VitePWA({
      registerType: 'autoUpdate',
      includeAssets: ['favicon.ico', 'robots.txt'],
      manifest: {
        name: 'Mon Application',
        short_name: 'MonApp',
        theme_color: '#0d6efd',
        display: 'standalone',
        start_url: '/',
        icons: [
          { src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
          { src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
          { src: '/icon-maskable.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }
        ]
      },
      workbox: {
        runtimeCaching: [/* mêmes règles que ci-dessus */]
      }
    })
  ]
});

L’option registerType: 'autoUpdate' signifie que le plugin gère automatiquement la mise à jour du service worker — pratique en début de projet, mais on lui préfère souvent 'prompt' en production pour informer l’utilisateur avant de basculer sur une nouvelle version. Le manifeste est généré et lié automatiquement, ce qui évite de maintenir un fichier séparé.

Étape 8 — Stratégie de mise à jour

La gestion des mises à jour est le sujet le plus délicat. Par défaut, un nouveau service worker installé reste en attente jusqu’à ce que tous les onglets de l’origine soient fermés. Cela peut prendre des jours. Deux approches pour accélérer la propagation :

Approche silencieuse : appeler self.skipWaiting() dans l’événement install et clients.claim() dans activate. Le nouveau worker prend immédiatement le contrôle de tous les onglets. Risque : les onglets ouverts peuvent recevoir une version du shell incompatible avec leur état JavaScript en mémoire. À réserver aux applications dont l’état est minimal.

Approche avec consentement : détecter la nouvelle version depuis la page via l’événement controllerchange et afficher une bannière « Nouvelle version disponible — Recharger ». Sur clic, appeler navigator.serviceWorker.controller.postMessage({type: 'SKIP_WAITING'}) puis recharger. Le service worker écoute ce message et déclenche alors self.skipWaiting(). C’est la stratégie recommandée pour la majorité des cas.

Voici le squelette côté client pour la seconde approche, en utilisant la bibliothèque workbox-window qui simplifie cette mécanique :

import { Workbox } from 'workbox-window';

if ('serviceWorker' in navigator) {
  const wb = new Workbox('/sw.js');
  wb.addEventListener('waiting', () => {
    if (confirm('Nouvelle version disponible. Recharger ?')) {
      wb.messageSkipWaiting();
      wb.addEventListener('controlling', () => window.location.reload());
    }
  });
  wb.register();
}

L’événement waiting est émis quand un nouveau service worker est installé mais bloqué en attente. L’événement controlling est émis quand le nouveau worker prend le contrôle effectif. Cette séquence garantit que le rechargement se fait avec la nouvelle version active.

Étape 9 — Précacher des routes de navigation pour les SPA

Les applications à page unique posent un problème particulier : chaque URL profonde (par exemple /produits/42) correspond en réalité au même index.html côté serveur, avec un routage qui se fait en JavaScript dans le navigateur. Hors-ligne, le service worker reçoit une requête pour /produits/42 et, par défaut, ne trouve rien dans le précache puisque seul /index.html y figure. Le résultat : page blanche ou erreur réseau.

La solution consiste à configurer une navigation fallback : toute requête de type navigate (c’est-à-dire une URL qui entraîne un changement de page) reçoit en réponse le contenu de /index.html, et la SPA prend ensuite le relais pour afficher la bonne vue. Dans la configuration Workbox :

module.exports = {
  globDirectory: 'dist/',
  globPatterns: ['**/*.{html,js,css,svg,png,woff2}'],
  swDest: 'dist/sw.js',
  navigateFallback: '/index.html',
  navigateFallbackDenylist: [/^\/api\//, /\/[^/?]+\.[^/]+$/]
};

L’option navigateFallbackDenylist est cruciale : elle exclut du fallback les URLs qui ne sont pas des navigations applicatives. Le premier patron exclut les appels API. Le second exclut tout ce qui contient un point dans le dernier segment — typiquement les fichiers (logo.svg, main.js). Sans cette liste d’exclusion, une image manquante recevrait paradoxalement index.html en réponse, ce qui ferait apparaître un cassage visuel difficile à diagnostiquer.

Étape 10 — Tester le scénario hors-ligne complet

Avant de déclarer le travail terminé, un test bout en bout simule une vraie panne réseau. Dans DevTools, onglet Network, basculer sur Offline. Tester ensuite : chargement de la page d’accueil, navigation vers une page profonde, retour, rechargement complet. Tout doit fonctionner sans message d’erreur. Puis basculer sur le profil 3G dans le menu déroulant des conditions réseau — qui simule 400 Kbps en téléchargement avec 2 secondes de latence — et vérifier que les images se chargent depuis le cache plutôt que d’attendre le réseau.

Sur appareil réel, le test le plus probant consiste à charger la page sur Wi-Fi puis activer le mode avion. Toute l’expérience doit rester opérationnelle, à l’exception des actions qui exigent vraiment le serveur (envoi d’une mutation par exemple, qui sera mise en file via Background Sync).

Erreurs fréquentes

Erreur Cause Solution
« Failed to register a ServiceWorker: An unknown error occurred when fetching the script » Le fichier sw.js n’est pas accessible à l’URL annoncée, ou pas servi en HTTPS Vérifier l’accès direct à l’URL dans le navigateur ; vérifier les en-têtes (Content-Type: application/javascript)
Mise à jour qui ne se propage jamais HTTP cache trop agressif sur sw.js Configurer Cache-Control: no-cache spécifiquement pour sw.js côté serveur
Précache qui n’inclut pas tous les fichiers Glob trop restrictif, ou maximumFileSizeToCacheInBytes dépassé Lancer la génération avec --debug, ajuster les patrons et la limite de taille
Erreur « Quota exceeded » dans la console Réponses opaques accumulées (CORS manquant) Limiter le cache d’images cross-origin avec ExpirationPlugin par taille
Service worker actif en dev mais pas en prod HTTPS manquant ou certificat invalide en production Vérifier le certificat ; le service worker exige HTTPS valide hors localhost

Tutoriels frères

Pour aller plus loin

Questions fréquentes

Workbox alourdit-il significativement le bundle ?
Le runtime Workbox pèse environ 25 ko minifié et gzippé. Ce coût n’est payé que par le service worker, pas par les pages. La page ne charge que la mince bibliothèque workbox-window (environ 3 ko) si vous l’utilisez.

Peut-on mélanger précache statique et stratégies runtime ?
Oui, c’est le cas le plus courant. Les actifs du bundle sont précachés à l’installation, les ressources dynamiques sont gérées par les règles runtimeCaching. Workbox priorise correctement les deux.

Comment supprimer complètement un service worker en cours d’utilisation ?
Publier un sw.js minimal — sans dépendre de Workbox — qui appelle self.registration.unregister() dans activate et nettoie tous les caches via caches.keys() puis caches.delete(). Les utilisateurs récupèreront cette version au prochain cycle de mise à jour, et seront alors débrouillés du service worker. Il n’y a pas de commande Workbox CLI dédiée — la suppression se code à la main, c’est moins de 20 lignes.

Workbox fonctionne-t-il avec une SPA en mode hash routing ?
Oui, mais il faut configurer navigateFallback dans la configuration pour que le service worker serve index.html en réponse à toute navigation interne. Sans cette option, le rechargement d’une URL profonde hors-ligne renvoie une erreur 404.

Service ITSkillsCenter

Site ou application web sur mesure

Conception Pro + Nom de domaine 1 an + Hébergement 1 an + Formation + Support 6 mois. Accès et code livrés. À partir de 350 000 FCFA.

Demander un devis
Publicité