Utiliser les service workers
============================

Dans cet article, nous aborderons les notions pour vous permettre de démarrer avec les *service workers* comme l'architecture associée, l'enregistrement d'un *service worker*, les processus d'installation et d'activation pour un nouveau *service worker*, la mise à jour d'un *service worker*, le contrôle du cache associé et les réponses personnalisées en appliquant ceci à une application d'exemple simple ayant des fonctionnalités hors-ligne.

Le point de départ des *service workers*
----------------------------------------

Un problème qui se pose depuis plusieurs années sur le Web est la perte de connexion au réseau. Une application web, si performante soit elle, fournira un service déplorable si on ne peut pas la télécharger. Plusieurs tentatives ont eu lieu pour résoudre ce problème et certains aspects ont été réglés. Toutefois, il était encore difficile de bien contrôler la mise en cache de données et de gérer l'interception de requêtes.

Les *service workers* aident à résoudre ces problèmes. En utilisant un *service worker*, on peut mettre en place une application qui utilise des fichiers en cache et ainsi fournir des fonctionnalités, même hors ligne, avant d'obtenir des données depuis le réseau. Ce qui est possible avec les applications natives devient possible avec les applications web.

Un *service worker* fonctionne comme un serveur intermédiaire (« proxy »), permettant de modifier les requêtes et les réponses en utilisant les éléments qu'il a en cache.

Mise en place pour manipuler les *service workers*
--------------------------------------------------

Les *service workers* sont présents par défaut dans les navigateurs. Pour exécuter du code dans des *service workers*, il faut qu'il soit servi avec HTTPS (pour des raisons de sécurité). Il est donc nécessaire d'avoir un serveur web prenant en charge HTTPS (ça peut être grâce à un service comme GitHub, Netlify, Vercel, etc.). Afin de simplifier le développement local, `localhost` est également considéré par les navigateurs comme une origine sécurisée.

Architecture de base
--------------------

Lors de la mise en place d'un *service worker*, on a généralement les étapes suivantes :

1. Le code du *service worker* est récupérée et enregistrée grâce à `serviceWorkerContainer.register()`. Si cela fonctionne, le *service worker* est exécuté dans une portée `ServiceWorkerGlobalScope` (en-US) : il s'agit d'un type de contexte de *worker* particulier, qui s'exécute en dehors du *thread* principal, sans accès au DOM. Le *service worker* est alors prêt à traiter des évènements.
2. L'installation se déroule alors. Un évènement `install` est toujours le premier évènement envoyé à un *service worker* (et peut être utilisé pour remplir une base de données IndexedDB, et mettre en cache des fichiers). Pendant cette étape, l'application prépare ce qui doit l'être pour fonctionner hors ligne.
3. Lorsque le gestionnaire d'évènements `oninstall` a terminé, on considère que le *service worker* est installé. À cet instant, une version précédente du *service worker* peut toujours être active et contrôler les pages ouvertes. Comme on ne veut pas que deux versions différentes du même *service worker* s'exécutent au même moment, la nouvelle version n'est pas encore active.
4. Une fois que toutes les pages contrôlées par l'ancienne version du *service worker* sont fermées, on peut alors enlever l'ancienne version. Le nouveau *service worker* installé reçoit un évènement `activate`. On utilise principalement `activate` pour nettoyer les ressources utilisées par les versions précédentes d'un *service worker*. Le nouveau *service worker* peut appeler `skipWaiting()` (en-US) pour demander l'activation immédiate, sans attendre la fermeture des pages ouvertes. Le nouveau *service worker* recevra alors l'évènement `activate` immédiatement, et prendra le contrôle des pages ouvertes concernées.
5. Le *service worker* contrôlera alors les pages qui ont été ouvertes après que la fonction `register()` a fini son exécution. Autrement dit, les documents devront être rechargés afin d'être contrôlé, car l'état de contrôle d'un document avec ou sans *service worker* est déterminé à son chargement et reste ainsi pendant sa durée de vie. Pour surcharger ce comportement par défaut et contrôler les pages ouvertes, un *service worker* peut appeler `clients.claim()`.
6. À chaque fois qu'une nouvelle version d'un *service worker* est récupérée, ce cycle se répète et les données de la version précédente sont nettoyées pendant l'activation de la nouvelle version.

![Diagramme illustrant le cycle de vie d'un service worker](/fr/docs/Web/API/Service_Worker_API/Using_Service_Workers/sw-lifecycle.svg)

Voici les évènements disponibles pour un *service worker* :

* `install` (en-US)
* `activate` (en-US)
* `message` (en-US)
* Évènements fonctionnels
    + `fetch` (en-US)
    + `sync` (en-US)
    + `push` (en-US)
Démonstration
-------------

Pour illustrer les bases de l'enregistrement et de l'installation d'un *service worker*, nous avons créé une application d'exemple intitulée simple-service-worker, qui est une galerie d'images de Lego Star Wars. Elle utilise une fonction à base de promesses pour lire les données des images depuis un objet JSON et charger les images avec `fetch()` avant de les afficher sur une ligne dans la page. L'exemple n'utilise que des ressources statiques. Nous verrons aussi l'enregistrement, l'installation et l'activation d'un *service worker*.

![Les mots Star Wars suivis par une image du personnage de Dark Vador en Lego](/fr/docs/Web/API/Service_Worker_API/Using_Service_Workers/demo-screenshot.png)

Vous pouvez voir le code source sur GitHub, et la démo.

### Enregistrement

Le premier bloc de code du fichier JavaScript `app.js` sert de point d'entrée pour l'utilisation des *service workers*.


```js
const registerServiceWorker = async () => {
  if ("serviceWorker" in navigator) {
    try {
      const registration = await navigator.serviceWorker.register("/sw.js", {
        scope: "/",
      });
      if (registration.installing) {
        console.log("Installation du service worker en cours");
      } else if (registration.waiting) {
        console.log("Service worker installé");
      } else if (registration.active) {
        console.log("Service worker actif");
      }
    } catch (error) {
      console.error(`L'enregistrement a échoué : ${error}`);
    }
  }
};

// …

registerServiceWorker();
```


1. Le bloc `if` teste la présence de la fonctionnalité pour s'assurer que les *service workers* sont bien pris en charge avant de tenter l'enregistrement.
2. Ensuite, on utilise la fonction `ServiceWorkerContainer.register()` pour enregistrer le *service worker* pour ce site. Il s'agit ici d'un fichier JavaScript. Attention, l'URL du fichier ciblé est relative à l'origine de la page et pas à l'URL du fichier JavaScript qui la référence.
3. Le paramètre `scope` est optionnel et peut être utilisé afin d'indiquer le sous-ensemble du contenu qu'on veut contrôler avec le *service worker*. Ici, nous avons indiqué `'/'`, ce qui représente tout le contenu situé sous l'origine de l'application. Si ce paramètre est absent, la valeur par défaut qui sera utilisée sera également `'/'` (nous l'avons inclus ici à des fins d'explication).

Cela permet d'enregistrer un *service worker* qui s'exécute dans un contexte de *worker* et n'a donc pas accès au DOM.

Un seul *service worker* peut contrôler de nombreuses pages. Chaque fois qu'une page concernée par la portée du *service worker* est chargée, le *service worker* est installé pour cette page et s'en occupe. Il faut donc faire attention aux variables globales dans le script d'un *service worker*, car chaque page ne récupère un exemplaire séparé distinct.

**Note :** En utilisant la détection de fonctionnalité comme nous l'avons fait plus haut, cela permet aux navigateurs qui ne prennent pas en charge ces fonctionnalités de servir l'application en ligne normalement.

#### Pourquoi est-ce l'enregistrement de mon *service worker* échoue ?

Il peut y avoir plusieurs raisons :

* L'application n'est pas servie via HTTPS.
* Le chemin vers le fichier du *service worker* n'est pas écrit correctement : il doit être relatif à l'origine et pas à la racine du répertoire de l'application. Pour notre exemple, le script du *worker* est situé à `https://bncb2v.csb.app/sw.js`, et la racine de l'application est `https://bncb2v.csb.app/`, mais il faut écrire le chemin ainsi : `/sw.js`.
* Il est interdit de pointer vers un *service worker* dont l'origine est différente de celle de l'application.
* La page concernée ne fait pas partie de la portée du *service worker*.
* La portée maximale d'un *service worker* correspond par défaut à l'emplacement du *worker*. Il est possible d'indiquer une liste de portées maximales plus larges avec l'en-tête HTTP `Service-Worker-Allowed`.
* Pour Firefox, les API *Service Worker* sont inaccessibles en navigation privée, lorsque l'historique est désactivé ou que les données de navigation (dont les cookies) sont supprimés à la fermeture de Firefox.
* Pour Chrome, l'enregistrement échoue si l'option « Bloquer tous les cookies (non recommandé) » est activée.
### Installation et activation : remplir le cache

Une fois le *service worker* enregistré, le navigateur essaiera d'installer le *service worker* sur la page/le site.

L'évènement `install` est déclenché lorsque l'installation s'est déroulée correctement. Il est généralement utilisé pour remplir les caches qui seront utilisés hors ligne. Pour cela, on utilise l'API de stockage des *service worker*, `cache` : un objet global du *service worker* qui permet de stocker les fichiers fournis par les réponses et de les référencées par des clés formées par les requêtes. Cette API fonctionne de façon semblable au cache standard du navigateur, mais est spécifique au domaine. Le cache persiste jusqu'à nouvel ordre.

Voici comme nous gérons l'évènement `install` dans notre exemple :


```js
const addResourcesToCache = async (resources) => {
  const cache = await caches.open("v1");
  await cache.addAll(resources);
};

self.addEventListener("install", (event) => {
  event.waitUntil(
    addResourcesToCache([
      "/",
      "/index.html",
      "/style.css",
      "/app.js",
      "/image-list.js",
      "/star-wars-logo.jpg",
      "/gallery/bountyHunters.jpg",
      "/gallery/myLittleVader.jpg",
      "/gallery/snowTroopers.jpg",
    ]),
  );
});
```


1. On ajoute un gestionnaire d'évènement pour `install` sur le *service worker* (représenté par `self`) et on chaîne un appel à la méthode `ExtendableEvent.waitUntil()` (en-US) lors de la réception de l'évènement, pour s'assurer que l'installation du *service worker* ne commencera pas avant que le code contenu dans `waitUntil()` ait été exécuté.
2. Dans la fonction `addResourcesToCache()`, on utilise la méthode `caches.open()` afin de créer un nouveau cache intitulé `v1`, qui sera la première version de notre cache pour les ressources de notre site. On appelle ensuite une fonction `addAll()` sur le cache ainsi créé et qui prend en paramètre un tableau d'URL pour les différentes ressources qu'on souhaite mettre en cache. Les URL sont relatives à l'emplacement du *worker*.
3. Si la promesse échoue, l'installation échoue et le *service worker* ne fera rien. Il est toujours possible de corriger le code et de réessayer lors du prochain enregistrement.
4. Après une installation réussie, on passe à l'activation du *service worker*. Lors d'une première installation, cela peut ne pas avoir beaucoup d'intérêt, mais cela s'avèrera utile lors des mises à jour du *service worker* (voir la section Mettre à jour le *service worker* ci-après).

**Note :** L'API *Web Storage* (`localStorage`) fonctionne de façon semblable au cache d'un *service worker* mais est synchrone et son utilisation n'est donc pas autorisée dans les *services workers*.

**Note :** Si besoin, l'API IndexedDB peut être utilisée dans un *service worker* pour stocker des données.

### Créer des réponses sur mesure pour les requêtes

Maintenant que les fichiers sont mis en cache, il faut indiquer au *service worker* quoi faire de ce contenu. Pour cela, on utilise l'évènement `fetch`.

1. Un évènement `fetch` est déclenché à chaque fois qu'une ressource doit être récupérée depuis une page contrôlée par un *service worker*. Cela inclut les documents situés dans la portée du *worker* et les ressources référencées depuis ces documents (ainsi, si `index.html` effectue une requête vers une origine différente pour charger une image, la requête passera quand même par le *service worker*).
2. On peut attacher un gestionnaire d'évènement pour `fetch` au *service worker*, puis appeler la méthode `respondWith()` sur l'évènement afin d'intercepter les réponses HTTP et les remplacer par le contenu voulu.

```js
self.addEventListener("fetch", (event) => {
  event
    .respondWith
    // contenu spécifique
    ();
});
```

3. On peut ainsi répondre avec les ressources dont l'URL correspond à la requête interceptée :

```js
self.addEventListener("fetch", (event) => {
  event.respondWith(caches.match(event.request));
});
```

`caches.match(event.request)` permet de cibler les ressources demandées sur le réseau avec les ressources équivalentes et qui sont disponibles dans le cache (si une telle ressource est disponible). La correspondance est effectuée avec l'URL et différents en-têtes, comme pour une requête HTTP normale.

![Diagramme illustrant le rôle de l'évènement fetch](/fr/docs/Web/API/Service_Worker_API/Using_Service_Workers/sw-fetch.svg)

Gérer les requêtes qui échouent
-------------------------------

`caches.match(event.request)` fonctionne à merveille s'il y a une ressource correspondante dans le cache du *service worker*, mais que se passe-t-il si ce n'est pas le cas ? Si on ne fournit pas de gestion d'erreur, la promesse est résolue avec `undefined` et rien ne sera renvoyé.

Dans ce cas, on peut tester la réponse du cache et, si besoin, utiliser une requête réseau classique :


```js
const cacheFirst = async (request) => {
  const responseFromCache = await caches.match(request);
  if (responseFromCache) {
    return responseFromCache;
  }
  return fetch(request);
};

self.addEventListener("fetch", (event) => {
  event.respondWith(cacheFirst(event.request));
});
```


Ainsi, si les ressources ne sont pas dans le cache, on les récupère depuis le réseau.

Une stratégie plus raffinée serait de mettre en cache les ressources que nous récupérons depuis le réseau afin qu'elles puissent être réutilisées hors ligne par la suite. Dans notre exemple, cela signifie que si nous ajoutons de nouvelles images à la galerie, notre application pourrait automatiquement les récupérer la première fois et les mettre en cache. Voici un fragment de code qui implémente cette méthode :


```js
const putInCache = async (request, response) => {
  const cache = await caches.open("v1");
  await cache.put(request, response);
};

const cacheFirst = async (request) => {
  const responseFromCache = await caches.match(request);
  if (responseFromCache) {
    return responseFromCache;
  }
  const responseFromNetwork = await fetch(request);
  putInCache(request, responseFromNetwork.clone());
  return responseFromNetwork;
};

self.addEventListener("fetch", (event) => {
  event.respondWith(cacheFirst(event.request));
});
```


Si la ressource de la requête n'est pas disponible dans le cache, on la demande depuis le réseau avec `await fetch(request)`. Ensuite, on clone la réponse dans le cache. La fonction `putInCache()` utilise `caches.open('v1')` et `cache.put()` afin d'ajouter les ressources au cache. La réponse originale est transmise au navigateur pour la page qui a demandé la ressource.

Cloner la réponse est nécessaire, car les flux de requête et de réponse ne peuvent être lus qu'une seule fois. Afin de fournir la réponse au navigateur et la mettre en cache, il faut la cloner. La version originale est fournie au navigateur et le clone est mis en cache. Chaque réponse est lue une seule fois.

Il peut sembler étrange qu'on n'attende pas le retour de la promesse dans `putInCache()`. En réalité, on ne veut pas attendre que le clone de la réponse ait été ajouté au cache avant de renvoyer la réponse.

Le problème restant est que si la requête ne correspond à rien en cache et que le réseau n'est pas disponible, la requête échouera. Voyons comment fournir un contenu par défaut dans ce cas-là :


```js
const putInCache = async (request, response) => {
  const cache = await caches.open("v1");
  await cache.put(request, response);
};

const cacheFirst = async ({ request, preloadResponsePromise, fallbackUrl }) => {
  // Pour commencer on essaie d'obtenir la ressource depuis le cache
  const responseFromCache = await caches.match(request);
  if (responseFromCache) {
    return responseFromCache;
  }

  // Ensuite, on tente de l'obtenir du réseau
  try {
    const responseFromNetwork = await fetch(request);
    // Une réponse ne peut être utilisée qu'une fois
    // On la clone pour en mettre une copie en cache
    // et servir l'originale au navigateur
    putInCache(request, responseFromNetwork.clone());
    return responseFromNetwork;
  } catch (error) {
    const fallbackResponse = await caches.match(fallbackUrl);
    if (fallbackResponse) {
      return fallbackResponse;
    }
    // Quand il n'y a même pas de contenu par défaut associé
    // on doit tout de même renvoyer un objet Response
    return new Response("Une erreur réseau s'est produite", {
      status: 408,
      headers: { "Content-Type": "text/plain" },
    });
  }
};

self.addEventListener("fetch", (event) => {
  event.respondWith(
    cacheFirst({
      request: event.request,
      fallbackUrl: "/gallery/myLittleVader.jpg",
    }),
  );
});
```


Ici, on utilise l'image comme contenu par défaut, car les seules mises à jour qui risquent d'échouer portent sur les nouvelles images. Tout le reste dépend de la phase d'installation que nous avons vue plus haut.

Préchargement du *worker* lors de la navigation
-----------------------------------------------

S'il est activé, le préchargement à la navigation (en-US) commence le téléchargement des ressources dès que la requête de récupération est émise, en parallèle de l'activation du *service worker*. Cela permet que le téléchargement démarre immédiatement lors de la navigation vers une page plutôt que d'avoir à d'abord attendre l'activation du *service worker*. Ce délai se produit rarement mais reste inévitable et, lorsqu'il survient, peut être significatif.

Pour commencer, la fonctionnalité doit être activée lors de l'activation du *service worker* en utilisant `registration.navigationPreload.enable()` (en-US) :


```js
self.addEventListener("activate", (event) => {
  event.waitUntil(self.registration?.navigationPreload.enable());
});
```


Ensuie, on utilisera `event.preloadResponse` (en-US) pour attendre que le téléchargement de la ressource préchargée soit terminée dans le gestionnaire d'évènement `fetch`.

Reprenons le code des sections précédentes et insérons la gestion du préchargement après la vérification du cache et avant la récupération depuis le réseau.

Voici l'algorithme mis à jour :

1. On vérifie le cache.
2. On attend `event.preloadResponse`, qui est passé sous la forme `preloadResponsePromise` à la fonction `cacheFirst`. On met en cache le résultat s'il y en a un.
3. S'il n'y a toujours aucune ressource récupérée, on tente de la récupérer depuis le réseau.


```js
const addResourcesToCache = async (resources) => {
  const cache = await caches.open("v1");
  await cache.addAll(resources);
};

const putInCache = async (request, response) => {
  const cache = await caches.open("v1");
  await cache.put(request, response);
};

const cacheFirst = async ({ request, preloadResponsePromise, fallbackUrl }) => {
  // Pour commencer on essaie d'obtenir la ressource depuis le cache
  const responseFromCache = await caches.match(request);
  if (responseFromCache) {
    return responseFromCache;
  }

  // Ensuite, on tente d'utiliser et de mettre en cache
  // la réponse préchargée si elle existe
  const preloadResponse = await preloadResponsePromise;
  if (preloadResponse) {
    console.info("using preload response", preloadResponse);
    putInCache(request, preloadResponse.clone());
    return preloadResponse;
  }

  // Ensuite, on tente de l'obtenir du réseau
  try {
    const responseFromNetwork = await fetch(request);
    // Une réponse ne peut être utilisée qu'une fois
    // On la clone pour en mettre une copie en cache
    // et servir l'originale au navigateur
    putInCache(request, responseFromNetwork.clone());
    return responseFromNetwork;
  } catch (error) {
    const fallbackResponse = await caches.match(fallbackUrl);
    if (fallbackResponse) {
      return fallbackResponse;
    }
    // Quand il n'y a même pas de contenu par défaut associé
    // on doit tout de même renvoyer un objet Response
    return new Response("Network error happened", {
      status: 408,
      headers: { "Content-Type": "text/plain" },
    });
  }
};

// On active le préchargement à la navigation
const enableNavigationPreload = async () => {
  if (self.registration.navigationPreload) {
    await self.registration.navigationPreload.enable();
  }
};

self.addEventListener("activate", (event) => {
  event.waitUntil(enableNavigationPreload());
});

self.addEventListener("install", (event) => {
  event.waitUntil(
    addResourcesToCache([
      "/",
      "/index.html",
      "/style.css",
      "/app.js",
      "/image-list.js",
      "/star-wars-logo.jpg",
      "/gallery/bountyHunters.jpg",
      "/gallery/myLittleVader.jpg",
      "/gallery/snowTroopers.jpg",
    ]),
  );
});

self.addEventListener("fetch", (event) => {
  event.respondWith(
    cacheFirst({
      request: event.request,
      preloadResponsePromise: event.preloadResponse,
      fallbackUrl: "/gallery/myLittleVader.jpg",
    }),
  );
});
```


On notera dans cet exemple qu'on télécharge et met en cache les mêmes données pour la ressource qu'elle soit téléchargée normalement ou préchargée. On pourrait aussi choisir de télécharger et de mettre en cache une ressource différente lors du préchargement. Pour plus d'informations, voir `NavigationPreloadManager` > réponses personnalisées (en-US).

Mettre à jour le *service worker*
---------------------------------

Si le *service worker* a précédemment été installé et qu'une nouvelle version est disponible lors du rafraîchissement ou du chargement de la page, la nouvelle version est installée en arrière-plan, mais n'est pas activée. Elle est uniquement activée lorsqu'il n'y a plus de pages chargées qui utilisent l'ancien *service worker*. Dès qu'il n'y a plus de page chargée, le nouveau *service worker* s'active.

**Note :** Il est possible de contourner ce comportement en utilisant `Clients.claim()`.

Il faut alors mettre à jour le gestionnaire d'évènement pour `install` dans le nouveau *service worker* (notez le nouveau numéro de version) :


```js
const addResourcesToCache = async (resources) => {
  const cache = await caches.open("v2");
  await cache.addAll(resources);
};

self.addEventListener("install", (event) => {
  event.waitUntil(
    addResourcesToCache([
      "/",
      "/index.html",
      "/style.css",
      "/app.js",
      "/image-list.js",

      // …

      // inclure les nouvelles ressources associées
      // à la nouvelle version…
    ]),
  );
});
```


Lorsque cette installation se produit, la version précédente est toujours utilisée pour les interceptions/récupérations de ressources. La nouvelle version est installée en arrière-plan. En appelant notre nouveau cache `v2`, le cache précédent (`v1`) n'est pas perturbé.

Lorsqu'aucune page n'utilise la version précédente, c'est le nouveau *service worker* qui est activé et qui devient alors responsable des interceptions/récupérations.

### Supprimer les anciens caches

Comme nous l'avons vu dans la section précédente, lorsqu'on met à jour un *service worker* avec une nouvelle version, on pourra créer un nouveau cache avec le gestionnaire d'évènement `install`. Tant qu'il y a des pages ouvertes qui sont contrôlées par l'ancienne version, il faut conserver les deux caches, car la version précédente utilise cette version précédente du cache. L'évènement `activate` peut ensuite être utilisé pour retirer des données des caches précédents.

Les promesses passées à `waitUntil()` bloqueront les autres évènements tant qu'elles ne seront pas terminées. Cela permet de s'assurer que les étapes de nettoyage auront été réalisées lorsque le premier évènement `fetch` parviendra au nouveau *service worker*.


```js
const deleteCache = async (key) => {
  await caches.delete(key);
};

const deleteOldCaches = async () => {
  const cacheKeepList = ["v2"];
  const keyList = await caches.keys();
  const cachesToDelete = keyList.filter((key) => !cacheKeepList.includes(key));
  await Promise.all(cachesToDelete.map(deleteCache));
};

self.addEventListener("activate", (event) => {
  event.waitUntil(deleteOldCaches());
});
```

Outils de développement
-----------------------

* Chrome
* Firefox
    + Il est possible de réinitialiser les *service workers* et leurs caches pour un site en utilisant le menu « Oublier ce site » dans l'historique, le bouton Gérer les données » dans les préférences ou le bouton « Effacer des données » qu'il est possible d'ajouter à la barre d'outils en la personnalisant.
* Edge
Voir aussi
----------

* Les promesses en JavaScript
* Utiliser les *web workers*