Le « hors connexion » est un grand sujet en ce moment, notamment parce que beaucoup d’applications web semblent aussi fonctionner en tant qu’applications mobiles. L’API qui aide au hors connexion : Application Cache (aussi appelée « appcache ») présente de nombreux problèmes, dont beaucoup sont abordés dans le billet de Jake Archibald : Application Cache is a Douchebag. Voici quelques-uns des problèmes qu’on rencontre avec AppCache :

Aujourd’hui il existe une nouvelle API disponible pour les développeurs qui garantit que leur application web fonctionne correctement : l’API Service Worker. L’API Service Worker permet aux développeurs de gérer ce qu’ils veulent mettre ou ne pas mettre en cache pour une utilisation hors connexion avec JavaScript.

Voici le livre de recettes des service workers (Service Worker Cookbook)

Pour présenter l’API Service Worker, nous utiliserons des exemples tout droit tirés du livre de recettes rédigé par Mozilla. Ce livre de recettes (ou cookbook en anglais) est un ensemble d’exemples pratiques et fonctionnels qui mettent en œuvre les service workers dans des applications web modernes. Nous déroulerons cette introduction aux service workers en trois billets :

Bien entendu, cette API ne se limite pas qu’aux aspects hors connexion, elle peut aussi être utile en termes de performances. Cela dit, j’aimerais commencer par les éléments de base en abordant les stratégies hors connexion utilisant les service workers.

Qu’entend-on par hors connexion ?

Hors connexion (ou offline en anglais) ne signifie pas uniquement que l’utilisateur n’est pas connecté à Internet, celui-ci peut également utiliser une connexion réseau instable. En bref, hors connexion signifie que l’utilisateur ne dispose pas d’une connexion fiable (une situation que nous connaissons tous).

Recette : Statut hors connexion

Avec cette recette, on voit comment utiliser un service worker pour mettre en cache une liste de fichiers et indiquer à l’utilisateur qu’il peut maintenant être hors connexion et utiliser l’application. L’application elle-même est plutôt simple : on montre une image aléatoire quand on clique sur un bouton. Voyons en détail les composants qui interviennent pour réaliser ceci.

Le service worker

Nous allons nous intéresser d’abord au fichier service-worker.js pour identifier ce que l’on met en cache. Nous allons mettre en cache des images aléatoires à afficher ainsi que la page d’affichage et les ressources JavaScript nécessaires, dans un cache appelée dependencies-cache :

var CACHE_NAME = 'dependencies-cache';
// Liste des fichiers nécessaires pour que l'application fonctionne hors connexion
 var REQUIRED_FILES = [
  'random-1.png',
  'random-2.png',
  'random-3.png',
  'random-4.png',
  'random-5.png',
  'random-6.png',
  'style.css',
  'index.html',
  '/', // URL distincte de index.html !
  'index.js',
  'app.js'
];

L’événement install du service worker ouvrira le cache et utilisera addAll pour demander au service worker de cacher les fichiers listés :

self.addEventListener('install', function(event) {
  // Étape d'installation : chargement de chaque fichier listé dans le cache
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        // Ajoute toutes les dépendances hors connexion au cache
        return cache.addAll(REQUIRED_FILES);
      })
      .then(function() {
              //A partir d'ici tout est dans le cache
        return self.skipWaiting();
      })
  );
});

L’événement fetch du service worker est déclenché à chaque requête de la page. Cet événement vous permet également de fournir un contenu alternatif à ce qui a été demandé. Pour les besoins du contenu hors connexion, cependant, le gestionnaire (listener) de l’événement fetch sera très simple : si le fichier est dans le cache, il est renvoyé depuis le cache ; sinon, le fichier est récupéré au niveau du serveur.

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // Trouvé dans le cache - renvoie la réponse depuis le cache
        if (response) {
          return response;
        }

        // Absent du cache - renvoie la réponse depuis le serveur
        // 'fetch' est surtout une solution de repli
        return fetch(event.request);
      }
    )
  );
});

La dernière partie de ce fichier service-worker.js correspond à l’activation du gestionnaire d’événement où nous appelons immédiatement le service worker afin d’éviter à l’utilisateur de rafraîchir la page pour le déclenchement de l’événement. L’activation se déclenche quand une version précédente d’un service worker (s’il en existe) a été remplacée et que le service worker à jour prend le contrôle.

self.addEventListener('activate', function(event) {
  // Appel de la méthode 'claim' pour obliger la réalisation 
  // d'un événement 'controllerchange' sur le navigator.serviceWorker
  event.waitUntil(self.clients.claim());
});

En résumé, nous ne voulons pas que l’utilisateur ait besoin de rafraichir la page pour que le service worker démarre, nous voulons que le service worker soit activé dès le chargement initial de la page.

L’enregistrement du service worker

Après la création du service worker, il faut maintenant l’enregistrer.

// Enregistrer le service worker
navigator.serviceWorker.register('service-worker.js', {
  scope: '.'
}).then(function(registration) {
  // Le service worker a été enregistré !
});

Souvenez-vous : l’objectif de cette recette est de signaler à l’utilisateur quand les fichiers nécessaires ont été cachés. Pour cela nous devons surveiller l’état du service worker. Quand il est « activé », nous savons que des fichiers essentiels ont été mis en cache et que notre application est prête à être utilisée hors connexion, nous pouvons donc en informer l’utilisateur.

// Surveillance d'un claim sur le service worker
navigator.serviceWorker.addEventListener('controllerchange', function(event) {
  // Surveillance d'un changement d'état du service worker
  navigator.serviceWorker.controller.addEventListener('statechange', function() {
    // Si le service worker devient "activated", 
    // information de l'utilisateur : il peut être hors connexion !
    if (this.state === 'activated') {
      // Affichage de la notification 
      // " vous pouvez utiliser l'application hors connexion"
      document.getElementById('offlineNotification').classList.remove('hidden');
    }
  });
});

Pour vérifier l’enregistrement et le fonctionnement de l’application hors connexion, il suffit d’utiliser cette recette ! Ici, elle fournit un bouton pour charger une image aléatoire en modifiant l’attribut src de l’image.

// Ce fichier est nécessaire pour que l'application fonctionne hors connexion
document.querySelector('#randomButton').addEventListener('click', function() {
  var image = document.querySelector('#logoImage');
  var currentIndex = Number(image.src.match('random-([0-9])')[1]);
  var newIndex = getRandomNumber();

  // s'assure que nous avons reçu une image différente de l'image actuelle
  while (newIndex === currentIndex) {
    newIndex = getRandomNumber();
  }

  image.src = 'random-' + newIndex + '.png';

  function getRandomNumber() {
    return Math.floor(Math.random() * 6) + 1;
  }
});

En modifiant la source de l’image, nous déclencherions une demande sur le réseau pour cette image, mais puisque cette image a été cachée par le service worker, il n’est pas nécessaire de faire une demande sur le réseau. Cette recette permet de traiter les cas les plus simples du traitement hors connexion : mettre en cache des fichiers statiques afin de les utiliser hors connexion.

Recette : Utiliser du contenu hors connexion en cas de besoin

Dans cette recette, on suit un autre scénario plutôt simple : on récupère une page en AJAX et on répond avec une ressource HTML en cache (offline.html) si la requête échoue.

Le service worker

Pour l’installation du service worker, on récupère le fichier offline.html et on le place dans un cache qu’on appellera offline :

self.addEventListener('install', function(event) {
  // On place `offline.html` dans le cache
  var offlineRequest = new Request('offline.html');
  event.waitUntil(
    fetch(offlineRequest).then(function(response) {
      return caches.open('offline').then(function(cache) {
        return cache.put(offlineRequest, response);
      });
    })
  );
});

Si cette requête échoue, le service worker ne s’enregistrera pas car la promesse sera rejetée. Le gestionnaire d’événement pour fetch attend une requête pour la page et, si celle-ci échoue, répond avec le fichier offline.html mis en cache pendant l’enregistrement.

self.addEventListener('fetch', function(event) {
  // On ne gère que les documents HTML en cas de besoin
  var request = event.request;
  // && request.headers.get('accept').includes('text/html')
  if (request.method === 'GET') {
    // `fetch()` utilisera le cache dès que possible
    event.respondWith(
      fetch(request).catch(function(error) {
        // `fetch()` lève une exception lorsque le serveur ne répond pas
        // mais pas lorsqu'on a des réponses 
        // HTTP valides, même `4xx` ou `5xx`.
        return caches.open('offline').then(function(cache) {
          return cache.match('offline.html');
        });
      })
    );
  }
  // On peut ajouter d'autres gestionnaires d'événements ici. Si on n'appelle
  // pas `event.respondWith()` la requête sera gérée sans le ServiceWorker.
});

Vous verrez ici que catch est utilisé pour détecter si la requête a échoué et que, dans ce cas, on répond avec offline.html.

L’enregistrement du service worker

Un service worker ne doit être enregistré qu’une seule fois. Dans cet exemple, on voit comment passer outre l’enregistrement s’il a déjà été fait. On vérifie la présence de la propriété navigator.serviceWorker.controller : si la propriété n’existe pas, alors on enregistre le service worker.

if (navigator.serviceWorker.controller) {
  // Un ServiceWorker contrôle le site au chargement
  // et peut utiliser du contenu hors connexion si besoin.
  console.log('DEBUG: serviceWorker.controller est équivalent à true');
  debug(navigator.serviceWorker.controller.scriptURL + ' (onload)', 'controller');
}
else {
  // On enregistre le ServiceWorker
  console.log('DEBUG: serviceWorker.controller est équivalent à false');
  navigator.serviceWorker.register('service-worker.js', {
    scope: './'
  }).then(function(reg) {
    debug(reg.scope, 'register');
  });
}

Lorsque le service worker est bien enregistré, vous pouvez tester la recette (et déclencher une nouvelle requête) en cliquant sur le lien « Rafraîchir », cela déclenchera un rafraîchissement de la page en réinitialisant le cache grâce à :

// Le lien de rafraîchissement doit ne pas se servir du cache :
parameterdocument.querySelector('#refresh').search = Date.now();

On peut fournir un message à l’utilisateur pour lui indiquer qu’il est hors connexion (plutôt que de garder le message par défaut du navigateur) et l’informer des raisons pour lesquelles l’application n’est pas disponible lorsqu’il est hors connexion.

Hors connexion et en avant !

Les service workers ont inauguré de nouvelles possibilités pour mieux contrôler le comportement hors connexion d’un site. Aujourd’hui, vous pouvez utiliser l’API Service Worker dans Chrome et avec Firefox Developer Edition. De nombreux sites utilisent déjà les service workers, avec Firefox Developer Edition, vous pouvez consulter about:serviceworkers pour connaitre la liste des service workers installés par les sites que vous avez visités. Page about:serviceworkers dans Firefox Developer Edition

Ce livre de recettes sur les service workers est rempli de cas pratiques et nous continuons à en ajouter. Préparez-vous pour le prochain article de cette série : Hors connexion et plus encore, où vous verrez comment utiliser l’API Service Worker pour gérer d’autres scénarios que le mode hors connexion.

À propos de David Walsh

David Walsh est développeur web à Mozilla et travaille principalement sur le Mozilla Developer Network. Il participe à des conférences, écrit sur son blog et est un optimiste du Web.