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. Mais une fois que vous savez comment cela fonctionne, vous pouvez proposer aux utilisateurs des mises à jour fluides et discrètes, 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'application orientée hors connexion possible
  • Permet à un nouveau service worker de se préparer sans perturber le service worker 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 s'exécute à 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 reçoit pas d'événements tels que fetch et push tant qu'il n'a pas terminé son installation et qu'il n'est pas devenu "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 récupérations passent par le service worker de portée. 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 &quot;DevTools&quot; 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 reviendrai plus tard sur les mises à jour.

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 signifie que l'installation a échoué et que le navigateur supprime 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 des pages différemment qu'elles ne le feraient via le réseau, clients.claim() peut être problématique, car votre service worker finit par contrôler certains clients qui se sont chargés sans lui.

Mettre à jour le service worker

En bref :

  • Une mise à jour est déclenchée si l'un des événements suivants se produit :
    • Navigation vers une page dans le champ d'application.
    • Des événements fonctionnels tels que push et sync, sauf si une vérification de mise à jour a été effectuée au cours des 24 dernières 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 worker possède un code d'état non OK (par exemple, 404), ne parvient pas à être analysé, génère une erreur lors de l'exécution ou est rejeté lors de l'installation, il est supprimé, mais le worker 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 que son installation est terminée.

Supposons que nous ayons modifié notre script de service worker pour qu'il réponde avec une image de cheval plutôt qu'une image de 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 les éléments de l'actuel, que l'ancien service worker utilise toujours.

Ce modèle crée 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 est appelé "attente". C'est ainsi que le navigateur s'assure 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 pour que la nouvelle version prenne le relais. Cela est dû au fonctionnement des navigations dans les navigateurs. 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. 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 ou quittez tous les onglets utilisant le service worker actuel. Ensuite, 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. Cependant, cette tâche est fastidieuse pendant le développement. Heureusement, les outils de développement peuvent vous aider, comme je vais le décrire 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 gère une liste de caches que je m'attends à trouver, et dans l'événement activate, je me débarrasse des autres, ce qui supprime l'ancien cache static-v1.

Si vous transmettez une promesse à event.waitUntil(), elle met en mémoire tampon les événements fonctionnels (fetch, push, sync, etc.) jusqu'à ce que la promesse soit résolue. Ainsi, lorsque votre événement fetch se déclenche, l'activation est complètement 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(). Une image d'une vache doit s'afficher sans que vous ayez à 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 prévoyez que l'utilisateur utilisera votre site pendant une longue période sans le recharger, vous pouvez appeler update() à un intervalle (par exemple, toutes les heures).

Éviter de modifier l'URL de votre script de 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 cela ! 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 comme indiqué ci-dessus, l'utilisateur ne reçoit jamais sw-v2.js, car sw-v1.js diffuse l'ancienne version de index.html à partir de son cache. Vous vous trouvez dans une situation où vous devez mettre à jour votre service worker pour 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.

Simplifier 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 (actualisation avec la touche Maj), 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 dans le cadre du Web extensible. En tant que développeurs de navigateurs, nous reconnaissons que nous ne sommes pas meilleurs en 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 continue

Comme vous pouvez le constater, il est utile de comprendre le cycle de vie des service workers. Avec cette compréhension, les comportements des service workers 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.