État d'esprit des service workers

Comment penser aux services workers

Les travailleurs de service sont puissants et méritent absolument d'être appris. Elles vous permettent d'offrir à vos utilisateurs une expérience entièrement nouvelle. Votre site peut se charger instantanément. Elle peut fonctionner hors connexion. Elle peut être installée en tant qu'application spécifique à une plate-forme et offrir une expérience tout aussi soignée, mais avec la couverture et la liberté du Web.

Cependant, les services workers ne ressemblent à rien de ce à quoi la plupart des développeurs Web sont habitués. Elles nécessitent une courbe d'apprentissage abrupte et comportent quelques écueils à surveiller.

Google Developers et moi-même avons récemment collaboré sur un projet : Service Workers, un jeu sans frais qui permet de comprendre les service workers. Lors de la création et de la gestion des tenants et aboutissants complexes des service workers, je suis tombé sur quelques écueils. Ce qui m'a le plus aidé, c'est d'imaginer quelques métaphores descriptives. Dans cet article, nous allons explorer ces modèles mentaux et nous familiariser avec les traits paradoxaux qui font des services workers à la fois des éléments difficiles et géniaux.

Mêmes, mais différents

Lorsque vous codez votre service worker, de nombreuses choses vous sembleront familières. Vous pouvez utiliser vos nouvelles fonctionnalités de langage JavaScript préférées. Vous écoutez les événements de cycle de vie comme vous le feriez avec les événements d'UI. Vous gérez le flux de contrôle avec des promesses comme vous en avez l'habitude.

Mais d'autres comportements de service worker vous laissent perplexe. En particulier lorsque vous actualisez la page et que vous ne voyez pas vos modifications de code appliquées.

Un nouveau calque

Normalement, lorsque vous créez un site, vous n'avez que deux couches à prendre en compte: le client et le serveur. Le service worker est une toute nouvelle couche qui se trouve au milieu.

Un service worker sert de couche intermédiaire entre le client et le serveur.

Considérez votre service worker comme une sorte d'extension de navigateur que votre site peut installer dans le navigateur de votre utilisateur. Une fois installé, le service worker étend le navigateur de votre site avec une couche intermédiaire puissante. Cette couche de service worker peut intercepter et gérer toutes les requêtes envoyées par votre site.

La couche du service worker a son propre cycle de vie, qui est indépendant de l'onglet du navigateur. Une simple actualisation de la page ne suffit pas pour mettre à jour un service worker, tout comme vous ne vous attendriez pas à ce qu'une actualisation de la page mette à jour le code déployé sur un serveur. Chaque couche possède ses propres règles de mise à jour.

Dans le jeu Service Workers, nous vous expliquons en détail le cycle de vie des service workers et vous donnons de nombreuses occasions de les utiliser.

Puissant, mais limité

Un service worker sur votre site vous offre des avantages incroyables. Votre site peut:

  • fonctionner parfaitement, même lorsque l'utilisateur est hors connexion
  • bénéficiez d'une amélioration considérable des performances grâce au mise en cache ;
  • utiliser des notifications push ;
  • être installée en tant que PWA ;

Bien que les service workers puissent effectuer de nombreuses tâches, ils sont limités par leur conception. Ils ne peuvent rien faire de synchrone ni dans le même thread que votre site. Vous n'aurez donc pas accès aux éléments suivants:

  • localStorage
  • le DOM
  • la fenêtre

La bonne nouvelle est qu'il existe plusieurs façons pour votre page de communiquer avec son service worker, y compris postMessage direct, des canaux de messages en tête-à-tête et des canaux de diffusion de type "un à plusieurs".

Longue durée, mais courte durée

Un service worker actif continue de fonctionner même après qu'un utilisateur a quitté votre site ou fermé l'onglet. Le navigateur conserve ce service worker afin qu'il soit prêt la prochaine fois que l'utilisateur reviendra sur votre site. Avant la toute première requête, le service worker a la possibilité de l'intercepter et de prendre le contrôle de la page. C'est ce qui permet à un site de fonctionner hors connexion : le service worker peut diffuser une version mise en cache de la page elle-même, même si l'utilisateur n'est pas connecté à Internet.

Dans Service Workers, nous visualisons ce concept avec Kolohe (un service worker amical) qui intercepte et gère les requêtes.

Arrêté

Bien que les services workers semblent immortels, ils peuvent être arrêtés presque à tout moment. Le navigateur ne veut pas gaspiller de ressources sur un service worker qui ne fait rien pour le moment. L'arrêt n'est pas la même chose que l'arrêt : le service worker reste installé et activé. Il est simplement mis en veille. La prochaine fois qu'il en aura besoin (par exemple, pour traiter une requête), le navigateur le réactivera.

waitUntil

Étant donné la possibilité constante d'être mis en veille, votre service worker doit pouvoir indiquer au navigateur quand il effectue une tâche importante et qu'il n'a pas envie de faire la sieste. C'est là que event.waitUntil() entre en jeu. Cette méthode étend le cycle de vie dans lequel elle est utilisée, ce qui l'empêche d'être arrêtée et de passer à la phase suivante de son cycle de vie tant que nous ne sommes pas prêts. Cela nous laisse le temps de configurer des caches, de récupérer des ressources sur le réseau, etc.

Cet exemple indique au navigateur que l'installation de notre service worker n'est pas terminée tant que le cache assets n'a pas été créé et rempli avec l'image d'une épée:

self.addEventListener("install", event => {
  event.waitUntil(
    caches.open("assets").then(cache => {
      return cache.addAll(["/weapons/sword/blade.png"]);
    })
  );
});

Attention à l'état global

Lorsque ce démarrage/arrêt se produit, le champ d'application global du service worker est réinitialisé. Veillez donc à ne pas utiliser d'état global dans votre service worker, sinon vous serez déçu la prochaine fois qu'il se réveillera et qu'il aura un état différent de celui qu'il attendait.

Prenons l'exemple suivant qui utilise un état global:

const favoriteNumber = Math.random();
let hasHandledARequest = false;

self.addEventListener("fetch", event => {
  console.log(favoriteNumber);
  console.log(hasHandledARequest);
  hasHandledARequest = true;
});

À chaque requête, ce service worker consigne un nombre (mettons 0.13981866382421893). La variable hasHandledARequest est également remplacée par true. Le service worker est maintenant inactif pendant un certain temps, de sorte que le navigateur l'arrête. La prochaine fois qu'une requête est envoyée, le service worker est à nouveau nécessaire. Le navigateur le réveille donc. Son script est évalué à nouveau. hasHandledARequest est maintenant réinitialisé sur false, et favoriteNumber est complètement différent : 0.5907281835659033.

Vous ne pouvez pas vous fier à l'état stocké dans un service worker. De plus, la création d'instances d'éléments tels que les canaux de messages peut entraîner des bugs. Vous obtiendrez une nouvelle instance chaque fois que le service worker s'arrêtera ou démarrera.

Dans le chapitre 3 sur les service workers, nous avons vu que notre service worker arrêté perdait toute couleur en attendant d'être réveillé.

Visualisation d'un service worker arrêté

Ensemble, mais séparés

Votre page ne peut être contrôlée que par un seul service worker à la fois. Toutefois, deux service workers peuvent être installés en même temps. Lorsque vous modifiez le code de votre service worker et actualisez la page, vous ne modifiez pas réellement votre service worker. Les services workers sont immutable. Vous en créez un tout nouveau. Ce nouveau service worker (appelons-le SW2) s'installe, mais il ne s'active pas encore. Il doit attendre la fin du service worker actuel (SW1) (lorsque l'utilisateur quitte votre site).

Interférer avec les caches d'un autre service worker

Lors de l'installation, SW2 peut configurer des éléments, généralement en créant et en remplissant des caches. Attention: ce nouveau service worker a accès à tout ce à quoi le service worker actuel a accès. Si vous n'êtes pas prudent, votre nouveau service worker en attente peut vraiment perturber votre service worker actuel. Voici quelques exemples de situations qui pourraient vous causer des problèmes:

  • SW2 peut supprimer un cache que SW1 utilise activement.
  • SW2 peut modifier le contenu d'un cache utilisé par SW1, ce qui peut entraîner une réponse de SW1 avec des composants que la page n'attend pas.

Ignorer skipWaiting

Un service worker peut également utiliser la méthode skipWaiting() à risque pour prendre le contrôle de la page dès qu'elle est installée. Ce n'est généralement pas une bonne idée, sauf si vous essayez intentionnellement de remplacer un service worker défectueux. Le nouveau service worker peut utiliser des ressources mises à jour que la page actuelle n'attend pas, ce qui entraîne des erreurs et des bugs.

Commencer par un nettoyage

Pour éviter que vos services workers ne se détruisent mutuellement, assurez-vous qu'ils utilisent des caches différents. Le moyen le plus simple d'y parvenir consiste à versionner les noms de cache qu'ils utilisent.

const version = 1;
const assetCacheName = `assets-${version}`;

self.addEventListener("install", event => {
  caches.open(assetCacheName).then(cache => {
    // confidently do stuff with your very own cache
  });
});

Lorsque vous déployez un nouveau service worker, vous devez modifier version pour qu'il effectue ce dont il a besoin avec un cache entièrement distinct de celui du service worker précédent.

Visualisation d'un cache

Nettoyage de fin

Une fois que votre service worker atteint l'état activated, vous savez qu'il a pris le relais et que le service worker précédent est redondant. À ce stade, il est important de nettoyer l'ancien service worker. Non seulement elle respecte les limites de stockage du cache de vos utilisateurs, mais elle peut également éviter les bugs involontaires.

La méthode caches.match() est un raccourci souvent utilisé pour récupérer un élément à partir de n'importe quel cache où il existe une correspondance. Toutefois, il itère les caches dans l'ordre de leur création. Supposons que vous disposiez de deux versions d'un fichier de script app.js dans deux caches différents : assets-1 et assets-2. Votre page attend le script plus récent stocké dans assets-2. Toutefois, si vous n'avez pas supprimé l'ancien cache, caches.match('app.js') renverra l'ancien de assets-1 et votre site risque de ne pas fonctionner.

Pour nettoyer les anciens services workers, il vous suffit de supprimer tout cache dont le nouveau service worker n'a pas besoin:

const version = 2;
const assetCacheName = `assets-${version}`;

self.addEventListener("activate", event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== assetCacheName){
            return caches.delete(cacheName);
          }
        });
      );
    });
  );
});

Empêcher vos service workers de s'entrechoquer demande un peu de travail et de discipline, mais cela en vaut la peine.

État d'esprit des service workers

Adopter le bon état d'esprit lorsque vous réfléchissez aux services workers vous aidera à créer le vôtre en toute confiance. Une fois que vous aurez compris comment les utiliser, vous pourrez créer des expériences incroyables pour vos utilisateurs.

Si vous souhaitez comprendre tout cela en jouant à un jeu, vous avez de la chance. Jouez à Service Workers pour découvrir les secrets des service workers et vaincre les bêtes hors connexion.