Accueil / Blog / Métier / 2016 / Découvrir le Service Worker

Découvrir le Service Worker

Par eco — publié 06/01/2016, édité le 07/01/2016
Tout (ou presque) pour rendre votre webapp disponible offline.
Découvrir le Service Worker

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.

ABONNEZ-VOUS À LA NEWSLETTER !
Voir aussi
React 16.3 : Introduction de la context API React 16.3 : Introduction de la context API 06/04/2018

React 16.3 apporte son lot de nouveautés, mais surtout la version stable de la context API.

Varnish et Drupal : gérer un cache anonyme étendu Varnish et Drupal : gérer un cache anonyme étendu 14/03/2018

Le rôle d'un Reverse Proxy Cache Varnish dans une architecture Web (type Drupal).

Découverte de React Native Découverte de React Native 18/04/2016

Présentation de React Native, quelles sont ses possibilités et peut-on l'utiliser en prod ?

Bien démarrer avec JavaScript Bien démarrer avec JavaScript 15/12/2015

Callbacks, this, scope... mais qu'est-ce que c'est que ce langage ?

Comment mettre en place Angular Universal Comment mettre en place Angular Universal 29/06/2017

Toutes les étapes détaillées et expliquées. Les pièges à éviter.