La vie d'un service worker

Il est difficile de savoir ce que font les service workers sans comprendre leur cycle de vie. Leur fonctionnement interne semblera opaque, voire arbitraire. N'oubliez pas que, comme toute autre API de navigateur, les comportements des travailleurs de service sont bien définis et spécifiés, et permettent les applications hors connexion, tout en facilitant les mises à jour sans perturber l'expérience utilisateur.

Avant de vous lancer dans Workbox, il est important de comprendre le cycle de vie des service workers afin que le fonctionnement de Workbox soit clair.

Définir des termes

Avant d'aborder le cycle de vie du service worker, il est utile de définir certains termes concernant son fonctionnement.

Contrôle et champ d'application

L'idée de contrôle est essentielle pour comprendre le fonctionnement des service workers. Une page décrite comme étant contrôlée par un service worker est une page qui permet à un service worker d'intercepter des requêtes réseau en son nom. Le service worker est présent et peut effectuer des tâches pour la page dans un champ d'application donné.

Champ d'application

Le champ d'application d'un service worker est déterminé par son emplacement sur un serveur Web. Si un service worker s'exécute sur une page située à /subdir/index.html et qu'il se trouve à /subdir/sw.js, son champ d'application est /subdir/. Pour voir le concept de champ d'application en action, consultez cet exemple:

  1. Accédez à https://service-worker-scope-viewer.glitch.me/subdir/index.html. Un message s'affiche indiquant qu'aucun service worker ne contrôle la page. Toutefois, cette page enregistre un service worker à partir de https://service-worker-scope-viewer.glitch.me/subdir/sw.js.
  2. Actualisez la page. Étant donné que le service worker a été enregistré et est maintenant actif, il contrôle la page. Un formulaire contenant la portée, l'état actuel et l'URL du service worker s'affiche. Remarque: le fait d'avoir à actualiser la page n'a rien à voir avec le champ d'application, mais plutôt avec le cycle de vie du service worker, qui sera expliqué plus tard.
  3. Accédez maintenant à https://service-worker-scope-viewer.glitch.me/index.html. Même si un service worker a été enregistré sur cette origine, un message indique qu'il n'y a pas de service worker actuel. En effet, cette page ne fait pas partie du champ d'application du service worker enregistré.

Le champ d'application limite les pages que le service worker contrôle. Dans cet exemple, cela signifie que le service worker chargé à partir de /subdir/sw.js ne peut contrôler que les pages situées dans /subdir/ ou son sous-arbre.

C'est ainsi que le champ d'application fonctionne par défaut, mais le champ d'application maximal autorisé peut être remplacé en définissant l'en-tête de réponse Service-Worker-Allowed, ainsi qu'en transmettant une option scope à la méthode register.

Sauf si vous avez une très bonne raison de limiter la portée du service worker à un sous-ensemble d'une origine, chargez un service worker à partir du répertoire racine du serveur Web afin que sa portée soit aussi large que possible, et ne vous souciez pas de l'en-tête Service-Worker-Allowed. C'est beaucoup plus simple pour tout le monde.

Client

Lorsqu'on dit qu'un service worker contrôle une page, il s'agit en réalité d'un client. Un client est une page ouverte dont l'URL relève du champ d'application du service worker. Plus précisément, il s'agit d'instances d'un WindowClient.

Cycle de vie d'un nouveau service worker

Pour qu'un service worker puisse contrôler une page, il doit d'abord être créé. Commençons par voir ce qui se passe lorsqu'un tout nouveau service worker est déployé pour un site Web qui n'en a pas.

Inscription

L'enregistrement est la première étape du cycle de vie du service worker:

<!-- In index.html, for example: -->
<script>
  // Don't register the service worker
  // until the page has fully loaded
  window.addEventListener('load', () => {
    // Is service worker available?
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js').then(() => {
        console.log('Service worker registered!');
      }).catch((error) => {
        console.warn('Error registering service worker:');
        console.warn(error);
      });
    }
  });
</script>

Ce code s'exécute sur le thread principal et effectue les opérations suivantes:

  1. Étant donné que la première visite de l'utilisateur sur un site Web se produit sans service worker enregistré, attendez que la page soit entièrement chargée avant d'en enregistrer un. Cela évite les conflits de bande passante si le service worker précache quoi que ce soit.
  2. Bien que les services workers soient largement compatibles, une vérification rapide permet d'éviter les erreurs dans les navigateurs où ils ne sont pas compatibles.
  3. Lorsque la page est entièrement chargée et si le service worker est compatible, enregistrez /sw.js.

Voici quelques points importants à retenir:

  • Les services workers ne sont disponibles que via HTTPS ou localhost.
  • Si le contenu d'un service worker contient des erreurs de syntaxe, l'enregistrement échoue et le service worker est supprimé.
  • Rappel: Les services workers fonctionnent dans un champ d'application. Ici, le champ d'application correspond à l'intégralité de l'origine, car elle a été chargée à partir du répertoire racine.
  • Lorsque l'enregistrement commence, l'état du service worker est défini sur 'installing'.

Une fois l'enregistrement terminé, l'installation commence.

Installation

Un service worker déclenche son événement install après l'enregistrement. install n'est appelé qu'une seule fois par service worker et ne se déclenchera pas à nouveau tant qu'il n'a pas été mis à jour. Un rappel pour l'événement install peut être enregistré dans le champ d'application du worker avec addEventListener:

// /sw.js
self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v1';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v1'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.bc7b80b7.css',
      '/css/home.fe5d0b23.css',
      '/js/home.d3cc4ba4.js',
      '/js/jquery.43ca4933.js'
    ]);
  }));
});

Cette opération crée une instance Cache et précache les composants. Nous aurons de nombreuses occasions de parler du préchargement plus tard. Concentrez-vous donc sur le rôle de event.waitUntil. event.waitUntil accepte une promesse et attend qu'elle soit résolue. Dans cet exemple, cette promesse effectue deux opérations asynchrones:

  1. Crée une instance Cache nommée 'MyFancyCache_v1'.
  2. Une fois le cache créé, un tableau d'URL d'éléments est préchargé à l'aide de sa méthode addAll asynchrone.

L'installation échoue si la ou les promesses transmises à event.waitUntil sont refusées. Dans ce cas, le service worker est supprimé.

Si les promesses sont résolues, l'installation aboutit, l'état du service worker passe à 'installed', puis s'active.

Activation

Si l'enregistrement et l'installation réussissent, le service worker s'active et son état devient 'activating'. Des tâches peuvent être effectuées lors de l'activation dans l'événement activate du service worker. Une tâche typique dans cet événement consiste à élaguer les anciens caches, mais pour un tout nouveau service worker, cela n'est pas pertinent pour le moment et sera développé lorsque nous parlerons des mises à jour du service worker.

Pour les nouveaux services workers, activate se déclenche immédiatement après la réussite de install. Une fois l'activation terminée, l'état du service worker devient 'activated'. Notez que, par défaut, le nouveau service worker ne commencera à contrôler la page qu'à la prochaine navigation ou actualisation de la page.

Gérer les mises à jour du service worker

Une fois le premier service worker déployé, il devra probablement être mis à jour ultérieurement. Par exemple, une mise à jour peut être nécessaire si des modifications sont apportées à la gestion des requêtes ou à la logique de préchargement.

Quand les mises à jour sont-elles effectuées ?

Les navigateurs recherchent les mises à jour d'un service worker dans les cas suivants:

  • L'utilisateur accède à une page dans le champ d'application du service worker.
  • navigator.serviceWorker.register() est appelé avec une URL différente de celle du service worker actuellement installé. N'allez pas modifier l'URL d'un service worker !
  • navigator.serviceWorker.register() est appelé avec la même URL que le service worker installé, mais avec un champ d'application différent. Encore une fois, évitez cela en conservant la portée à la racine d'une origine, si possible.
  • Lorsque des événements tels que 'push' ou 'sync' ont été déclenchés au cours des dernières 24 heures, mais ne vous inquiétez pas encore de ces événements.

Comment les mises à jour sont-elles effectuées ?

Il est important de savoir quand le navigateur met à jour un service worker, mais aussi comment. En supposant que l'URL ou le champ d'application d'un service worker ne change pas, un service worker actuellement installé ne passe à une nouvelle version que si son contenu a changé.

Les navigateurs détectent les modifications de différentes manières:

  • Toute modification d'octet par octet des scripts demandée par importScripts, le cas échéant.
  • Toute modification apportée au code de niveau supérieur du service worker, qui affecte l'empreinte générée par le navigateur.

Le navigateur effectue une grande partie du travail. Pour vous assurer que le navigateur dispose de tout ce dont il a besoin pour détecter de manière fiable les modifications apportées au contenu d'un service worker, ne demandez pas au cache HTTP de le conserver et ne modifiez pas son nom de fichier. Le navigateur effectue automatiquement des vérifications de mise à jour lorsqu'un utilisateur accède à une nouvelle page dans le champ d'application d'un service worker.

Déclencher manuellement des vérifications de mises à jour

En ce qui concerne les mises à jour, la logique d'enregistrement ne devrait généralement pas changer. Toutefois, il peut y avoir une exception si les sessions sur un site Web sont de longue durée. Cela peut se produire dans les applications monopages où les requêtes de navigation sont rares, car l'application rencontre généralement une requête de navigation au début du cycle de vie de l'application. Dans ce cas, une mise à jour manuelle peut être déclenchée sur le thread principal:

navigator.serviceWorker.ready.then((registration) => {
  registration.update();
});

Pour les sites Web traditionnels ou dans tous les cas où les sessions utilisateur ne sont pas de longue durée, le déclenchement de mises à jour manuelles n'est probablement pas nécessaire.

Installation

Lorsque vous utilisez un bundler pour générer des éléments statiques, ces éléments contiennent des hachages dans leur nom, comme framework.3defa9d2.js. Supposons que certains de ces éléments soient pré-mis en cache pour un accès hors connexion ultérieur. Pour ce faire, vous devez mettre à jour le service worker afin de pré-cacher les composants mis à jour:

self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v2';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v2'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.ced4aef2.css',
      '/css/home.cbe409ad.css',
      '/js/home.109defa4.js',
      '/js/jquery.38caf32d.js'
    ]);
  }));
});

Deux éléments diffèrent de l'exemple d'événement install précédent:

  1. Une nouvelle instance Cache avec une clé 'MyFancyCacheName_v2' est créée.
  2. Les noms des éléments préchargés ont changé.

Notez qu'un service worker mis à jour est installé à côté du précédent. Cela signifie que l'ancien service worker contrôle toujours les pages ouvertes. Après l'installation, le nouveau passe à l'état d'attente jusqu'à ce qu'il soit activé.

Par défaut, un nouveau service worker s'active lorsqu'aucun client n'est contrôlé par l'ancien. Cela se produit lorsque tous les onglets ouverts du site Web concerné sont fermés.

Activation

Lorsqu'un service worker mis à jour est installé et que la phase d'attente se termine, il s'active et l'ancien service worker est supprimé. Une tâche courante à effectuer dans l'événement activate d'un service worker mis à jour consiste à élaguer les anciens caches. Supprimez les anciens caches en obtenant les clés de toutes les instances Cache ouvertes avec caches.keys et en supprimant les caches qui ne figurent pas dans une liste d'autorisation définie avec caches.delete:

self.addEventListener('activate', (event) => {
  // Specify allowed cache keys
  const cacheAllowList = ['MyFancyCacheName_v2'];

  // Get all the currently active `Cache` instances.
  event.waitUntil(caches.keys().then((keys) => {
    // Delete all caches that aren't in the allow list:
    return Promise.all(keys.map((key) => {
      if (!cacheAllowList.includes(key)) {
        return caches.delete(key);
      }
    }));
  }));
});

Les anciens caches ne sont pas automatiquement nettoyés. Nous devons le faire nous-mêmes, sinon nous risquons de dépasser les quotas de stockage. Étant donné que 'MyFancyCacheName_v1' du premier service worker est obsolète, la liste d'autorisation du cache est mise à jour pour spécifier 'MyFancyCacheName_v2', ce qui supprime les caches portant un nom différent.

L'événement activate se termine une fois l'ancien cache supprimé. À ce stade, le nouveau service worker prend le contrôle de la page et remplace finalement l'ancien.

Le cycle de vie continue

Que Workbox soit utilisé pour gérer le déploiement et les mises à jour des service workers, ou que l'API Service Worker soit utilisée directement, 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 que mystérieux.

Pour en savoir plus sur ce sujet, consultez cet article de Jake Archibald. Le cycle de vie des services est très complexe, mais il est possible de le comprendre, et ces connaissances vous seront très utiles lorsque vous utiliserez Workbox.