Makina Blog

Le blog Makina-corpus

Découvrir le Service Worker


Tout (ou presque) pour rendre votre webapp disponible offline.

Si vous n'en avez jamais entendu parler

Un Service Worker est un script chargé parallèlement aux scripts de votre page et qui va s'exécuter en dehors du contexte de votre page web. Bien que le Service Worker n'ait pas accès au DOM ou aux interactions avec l'utilisateur, il va pouvoir communiquer avec vos scripts via l'API postMessage. Il se place en proxy de votre Web App, interceptant toutes les requêtes serveur et propose par exemple d'y répondre avec un cache ou en récupérant des données du LocalStorage ou d'IndexedDB. Il rend donc votre application disponible offline.

Les promesses des Service Workers

Que nous soyons dans un contexte d'application mobile hybride ou de webapp responsive, la mobilité des terminaux d'accès traîne souvent avec elle son côté obscur : la perte de connectivité (ou la faible connectivité). Sans accès au serveur, nos applications deviennent inutiles et ce n'est pas l'AppCache qui va venir à notre secours. Les Service Workers nous permettent de pallier ces problèmes en utilisant des ressources en cache si le réseau n'est pas disponible, permettant ainsi à notre application de fournir une expérience peu dégradée avant de retrouver un réseau disponible.

A script apart

Le script d'un Service Worker tourne dans le navigateur, mais en arrière-plan, sans accès au DOM ni aux interactions avec les utilisateurs. Il se place entre votre webapp et le réseau, permettant de jouer le rôle de proxy pour nos ressources. Il a une durée de vie indépendante du site web, il s'arrête lorsqu'il n'est pas utilisé et redémarre si besoin. Il n'a pas besoin de votre application pour tourner et peut donc permettre l'envoi de notifications (Comme le précise caniuse.com il faudra essayer la démo avec Chromium ou Firefox 44).

Le cycle de vie d'un Service Worker

Pour installer un Service Worker pour votre site, nous devrons l'enregistrer (register) dans le script de notre webapp. Une fois installé, notre Service Worker va s'activer (activate), c'est à ce moment que nous allons gérer la mise à jour du cache ou du Service Worker lui-même.

Après l'activation, le Service Worker est prêt à intercepter les événements fetch et message émis respectivement par une requête serveur ou un appel via l'API postMessage.

Découvrir et tester les Services Workers

Mozilla et Google sont à la pointe concernant les Service Workers. Ils proposent tous les deux des recettes et autres patterns pour les utiliser au mieux.

N'hésitez pas à visiter le site https://serviceworke.rs de Mozilla pour découvrir les Service Workers en action.

Profitez de l'Offline Cookbook de Jake Archibald pour faire les points sur les cas d'utilisation. Il a même développé une application pour les besoins de démonstration.

Implémenter son premier Service Worker

Prérequis

Le fichier html (source)

L'application charge trois ressources : un fichier de style CSS, un fichier contenant les données et un fichier JavaScript pour générer la page.

Le fichier de données (source)

Le fichier déclare une variable dans le scope global contenant une liste d'images avec pour chacune : un nom, un texte alternatif, une url et une attribution.

Le fichier app.js (source)

Le premier bloc enregistre le Service Worker avec le code suivant :

if ('serviceWorker' in navigator) {
  navigator.serviceWorker
    .register('/sw-test/sw.js', { scope: '/sw-test/' })
    .then(function(reg) {
      // suivre l'état de l'enregistrement du Service Worker : `installing`, `waiting`, `active`
    });
}

Le script du Service Worker est situé à l'adresse /sw-test/sw.js, nous rentrerons dans les détails dans le prochain paragraphe.

La suite du script récupère le fichier de données et crée pour chacune des entrées de la galerie une balise <img> avec l'url de l'image, une balise <caption> avec le titre et l'attribution, enfin une figure parente <figure> pour encapsuler le tout. Tout cela se passe après que le document ait été chargé grâce à window.onload.

Ici, rien de particulier, une requête ajax récupère les données et l'API document.createElement permet de créer les différents éléments HTML et de les insérer dans la balise <section> du fichier index.html.

Le Service Worker (source)

Au premier appel du Service Worker, ce dernier est installé dans le navigateur de l'utilisateur grâce à :

this.addEventListener('install', function(event) {
  // ajouter les fichiers au cache
});

event.waitUntil permet de bloquer les autres événements jusqu'à la résolution (ou le rejet) des promesses passées en paramètres.

Le Service Worker propose une interface cache pour représenter les paires d'objets Request/Response qui seront mises en cache. On peut enregistrer plusieurs objets cache pour un même domaine. Ainsi le code suivant ouvre ‒ s'il existe ‒ ou crée ‒ sinon ‒ le cache v1 et y enregistrera, lorsqu'elles seront appelées, les paires de requêtes/réponses correspondant aux routes écrites :

caches.open('v1').then(function(cache) {
  return cache.addAll([
    '/sw-test/',
    '/sw-test/index.html',
    '/sw-test/style.css',
    '/sw-test/app.js',
    '/sw-test/image-list.js',
    '/sw-test/star-wars-logo.jpg',
    '/sw-test/gallery/',
    '/sw-test/gallery/bountyHunters.jpg',
    '/sw-test/gallery/myLittleVader.jpg',
    '/sw-test/gallery/snowTroopers.jpg'
  ]);
});

On remarquera l'utilisation des promesses dont on a déjà évoqué l'utilité.

Pour intercepter les requêtes, on ajoutera un comportement à l'événement fetch :

this.addEventListener('fetch', function(event) {
  // C'est là que la magie opère, Noël !
});

event.respondWith permet d'enregistrer une réponse personnalisée ou de gérer les erreurs du réseau.

Ensuite on retourne simplement la ressource qui correspond à la requête si elle est disponible dans le cache :

caches.match(event.request);

S'il n'y a pas de correspondance dans le cache, la promesse caches.match sera rejetée et nous aurons une erreur réseau classique (404). Mais on peut aussi utiliser les possibilités des Service Workers pour proposer un traitement plus adéquat à nos erreurs réseau :

caches.match(event.request).catch(function() {
  return fetch(event.request);
})

Ici, si le cache ne contient pas la ressource correspondante à la requête, on tente de faire un appel réseau avec cette même requête. Et dans le cas de notre exemple, nous faisons encore mieux en rajoutant cette ressource au cache si la requête réseau a fonctionné :

var response;
event.respondWith(caches.match(event.request).catch(function() {
  return fetch(event.request);
}).then(function(r) {
  response = r;
  caches.open('v1').then(function(cache) {
    cache.put(event.request, response);
  });
  return response.clone();
}));

Nous retournons une copie de la réponse car les objets requêtes et réponses sont des flux qui ne peuvent être consommés qu'une seule fois. Ici on consomme la réponse pour l'ajouter dans le cache et on retourne la réponse à celui qui a fait la requête, l'objet XMLHttpRequest de app.js.

Enfin, si la requête n'a pas de correspondance dans le cache et si le réseau n'est pas disponible, on tombe dans le dernier cas :

catch(function() {
  return caches.match('/sw-test/gallery/myLittleVader.jpg');
})

Et on retourne une ressource par défaut, ici une des images que nous savons disponibles dans le cache.

Pour preuve, une fois le Service Worker activé (visitez une première fois https://mdn.github.io/sw-test), essayez donc de lire la ressource https://mdn.github.io/sw-test/mylittleponey qui n'existe pas dans l'application de démonstration.

Mise à jour et gestion du cache

Lorsque nous avons déjà une version de notre Service Worker installée et activée, celle-ci restera responsable des traitements des requêtes tant qu'il y aura des pages utilisant cette version du Service Worker.

Si nous mettons à disposition une nouvelle version de notre Service Worker, celle-ci sera installée en arrière-plan, mais ne sera activée et responsable des traitements de requêtes que lorsque plus aucune page chargée n'utilisera l'ancienne version.

Pour mettre à jour notre Service Worker, il nous suffira de changer la version du cache comme ceci :

this.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('v2').then(function(cache) {
      return cache.addAll([
        '/sw-test/',
        '/sw-test/index.html',
        '/sw-test/style.css',
        '/sw-test/app.js',
        '/sw-test/image-list.js',

         …

         // include other new resources for the new version...
      ]);
    });
  );
});

L'événement activate est émis juste avant que le Service Worker ne soit responsable des traitements des requêtes, c'est donc le bon moment pour gérer le cache en supprimant les entrées qui ne sont plus nécessaires, par exemple.

On écrira quelque chose comme ça :

this.addEventListener('activate', function(event) {
  var cacheWhitelist = ['v2'];

  event.waitUntil(
    caches.keys().then(function(keyList) {
      return Promise.all(keyList.map(function(key) {
        if (cacheWhitelist.indexOf(key) === -1) {
          return caches.delete(keyList[i]);
        }
      }));
    })
  );
});

Dev tools

Les navigateurs qui implémentent le nécessaire aux Service Workers ajoutent quelques outils pour les lister, les debugger, les démarrer, les stopper, les désinscrire (unregister).

Pour Chromium, les outils sont disponibles via chrome://inspect/#service-workers et chrome://serviceworker-internals.

Pour Firefox, ils sont disponibles via about:serviceworkers.

Les dev tools vous permettent également de tester les mode offline de votre web app comme vu plus haut avec : pour Firefox ou pour Chromium.

Annexes

Autres applications

Nous couvrirons sûrement les autres cas d'utilisation du Service Worker qui peut servir à autre chose que de fournir des contenus lorsqu'on est hors ligne, comme par exemple :

  • La synchronisation des données en arrière-plan ;
  • Répondre aux requêtes depuis d'autres origines ;
  • Recevoir des données lourdes à calculer comme des données de géolocalisation ou de gyroscope, afin que plusieurs pages puissent y accéder sans refaire les calculs ;
  • Compilation et gestion de dépendances de CoffeeScript, less, des modules CJS/AMD, etc. pour les besoins de développement ;
  • Gestion et utilisation de services en d'arrière-plan ;
  • Templates personnalisés en fonction de patterns d'URL ;
  • Amélioration des performances, par exemple en pré-chargeant les ressources que l'utilisateur pourrait utiliser plus tard telles que les prochaines photos d'un album.

Cette liste, proposée par Mozilla Developer Network, n'est clairement pas exhaustive. On peut retrouver d'autres cas d'utilisation en action sur le site https://serviceworke.rs proposé également par Mozilla comme :

Quelques liens

Enfin voici quelques ressources pour aller plus loin.

Actualités en lien

Image
Capture d'une partie de carte montrant un réseau de voies sur un fond de carte sombre. Au centre, une popup affiche les information de l'un des tronçons du réseau.
28/02/2024

Géné­rer un fichier PMTiles avec Tippe­ca­noe

Exemple de géné­ra­tion et d’af­fi­chage d’un jeu de tuiles vecto­rielles en PMTiles à partir de données publiques.

Voir l'article
Image
Read The Docs
01/02/2024

Publier une documentation VitePress sur Read The Docs

À l'origine, le site de documentation Read The Docs n'acceptait que les documentations Sphinx ou MKDocs. Depuis peu, le site laisse les mains libres pour builder sa documentation avec l'outil de son choix. Voici un exemple avec VitePress.

Voir l'article
Image
Encart Article Eco-conception
25/04/2023

Comment compresser son code applicatif de manière efficace avec Nginx et Brotli ?

Dans cet article, nous allons mettre en place un algorithme de compression des données textuelles plus efficace, que celui utilisé habituellement, pour réduire le poids d'une page web.

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus