La vie d'un service worker

Il est difficile de savoir ce que font les service workers sans comprendre leur cycle de vie. Leurs rouages internes paraîtront opaques, voire arbitraires. Comme pour toute autre API de navigateur, il est utile de rappeler que le comportement des service workers est bien défini et spécifié, et qu'il rend possible les applications hors connexion, tout en facilitant les mises à jour sans perturber l'expérience utilisateur.

Avant de vous plonger dans Workbox, il est important de comprendre le cycle de vie des service workers afin de déterminer sa pertinence.

Définition des termes

Avant d'aborder le cycle de vie des service workers, il est utile de définir quelques termes décrivant son fonctionnement.

Contrôle et portée

Le concept de contrôle est crucial pour comprendre le fonctionnement des service workers. Une page décrite comme contrôlée par un service worker est une page qui lui permet d'intercepter les requêtes réseau en son nom. Il est présent et capable d'effectuer des opérations sur la page dans un champ d'application donné.

Définition du 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 dans l'emplacement /subdir/index.html et situé dans l'emplacement /subdir/sw.js, le champ d'application du service worker est /subdir/. Pour voir le concept de portée en action, consultez cet exemple:

  1. Accédez à https://service-worker-scope-viewer.glitch.me/subdir/index.html. Le message qui s'affiche indique 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. Comme le service worker a été enregistré et est désormais actif, il contrôle la page. Un formulaire contenant le champ d'application, l'état actuel et l'URL du service worker est visible. Remarque: devoir 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é ultérieurement.
  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'existe aucun service worker actuel. Cela est dû au fait que cette page n'entre pas dans le champ d'application du service worker enregistré.

Le niveau d'accès limite les pages contrôlées par le service worker. Dans cet exemple, cela signifie que le service worker chargé depuis /subdir/sw.js ne peut contrôler que les pages situées dans /subdir/ ou dans sa sous-arborescence.

La définition ci-dessus présente le fonctionnement par défaut de la portée, mais vous pouvez ignorer le champ d'application maximal autorisé en définissant l'en-tête de réponse Service-Worker-Allowed et en transmettant une option scope à la méthode register.

À moins qu'il n'y ait une excellente raison de limiter le champ d'application du service worker à un sous-ensemble d'une origine, chargez un service worker à partir du répertoire racine du serveur Web afin que son champ d'application soit aussi étendu que possible, et ne vous souciez pas de l'en-tête Service-Worker-Allowed. C'est beaucoup plus simple pour tout le monde de cette façon.

Nom du client

Quand on dit qu'un service worker contrôle une page, c'est vraiment un client. Un client est une page ouverte dont l'URL relève du service worker. Plus précisément, il s'agit d'instances de WindowClient.

Cycle de vie d'un nouveau service worker

Pour qu'un service worker puisse contrôler une page, celle-ci doit d'abord être créée. Commençons par ce qui se passe lorsqu'un tout nouveau service worker est déployé pour un site Web sans service worker actif.

Inscription

L'enregistrement est l'étape initiale du cycle de vie d'un 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 d'un utilisateur sur un site Web a lieu sans service worker enregistré, attendez que la page soit entièrement chargée avant d'en enregistrer une. Cela permet d'éviter les conflits dans la bande passante si le service worker effectue une mise en cache préalable.
  2. Bien que le service worker soit compatible, une vérification rapide permet d'éviter les erreurs dans les navigateurs où la compatibilité n'est pas assurée.
  3. Une fois la page entièrement chargée et si le service worker est compatible, enregistrez /sw.js.

Voici quelques points essentiels à retenir:

  • Les service 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 service workers interviennent dans un périmètre. Ici, le champ d'application correspond à l'intégralité de l'origine, telle qu'elle a été chargée à partir du répertoire racine.
  • Au début de l'enregistrement, 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 plus tant qu'il n'aura pas été mis à jour. Un rappel pour l'événement install peut être enregistré dans le champ d'application du nœud de calcul 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 met en cache les éléments en amont. Nous aurons de nombreuses occasions de parler de la mise en cache préliminaire ultérieurement. Concentrons-nous donc sur le rôle de event.waitUntil. event.waitUntil accepte une promesse et attend que cette promesse 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 mis en cache en amont à l'aide de sa méthode addAll asynchrone.

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

Si les promesses sont resolve, l'installation réussit et l'état du service worker passe à 'installed', puis est activé.

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, ce n'est pas pertinent pour le moment. Nous en parlerons plus en détail lorsque nous parlerons des mises à jour des service workers.

Pour les nouveaux service workers, activate se déclenche immédiatement après la réussite de l'opération 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 des service workers

Une fois le premier service worker déployé, il devra probablement être mis à jour ultérieurement. Par exemple, une mise à jour peut être requise en cas de modification de la logique de traitement des requêtes ou de mise en cache préalable.

En cas de mise à jour

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

  • L'utilisateur accède à une page comprise 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é, mais ne la modifiez pas.
  • navigator.serviceWorker.register() est appelé avec la même URL que le service worker installé, mais avec un champ d'application différent. Là encore, évitez ce problème en gardant le champ d'application à 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 pour le moment.

Fonctionnement des mises à jour

Il est important de savoir quand le navigateur met à jour un service worker, mais il est important de savoir comment procéder. En supposant que l'URL ou le champ d'application d'un service worker n'ont pas changé, un service worker actuellement installé n'est mis à jour vers une nouvelle version que si son contenu a changé.

Les navigateurs détectent les modifications de plusieurs façons:

  • Toutes les modifications octet par octet des scripts demandés par importScripts, le cas échéant.
  • Toute modification du code de premier niveau du service worker, qui affecte l'empreinte générée par le navigateur.

Le navigateur fait le gros du travail ici. 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, n'indiquez 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 lorsque l'utilisateur navigue vers une nouvelle page dans le champ d'application d'un service worker.

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

Concernant les mises à jour, la logique d'enregistrement ne devrait généralement pas changer. Pourtant, il est possible que les sessions sur un site Web durent longtemps. 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 de son cycle de vie. Dans de telles situations, 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 les cas où les sessions utilisateur ne durent pas longtemps, le déclenchement des mises à jour manuelles n'est probablement pas nécessaire.

Installation

Lorsque vous utilisez un bundler pour générer des éléments statiques, leur nom contient des hachages tels que framework.3defa9d2.js. Supposons que certains de ces éléments soient mis en pré-cache pour être accessibles hors connexion ultérieurement. Cela nécessiterait une mise à jour du service worker pour effectuer une mise en cache préalable des éléments 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 choses sont différentes du premier exemple d'événement install présenté précédemment:

  1. Une instance Cache avec une clé de 'MyFancyCacheName_v2' est créée.
  2. Les noms des éléments mis en pré-cache ont changé.

Notez qu'un service worker mis à jour est installé en même temps que le précédent. Cela signifie que l'ancien service worker a toujours le contrôle des pages ouvertes et qu'après l'installation, le nouveau passe en état d'attente jusqu'à son activation.

Par défaut, un nouveau service worker est activé lorsqu'aucun client n'est contrôlé par l'ancien. Cela se produit lorsque tous les onglets ouverts du site Web correspondant 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 se nettoient pas. Nous devons le faire nous-mêmes, sinon nous risquons de dépasser les quotas de stockage. Comme 'MyFancyCacheName_v1' du premier service worker est obsolète, la liste d'autorisation du cache est mise à jour pour spécifier 'MyFancyCacheName_v2', qui supprime les caches portant un nom différent.

L'événement activate s'arrêtera après la suppression de l'ancien cache. À ce stade, le nouveau service worker prendra le contrôle de la page et remplacera l'ancienne.

Le cycle de vie dure

Que vous utilisiez Workbox pour gérer le déploiement et les mises à jour d'un service worker ou que l'API Service Worker soit utilisée directement, vous avez tout intérêt à comprendre le cycle de vie d'un service worker. Sachant cela, les comportements des service workers devraient sembler plus logiques que mystérieux.

Si vous souhaitez approfondir le sujet, consultez cet article de Jake Archibald. La façon dont l'ensemble du cycle de vie du service présente des différences est très nuancée, mais cela est facile à comprendre, et ces connaissances vous seront très utiles lorsque vous utiliserez Workbox.