Cycle de vie d'un service worker

Jake Archibald
Jake Archibald

Le cycle de vie du service worker est sa partie la plus compliquée. Si vous ne savez pas ce qu'elle essaie de faire ni quels sont les avantages, vous pouvez avoir l'impression qu'elle vous affronte. Mais 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 plupart de ce que vous devez savoir.

L'intent

L'objectif du cycle de vie est le suivant:

  • Donnez la priorité à l'activité hors connexion.
  • Permettez à un nouveau service worker de se préparer sans perturber le service worker actuel.
  • Assurez-vous qu'une page couverte est contrôlée par le même service worker (ou 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 être exécutées en même temps. Parfois, ce n'est pas grave, mais s'il s'agit de stockage, vous pouvez facilement vous retrouver avec deux onglets qui ont 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, ou pire, une perte de données.

Le premier service worker

En bref :

  • L'événement install est le premier qu'un service worker reçoit. Il ne se produit qu'une seule fois.
  • Une promesse transmise à installEvent.waitUntil() indique la durée ainsi que 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 que l'installation n'est pas terminée et que l'état devient "actif".
  • Par défaut, l'exploration d'une page ne passe pas par un service worker, sauf si la demande de page elle-même est passée par un service worker. Vous devez donc actualiser la page pour voir les effets du service worker.
  • clients.claim() peut ignorer 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 l'image d'un 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 l'image d'un chat et la diffuse chaque fois qu'une requête est envoyée à /dog.svg. Toutefois, si vous exécutez l'exemple ci-dessus, un chien s'affiche la première fois que vous chargez la page. Appuyez sur "Actualiser" pour voir le chat.

Champ d'application et contrôle

Le champ d'application par défaut de l'enregistrement d'un service worker est ./ par rapport à l'URL du 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 couverts. 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 via une instance de service worker.

Télécharger, analyser et exécuter

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

Les outils pour les développeurs Chrome affichent l'erreur dans la console et dans la section "Service worker" de l'onglet "Application" :

Erreur affichée dans l&#39;onglet &quot;Outils de développement&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 worker 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 reçoit son propre événement install. Nous aborderons les mises à jour plus en détail ultérieurement.

L'événement install vous offre l'opportunité de mettre en cache tout ce dont vous avez besoin avant de pouvoir contrôler les clients. La promesse que vous transmettez à event.waitUntil() permet au navigateur de savoir 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 rejette le service worker. Il ne contrôlera jamais les clients. Cela signifie que nous ne pouvons pas compter sur la présence de cat.svg dans le cache pour 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 des événements fonctionnels tels que push et sync, un événement activate est généré. Toutefois, cela ne signifie pas que la page qui a appelé .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, il ne traite pas la requête et vous voyez toujours l'image du chien. La valeur 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 une deuxième fois la démonstration (en d'autres termes, actualisez la page), il sera contrôlé. 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 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 ce paramètre est sensible au facteur 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 que via le réseau, clients.claim() peut s'avérer problématique, car il finit par contrôler certains clients qui se chargent sans cette autorisation.

Mettre à jour le service worker

En bref :

  • Une mise à jour est déclenchée dans les cas suivants :
    • Navigation vers une page couverte.
    • Un événement fonctionnel tel que push et sync, sauf si une vérification de mise à jour a été effectuée au cours des dernières 24 heures
    • Appeler .register() uniquement si l'URL du service worker a changé. Toutefois, vous devez éviter de modifier l'URL de 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 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 est différent de celui dont dispose déjà le navigateur. (Nous les étendons également aux scripts/modules importés.)
  • 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 non-ok (par exemple, 404), échoue à l'analyse, génère une erreur pendant l'exécution ou refuse pendant l'installation, le nouveau nœud de calcul est ignoré, mais le nœud actuel reste actif.
  • Une fois l'installation terminée, le worker mis à jour est wait jusqu'à ce que le worker existant ne contrôle aucun client. Notez que les clients se chevauchent lors d'une actualisation.
  • self.skipWaiting() évite l'attente, ce qui signifie que le service worker s'active dès la fin de l'installation.

Supposons que nous modifiions notre script de service worker pour répondre avec une image représentant un cheval plutôt qu'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. Vous devriez toujours voir une image de chat. Voici pourquoi...

Installer

Notez que j'ai remplacé le nom du cache static-v1 par static-v2. Je peux donc configurer le nouveau cache sans écraser le cache actuel, que l'ancien service worker utilise encore.

Ce modèle crée des caches spécifiques à chaque version, semblables aux éléments qu'une application native contient avec son exécutable. Vous pouvez également avoir des caches qui ne sont pas spécifiques à une version, comme avatars.

Waiting

Une fois l'installation terminée, le service worker mis à jour retarde l'activation jusqu'à ce que le service worker existant ne contrôle plus les clients. Cet état, appelé "waiting" (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 une photo d'un chat, car le worker V2 n'a pas encore été activé. Vous pouvez voir le nouveau service worker en attente dans l'onglet "Application" des outils de développement:

Outils de développement montrant un nouveau service worker en attente

Même si un seul onglet est ouvert pour la démo, il ne suffit pas d'actualiser la page pour laisser la nouvelle version prendre le contrôle. 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. 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 revenez à la démonstration, vous devriez voir le cheval.

Ce schéma est similaire à celui des mises à jour de Chrome. Les mises à jour de Chrome sont téléchargées en arrière-plan, mais elles ne s'appliquent qu'au redémarrage du navigateur. En attendant, vous pouvez continuer à utiliser la version actuelle sans interruption. Cela peut toutefois s'avérer pénible pendant le développement, mais les outils de développement proposent des solutions plus simples, que nous aborderons plus loin dans cet article.

Activation

Ce script se déclenche une fois que l'ancien service worker a disparu et que le nouveau peut contrôler les clients. C'est le moment idéal pour effectuer des actions que vous ne pouviez pas faire pendant l'utilisation de l'ancien nœud de calcul, comme migrer des bases de données et vider les caches.

Dans la démonstration ci-dessus, je maintiens la liste des caches auxquels je m'attendais et, dans l'événement activate, je me débarrasse de 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.) sont 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.

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

Ainsi, votre service worker rejette le worker actif actuel et s'active dès qu'il entre en phase d'attente (ou immédiatement s'il se trouve déjà dans la phase d'attente). Votre nœud de calcul ne passe pas d'installation, il attend juste.

Peu importe lorsque vous appelez skipWaiting(), tant que l'appel est 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'un postMessage() pour le service worker. Par 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 clients.claim(), il s'agit d'une course. La vache ne s'affichera que si le nouveau service worker récupère, installe et active l'image 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 actualiser la page, vous pouvez appeler update() à intervalles réguliers (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 pas faire ! Cette pratique est généralement déconseillée pour les service workers. Il suffit de mettre à jour le script à son emplacement actuel.

Cela peut vous amener à 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 de fonctionner en priorité hors connexion.
  3. Mettez à jour index.html pour qu'elle enregistre votre nouvelle sw-v2.js.

Dans ce cas, l'utilisateur n'obtient jamais sw-v2.js, car sw-v1.js diffuse l'ancienne version de index.html à partir de son cache. Vous vous êtes mis à jour un service worker afin de pouvoir le mettre à jour. 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. Ce n'est pas quelque chose que je ferais en production.

Faciliter le développement

Le cycle de vie des service workers est conçu en tenant compte de l'utilisateur, mais pendant le développement, cela peut s'avérer un peu pénible. Heureusement, il existe quelques outils pour vous aider:

Mettre à jour lors de l'actualisation

C'est mon préféré.

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

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

  1. Récupérez à nouveau le service worker.
  2. Installez-la en tant que nouvelle version, même si elle est identique à l'octet, ce qui signifie que votre événement install s'exécute et que vos caches sont mis à jour.
  3. Ignorez la phase d'attente afin que le nouveau service worker s'active.
  4. Parcourez la page.

Cela signifie que vous recevrez vos mises à jour à chaque navigation (y compris lors de l'actualisation) sans avoir à actualiser deux fois l'onglet ni à le fermer.

Ignorer l'attente

Affichage des outils de développement avec l&#39;option &quot;Ignorer l&#39;attente&quot;

Si un worker est en attente, vous pouvez appuyer sur "Ignorer l'attente " dans les outils de développement pour l'activer immédiatement.

Maj et actualisation

Si vous forcez l'actualisation de la page (Maj+reload), le service worker contourne l'ensemble du service worker. Elle ne sera pas contrôlée. Cette fonctionnalité est incluse dans les spécifications. Elle est donc compatible avec d'autres navigateurs compatibles avec les workers.

Gérer les mises à jour

Le service worker a été conçu pour le Web étendu. L'idée est qu'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 devons pas fournir d'API de haut niveau étroites qui résolvent un problème particulier à l'aide de modèles que nous apprécions. Nous devons plutôt vous donner accès à l'instinct du navigateur et vous laisser le faire comme vous le souhaitez, de la manière la plus adaptée à vos utilisateurs.

Ainsi, 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 dure

Comme vous pouvez le voir, il est utile de comprendre le cycle de vie des service workers. Avec cette compréhension, leur comportement devrait vous sembler plus logique et moins mystérieux. Ces connaissances vous permettront de déployer et de mettre à jour vos service workers en toute confiance.