Cycle de vie d'un service worker

Jake Archibald
Jake Archibald

Le cycle de vie du service worker est sa partie la plus complexe. Si vous ne savez pas ce qu'il essaie de faire et quels sont ses avantages, vous pouvez avoir l'impression qu'il vous oppose. Une fois que vous savez comment cela fonctionne, vous pouvez proposer des mises à jour fluides et discrètes aux utilisateurs, en combinant le meilleur des modèles Web et natifs.

Il s'agit d'un examen approfondi, mais les points à la fin de chaque section couvrent la plupart des informations dont vous avez besoin.

L'intent

L'objectif du cycle de vie est de :

  • Rendre l'approche "d'abord hors connexion" possible
  • Permet à un nouveau service worker de se préparer sans perturber le service actuel.
  • Assurez-vous qu'une page incluse dans le champ d'application est contrôlée par le même service worker (ou aucun service worker) tout au long.
  • Assurez-vous qu'une seule version de votre site est exécutée à la fois.

Ce dernier point est très important. Sans service worker, les utilisateurs peuvent charger un onglet sur votre site, puis en ouvrir un autre plus tard. Cela peut entraîner l'exécution de deux versions de votre site en même temps. Cela peut parfois être acceptable, mais si vous gérez de l'espace de stockage, vous pouvez facilement vous retrouver avec deux onglets ayant des opinions très différentes sur la façon dont leur espace de stockage partagé doit être géré. Cela peut entraîner des erreurs, voire une perte de données.

Premier service worker

En bref :

  • L'événement install est le premier événement reçu par un service worker. Il ne se produit qu'une seule fois.
  • Une promesse transmise à installEvent.waitUntil() indique la durée et la réussite ou l'échec de votre installation.
  • Un service worker ne recevra pas d'événements tels que fetch et push tant qu'il n'aura pas terminé l'installation et ne sera pas passé à l'état "actif".
  • Par défaut, les récupérations d'une page ne passent pas par un service worker, sauf si la requête de la page elle-même est passée par un service worker. Vous devrez donc actualiser la page pour voir les effets du service worker.
  • clients.claim() peut remplacer cette valeur par défaut et prendre le contrôle des pages non contrôlées.

Prenons ce code HTML :

<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered!', reg))
    .catch(err => console.log('Boo!', err));

  setTimeout(() => {
    const img = new Image();
    img.src = '/dog.svg';
    document.body.appendChild(img);
  }, 3000);
</script>

Il enregistre un service worker et ajoute une image de chien au bout de trois secondes.

Voici son service worker, sw.js :

self.addEventListener('install', event => {
  console.log('V1 installing…');

  // cache a cat SVG
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/cat.svg'))
  );
});

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

Il met en cache une image d'un chat et la diffuse chaque fois qu'une requête est envoyée pour /dog.svg. Toutefois, si vous exécutez l'exemple ci-dessus, un chien s'affichera la première fois que vous chargerez la page. Appuyez sur "Actualiser" pour voir le chat.

Champ d'application et contrôle

Le champ d'application par défaut d'un enregistrement de service worker est ./ par rapport à l'URL du script. Par conséquent, si vous enregistrez un service worker à //example.com/foo/bar.js, son champ d'application par défaut est //example.com/foo/.

Nous appelons les pages, les workers et les workers partagés clients. Votre service worker ne peut contrôler que les clients qui sont concernés. Une fois qu'un client est "contrôlé", ses extractions passent par le service worker couvert. Vous pouvez détecter si un client est contrôlé via navigator.serviceWorker.controller, qui sera nul ou une instance de service worker.

Télécharger, analyser et exécuter

Votre tout premier service worker se télécharge lorsque vous appelez .register(). Si le téléchargement ou l'analyse de votre script échoue, ou si une erreur est générée lors de son exécution initiale, la promesse d'enregistrement est rejetée et le service worker est supprimé.

Les outils de développement de Chrome affichent l'erreur dans la console et dans la section du service worker de l'onglet de l'application :

Erreur affichée dans l&#39;onglet des outils pour les développeurs du service worker

Installer

Le premier événement qu'un service worker reçoit est install. Il est déclenché dès que le nœud de calcul s'exécute et n'est appelé qu'une seule fois par service worker. Si vous modifiez votre script de service worker, le navigateur le considère comme un service worker différent et il reçoit son propre événement install. Je vous présenterai les mises à jour en détail ultérieurement.

L'événement install vous permet de mettre en cache tout ce dont vous avez besoin avant de pouvoir contrôler les clients. La promesse que vous transmettez à event.waitUntil() indique au navigateur quand l'installation est terminée et si elle a réussi.

Si votre promesse est refusée, cela indique que l'installation a échoué et que le navigateur élimine le service worker. Il ne contrôlera jamais les clients. Cela signifie que nous pouvons nous attendre à ce que cat.svg soit présent dans le cache dans nos événements fetch. Il s'agit d'une dépendance.

Activer

Une fois que votre service worker est prêt à contrôler les clients et à gérer les événements fonctionnels tels que push et sync, vous recevrez un événement activate. Cela ne signifie pas pour autant que la page qui a appelé .register() sera contrôlée.

La première fois que vous chargez la démo, même si dog.svg est demandé longtemps après l'activation du service worker, il ne gère pas la requête et l'image du chien s'affiche toujours. La valeur par défaut est consistency (cohérence). Si votre page se charge sans service worker, ses sous-ressources ne se chargeront pas non plus. Si vous chargez la démo une seconde fois (en d'autres termes, si vous actualisez la page), elle sera contrôlée. La page et l'image passeront par des événements fetch, et un chat s'affichera à la place.

clients.claim

Vous pouvez prendre le contrôle des clients non contrôlés en appelant clients.claim() dans votre service worker une fois qu'il est activé.

Voici une variante de la démonstration ci-dessus qui appelle clients.claim() dans son événement activate. Vous devriez voir un chat la première fois. Je dis "devrait", car il s'agit d'un élément sensible au temps. Vous ne verrez un chat que si le service worker s'active et que clients.claim() prend effet avant que l'image ne tente de se charger.

Si vous utilisez votre service worker pour charger les pages différemment de ce qu'elles seraient chargées via le réseau, clients.claim() peut s'avérer problématique, car votre service worker finit par contrôler certains clients qui se chargent sans cet outil.

Mettre à jour le service worker

En bref :

  • Une mise à jour est déclenchée si l'un des événements suivants se produit :
    • Une navigation vers une page couverte.
    • Un événement fonctionnel tel que push et sync, sauf si une recherche de mises à jour a eu lieu au cours des dernières 24 heures
    • Appel de .register() uniquement si l'URL du service worker a changé. Toutefois, évitez de modifier l'URL du nœud de calcul.
  • La plupart des navigateurs, y compris Chrome 68 et versions ultérieures, ignorent par défaut les en-têtes de mise en cache lors de la recherche de mises à jour du script de service worker enregistré. Ils respectent toujours les en-têtes de mise en cache lors de l'extraction des ressources chargées dans un service worker via importScripts(). Vous pouvez ignorer ce comportement par défaut en définissant l'option updateViaCache lorsque vous enregistrez votre service worker.
  • Votre service worker est considéré comme mis à jour s'il diffère par octet de celui que le navigateur possède déjà. (Nous allons également l'étendre aux scripts/modules importés.)
  • Le service worker mis à jour est lancé à côté de l'existant et reçoit son propre événement install.
  • Si votre nouveau nœud de calcul présente un code d'état incorrect (par exemple, 404), échoue à l'analyse, génère une erreur lors de l'exécution ou refuse lors de l'installation, il est ignoré, mais le nœud actuel reste actif.
  • Une fois l'installation terminée, le nœud de calcul mis à jour wait jusqu'à ce que le nœud de calcul existant ne contrôle plus aucun client. (Notez que les clients se chevauchent lors d'une actualisation.)
  • self.skipWaiting() empêche l'attente, ce qui signifie que le service worker s'active dès la fin de l'installation.

Imaginons que nous ayons modifié le script de notre service worker pour qu'il renvoie une photo d'un cheval plutôt que d'un chat:

const expectedCaches = ['static-v2'];

self.addEventListener('install', event => {
  console.log('V2 installing…');

  // cache a horse SVG into a new cache, static-v2
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  // delete any caches that aren't in expectedCaches
  // which will get rid of static-v1
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(() => {
      console.log('V2 now ready to handle fetches!');
    })
  );
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the horse SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

Découvrez une démonstration de ce processus ci-dessus. Une image de chat devrait toujours s'afficher. Voici pourquoi :

Installer

Notez que j'ai remplacé le nom du cache static-v1 par static-v2. Cela signifie que je peux configurer le nouveau cache sans écraser des éléments dans le cache actuel, que l'ancien service worker utilise toujours.

Ces modèles créent des caches spécifiques à la version, semblables aux éléments qu'une application native regroupe avec son exécutable. Vous pouvez également avoir des caches qui ne sont pas spécifiques à une version, comme avatars.

En attente

Une fois installé, le service worker mis à jour retarde son activation jusqu'à ce que le service worker existant ne contrôle plus les clients. Cet état, appelé "en attente", permet au navigateur de s'assurer qu'une seule version de votre service worker s'exécute à la fois.

Si vous avez exécuté la démo mise à jour, vous devriez toujours voir une image de chat, car le worker V2 n'est pas encore activé. Vous pouvez voir le nouveau service worker en attente dans l'onglet "Application" de DevTools :

DevTools affichant le nouveau service worker en attente

Même si vous n'avez qu'un seul onglet ouvert pour la démonstration, actualiser la page ne suffit pas à remplacer la nouvelle version. Cela est dû au fonctionnement de la navigation dans le navigateur. Lorsque vous naviguez, la page actuelle ne disparaît pas tant que les en-têtes de réponse n'ont pas été reçus, et même dans ce cas, la page actuelle peut rester si la réponse comporte un en-tête Content-Disposition. En raison de ce chevauchement, le service worker actuel contrôle toujours un client lors d'une actualisation.

Pour obtenir la mise à jour, fermez tous les onglets ou quittez-les à l'aide du service worker actuel. Lorsque vous accédez à nouveau à la démonstration, vous devriez voir le cheval.

Ce modèle est semblable à celui de Chrome. Les mises à jour de Chrome sont téléchargées en arrière-plan, mais ne sont appliquées qu'après le redémarrage de Chrome. En attendant, vous pouvez continuer à utiliser la version actuelle sans interruption. Cela peut cependant être pénible pendant le développement, mais les outils de développement peuvent vous faciliter la tâche. Nous en parlerons plus loin dans cet article.

Activer

Cette méthode s'exécute une fois que l'ancien service worker a disparu et que le nouveau service worker peut contrôler les clients. C'est le moment idéal pour effectuer des tâches que vous ne pouviez pas effectuer lorsque l'ancien worker était encore utilisé, comme migrer des bases de données et vider des caches.

Dans la démonstration ci-dessus, je maintiens la liste des caches attendus. Dans l'événement activate, je supprime tous les autres caches, ce qui supprime l'ancien cache static-v1.

Si vous transmettez une promesse à event.waitUntil(), les événements fonctionnels (fetch, push, sync, etc.) seront mis en mémoire tampon jusqu'à ce que la promesse soit résolue. Ainsi, lorsque votre événement fetch se déclenche, l'activation est terminée.

Ignorer la phase d'attente

La phase d'attente signifie que vous n'exécutez qu'une seule version de votre site à la fois. Toutefois, si vous n'avez pas besoin de cette fonctionnalité, vous pouvez activer votre nouveau service worker plus tôt en appelant self.skipWaiting().

Votre service worker expulse alors le nœud de calcul actif actuel et s'active dès qu'il passe à l'état "En attente" (ou immédiatement s'il est déjà dans cet état). Cela n'empêche pas votre nœud de calcul d'installer le package, mais de simplement attendre.

Il n'a pas vraiment d'importance quand vous appelez skipWaiting(), tant que c'est pendant ou avant l'attente. Il est assez courant de l'appeler dans l'événement install :

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

Toutefois, vous pouvez l'appeler en tant que résultat d'un postMessage() au service worker. Par exemple, vous souhaitez skipWaiting() après une interaction de l'utilisateur.

Voici une démonstration qui utilise skipWaiting(). Vous devriez voir la photo d'une vache sans avoir à quitter la page. Comme pour clients.claim(), il s'agit d'une course. Vous ne verrez donc la vache que si le nouveau service worker récupère, installe et active l'image avant que la page ne tente de la charger.

Mises à jour manuelles

Comme indiqué précédemment, le navigateur recherche automatiquement les mises à jour après les navigations et les événements fonctionnels, mais vous pouvez également les déclencher manuellement :

navigator.serviceWorker.register('/sw.js').then(reg => {
  // sometime later…
  reg.update();
});

Si vous pensez que l'utilisateur utilisera votre site pendant une longue période sans recharger la page, vous pouvez appeler update() à intervalles réguliers (toutes les heures, par exemple).

Évitez de modifier l'URL de votre script service worker

Si vous avez lu mon article sur les bonnes pratiques de mise en cache, vous pouvez envisager d'attribuer une URL unique à chaque version de votre service worker. Ne faites pas ça ! Il est généralement déconseillé de procéder ainsi pour les services workers. Il suffit de mettre à jour le script à son emplacement actuel.

Vous pouvez rencontrer un problème comme celui-ci :

  1. index.html enregistre sw-v1.js en tant que service worker.
  2. sw-v1.js met en cache et diffuse index.html pour fonctionner en priorité hors connexion.
  3. Vous mettez à jour index.html pour qu'il enregistre votre nouveau sw-v2.js.

Si vous procédez ainsi, l'utilisateur ne recevra jamais sw-v2.js, car sw-v1.js diffuse l'ancienne version de index.html à partir de son cache. Vous vous êtes placé dans une situation où vous devez mettre à jour votre service worker afin de mettre à jour votre service worker. Beurk.

Toutefois, pour la démo ci-dessus, j'ai modifié l'URL du service worker. Cela permet de passer d'une version à l'autre pour la démonstration. Ce n'est pas quelque chose que je ferais en production.

Faciliter le développement

Le cycle de vie du service worker est conçu en gardant l'utilisateur à l'esprit, mais pendant le développement, il est un peu pénible. Heureusement, il existe quelques outils pour vous aider:

Mettre à jour lors de l'actualisation

C'est ma préférée.

Outils de développement affichant &quot;Mettre à jour lors de l&#39;actualisation&quot;

Le cycle de vie est ainsi plus adapté aux développeurs. Chaque navigation :

  1. Récupérez le service worker.
  2. Installez-le en tant que nouvelle version, même s'il est identique au niveau des octets. Cela signifie que votre événement install s'exécute et que vos caches sont mis à jour.
  3. Ignorez la phase d'attente pour que le nouveau service worker s'active.
  4. Parcourez la page.

Vous recevrez ainsi les mises à jour à chaque navigation (y compris lors de l'actualisation) sans avoir à recharger deux fois la page ni à fermer l'onglet.

Ignorer l'attente

Outils de développement affichant &quot;Ignorer l&#39;attente&quot;

Si un nœud de calcul est en attente, vous pouvez appuyer sur "Ignorer l'attente" dans les outils de développement pour le définir immédiatement sur "Actif".

Maj+Actualiser

Si vous forcez l'actualisation de la page (avec le paramètre "Shift-reload"), le service worker est complètement ignoré. Il sera incontrôlé. Cette fonctionnalité figure dans la spécification. Elle fonctionne donc dans d'autres navigateurs compatibles avec les service workers.

Gérer les mises à jour

Le service worker a été conçu pour un Web extensible. L'idée est que nous, en tant que développeurs de navigateurs, reconnaissons que nous ne sommes pas meilleurs en matière de développement Web que les développeurs Web. Par conséquent, nous ne devrions pas fournir d'API hautes levées étroites qui résolvent un problème particulier à l'aide de modèles qui nous plaisent. Nous devrions plutôt vous donner accès à l'intérieur du navigateur et vous permettre de faire ce que vous voulez, de la manière la plus adaptée à vos utilisateurs.

Pour activer autant de modèles que possible, l'ensemble du cycle de mise à jour est observable :

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // the installing worker, or undefined
  reg.waiting; // the waiting worker, or undefined
  reg.active; // the active worker, or undefined

  reg.addEventListener('updatefound', () => {
    // A wild service worker has appeared in reg.installing!
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - the install event has fired, but not yet complete
    // "installed"  - install complete
    // "activating" - the activate event has fired, but not yet complete
    // "activated"  - fully active
    // "redundant"  - discarded. Either failed install, or it's been
    //                replaced by a newer version

    newWorker.addEventListener('statechange', () => {
      // newWorker.state has changed
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // This fires when the service worker controlling this page
  // changes, eg a new worker has skipped waiting and become
  // the new active worker.
});

Le cycle de vie ne change pas

Comme vous pouvez le voir, il est utile de comprendre le cycle de vie des service workers. De cette manière, leurs comportements devraient sembler plus logiques et moins mystérieux. Ces connaissances vous permettront de déployer et de mettre à jour des service workers en toute confiance.