Cycle de vie d'un service worker

Jake Archibald
Jake Archibald

Le cycle de vie du service worker est la phase la plus complexe. Si vous ne savez pas ce que cette solution essaie de faire ni quels en sont les avantages, vous pouvez avoir l'impression de vous battre. 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'une présentation détaillée, mais les puces au début de chaque section couvrent la majeure partie de ce que vous devez savoir.

L'intention

L'objectif du cycle de vie est le suivant:

  • Favorisez l'utilisation hors connexion.
  • Permet à un nouveau service worker de se préparer sans perturber le service actuel.
  • Assurez-vous qu'une page concernée est toujours contrôlée par le même service worker (ou par aucun service worker).
  • Assurez-vous qu'une seule version de votre site est exécutée à la fois.

Ce dernier point est assez important. Sans service workers, les utilisateurs peuvent charger un onglet sur votre site, puis en ouvrir un autre par la suite. Par conséquent, deux versions de votre site peuvent fonctionner en même temps. Parfois, ce n'est pas grave, mais si vous avez affaire à du stockage, vous pouvez facilement vous retrouver avec deux onglets ayant des opinions très différentes sur la façon dont leur stockage partagé doit être géré. Cela peut entraîner des erreurs, ou pire, une perte de données.

Le 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 le succès 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é son installation et qu'il ne sera pas "actif".
  • Par défaut, les extractions d'une page ne passent pas par un service worker, sauf si la requête de page elle-même passe par un service worker. Vous devez 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 ceci en 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>

Elle 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 de 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, vous verrez un chien la première fois que vous chargerez la page. Appuyez sur "Actualiser". Le chat apparaît.

Champ d'application et contrôle

Le champ d'application par défaut d'un enregistrement de service worker est ./ par rapport à l'URL de script. Cela signifie que si vous enregistrez un service worker sur //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 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 est nul ou une instance de service worker.

Télécharger, analyser et exécuter

Le tout premier service worker est téléchargé lorsque vous appelez .register(). Si votre script ne parvient pas à télécharger ou à analyser le script, ou s'il génère une erreur 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 service worker de l'onglet de l'application:

Erreur affichée dans l&#39;onglet &quot;Outils de développement&quot; de service worker

Installer

Le premier événement reçu par un service worker est install. Elle est déclenchée dès que le worker s'exécute et n'est appelée 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 recevra 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 pour pouvoir contrôler les clients. La promesse que vous transmettez à event.waitUntil() permet au navigateur de savoir quand votre 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 compter sur la présence de cat.svg dans le cache de nos événements fetch. C'est une dépendance.

Activation

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 recevez un événement activate. Toutefois, cela ne signifie pas que la page intitulée .register() sera contrôlée.

La première fois que vous chargez la démonstration, même si dog.svg est demandé longtemps après l'activation du service worker, celui-ci ne la traite pas, et l'image du chien s'affiche toujours. Le paramètre par défaut est la cohérence. Si votre page se charge sans service worker, ses sous-ressources ne le seront pas non plus. Si vous chargez la démonstration une deuxième fois (en d'autres termes, si vous actualisez la page), l'activité est contrôlée. La page et l'image passeront par des événements fetch, et vous verrez un chat à la place.

clients.claim

Vous pouvez prendre le contrôle de 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 c'est un sujet sensible au facteur temps. Vous ne verrez un chat que si le service worker s'active et que clients.claim() prend effet avant la tentative de chargement de l'image.

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 dans les cas suivants :
    • 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
    • N'appelez .register() que si l'URL du service worker a changé. Toutefois, vous devez éviter de modifier l'URL de travail.
  • 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 la récupération des ressources chargées dans un service worker via importScripts(). Vous pouvez ignorer ce comportement par défaut en définissant l'option updateViaCache lors de l'enregistrement de votre service worker.
  • Votre service worker est considéré comme mis à jour s'il diffère de celui dont dispose déjà le navigateur. (Nous étendons cela aux scripts/modules importés également.)
  • Le service worker mis à jour est lancé en même temps que le service existant et obtient 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 installé, le nœud de calcul mis à jour wait jusqu'à ce qu'il ne contrôle 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'));
  }
});

Regardez une démonstration des fonctionnalités ci-dessus. L'image d'un 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 du cache actuel, que l'ancien service worker utilise encore.

Ce modèle crée des caches spécifiques à chaque version, comme les éléments qu'une application native pourrait regrouper avec son exécutable. Il se peut également que vous ayez des caches qui ne sont pas spécifiques à la version, tels que 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émonstration mise à jour, vous devriez toujours voir l'image d'un chat, car le worker V2 n'a pas encore été activé. Le nouveau service worker est en attente dans l'onglet "Application" des outils de développement:

Les outils de développement affichent un nouveau service worker en attente

Même si vous n'avez ouvert qu'un seul onglet pour la démonstration, il ne suffit pas d'actualiser la page pour que la nouvelle version prenne le relais. 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. Ensuite, lorsque vous accédez à nouveau à la démonstration, vous devriez voir le cheval.

Ce schéma est semblable à celui utilisé pour les mises à jour de Chrome. Les mises à jour de Chrome se téléchargent en arrière-plan, mais elles ne s'appliquent qu'au redémarrage de Chrome. En attendant, vous pouvez continuer à utiliser la version actuelle sans interruption. Cette opération peut toutefois poser problème lors du développement, mais les outils de développement peuvent vous faciliter la tâche. Nous y reviendrons plus tard dans cet article.

Activation

Celui-ci se déclenche une fois que l'ancien service worker a disparu et que le nouveau service peut contrôler les clients. C'est le moment idéal pour effectuer des tâches que vous ne pouviez pas faire quand l'ancien nœud de calcul était encore utilisé, comme migrer des bases de données et effacer les caches.

Dans la démonstration ci-dessus, je maintiens la liste des caches attendus. Dans l'événement activate, je supprime tous les autres, 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 complètement terminée.

Évitez 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 faire en sorte que votre nouveau service worker s'active plus tôt en appelant self.skipWaiting().

Votre service worker exclura alors le nœud de calcul actif actuel et s'activera dès qu'il se trouvera dans la phase d'attente (ou immédiatement s'il se trouve déjà dans cette phase). Cela n'empêche pas votre nœud de calcul d'ignorer l'installation, simplement d'attendre.

Peu importe quand vous appelez skipWaiting(), du moment que l'appel a été effectué pendant ou avant l'appel. 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'une opération postMessage() sur le service worker. Comme dans cet exemple, vous souhaitez skipWaiting() après une interaction 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 race. Vous ne verrez donc la vache que si le nouveau service worker récupère, installe et s'active avant que la page ne tente de charger l'image.

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 pas faire ! Il s'agit généralement d'une mauvaise pratique pour les service workers. Il suffit de mettre à jour le script à son emplacement actuel.

Cela peut vous amener à 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 afin qu'il fonctionne d'abord hors connexion.
  3. Mettez à jour index.html pour qu'il enregistre votre nouveau sw-v2.js.

Si vous procédez ainsi, l'utilisateur n'obtiendra 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. Eh !

Toutefois, pour la démonstration ci-dessus, j'ai modifié l'URL du service worker. Ainsi, pour les besoins de la démonstration, vous pouvez passer d'une version à l'autre. Je ne le ferais pas en production.

Faciliter le développement

Le cycle de vie d'un service worker a été conçu en tenant compte de l'utilisateur, mais lors du développement, il peut être pénible. Heureusement, il existe quelques outils pour vous aider:

Mettre à jour lors de l'actualisation

Celle-ci est ma préférée.

Les outils de développement affichent le message &quot;Mettre à jour lors de l&#39;actualisation&quot;

Cela modifie le cycle de vie afin qu'il soit adapté aux développeurs. Chaque navigation:

  1. Récupérez de nouveau le service worker.
  2. Installez-la en tant que nouvelle version, même si les octets sont identiques, ce qui 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. Parcourir la page

Cela signifie que vous recevez vos mises à jour à chaque actualisation (y compris lors d'une actualisation) sans avoir à actualiser deux fois ni à fermer l'onglet.

Ignorer l'attente

Les outils de développement affichent le message &quot;Ignorer l&#39;attente&quot;

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

Maj/Actualiser

Si vous forcez l'actualisation de la page (avec le paramètre "shift-reload"), le service worker est complètement ignoré. Ce sera incontrôlable. Cette fonctionnalité est indiquée dans les spécifications. Elle peut donc être utilisée dans d'autres navigateurs compatibles avec les service workers.

Gérer les mises à jour

Le service worker a été conçu pour le Web extensible. L'idée est qu'en tant que développeurs de navigateurs, nous reconnaissons que nous ne sommes pas meilleurs en matière de développement Web que les développeurs Web. Par conséquent, nous ne devons pas fournir d'API restreintes de haut niveau qui résolvent un problème particulier à l'aide de modèles que nous apprécions, mais plutôt vous donner accès à l'essentiel du navigateur et vous laisser faire comme vous le souhaitez, de la manière qui convient le mieux à vos utilisateurs.

Ainsi, pour activer autant de motifs 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 gagner en confiance lorsque vous déployez et mettez à jour vos service workers.