[BDX.io] Web hors ligne avec les Services Workers

Le 16 Octobre dernier se déroulait la deuxième édition du BDX.IO (http://www.bdx.io), la conférence bordelaise autour du monde Java / JS / BigData. Plus de 40 conférences, 512 participants et les vidéos déjà disponibles sur Youtube : https://www.youtube.com/channel/UCA7pEYY0BlgCdpbnjhCDezQ/videos

Dans sa conférence “Web hors ligne”, Hubert Sablonnière nous a présentés les différentes solutions (inefficaces) de Offline first. Puis il nous a montré LA bonne solution : les Services Workers. Lorsque l’on a un forfait “presqu’Internet” (pas de connexion, plus de forfait data, etc.) cela peut s’avérer très utile !

Le Offline first

AppCache existe depuis 2009 et permet de faire fonctionner un site web en mode offline. Il faut créer un fichier manifest .appcache dans lequel on définit les éléments à mettre en cache, les URLs externes nécessitant du réseau, etc.

Mais son utilisation est très inefficace :

  • Dès qu’on modifie le Manifest cela recharge tout le cache. Et c’est la seule manière pour charger une nouvelle version d’une ressource car…
  • AppCache utilise toujours la version en cache même si on est online.
  • Avec du SSL, il faut que toutes les ressources soient sur le même domaine.
  • Les ressources non misent en cache ne seront pas chargées sur une page en cache (même si vous êtes online donc…)
  • Tout doit être dans la manifest, donc si vous avez deux versions pour le site Desktop et le site Mobile, les deux seront téléchargés dans le cache… Bye bye les optimisations de bande passante !

Pour palier à ce cache totalement inutile, tournons nous vers les Services Workers !

Services Workers

“The service worker is like a shared worker, but whereas pages control a shared worker, a service worker controls pages”

Excitant non ? 🙂

On va pouvoir exécuter du Javascript avant que la page ne s’ouvre, en interceptant et modifiant la navigation et les requêtes des ressources.

Alors évidemment, pour des questions de sécurité, tout ceci n’est faisable qu’en HTTPS. Vous ne pourrez pas faire fonctionner des Services Workers sur du HTTP.

En terme de caractéristiques, un Service Worker :

  • s’exécute dans son propre contexte (généralement son propre thread),
  • n’est pas lié à une page particulière,
  • n’a pas accès au DOM,
  • peut s’exécuter sans page du tout,
  • peut se terminer s’il n’est plus en utilisation et se recréer par la suite (event-driven),
  • ne fonctionne qu’en HTTPS.

Ainsi, un Service Worker peut s’utiliser pour accélérer le chargement des pages ou fonctionner en mode déconnecté. Il peut aussi s’utiliser comme outil de base pour exécuter des tâches en arrière plan, comme le push de messages ou la synchronisation.

Un petit exemple :

if('serviceWorker' in navigator) {
navigator.serviceWorker.register('/my-blog/sw.js', 
{scope:'/my-blog/'})
.then(function(sw) {// registration worked!
                    })
.catch(function() {// registration failed :(
                    })
    ;}

Si l’enregistrement du Service Worker se trouvant dans sw.js fonctionne, il commencera à s’installer pour tout URL commençant par my-blog.

Le script “sw.js” fonctionnera dans un thread à part. Avec les lignes ci-dessous, le Service Worker se met à écouter l’event “fetch”, qui sera lancé par toute navigation vers une page de /my-blog/* et par toute requête originaire de ces pages (même vers un autre domaine).

self.addEventListener('fetch',function(event) {console.log(event.request);});

Utiliser un site en offline

Un Service Worker a le cycle de vie suivant :

  1. Téléchargement
  2. Installation
  3. Activation

On utilise généralement l’étape d’installation pour initialiser son Service Worker :

self.addEventListener('install', function(event) {
    event.waitUntil(caches.open('static-v1').then(function(cache) {
        return cache.addAll(['/my-blog/', '/my-blog/fallback.html', new Request('//mycdn.com/style.css', {
            mode: 'no-cors'
        }), new Request('//mycdn.com/script.js', {
            mode: 'no-cors'
        })]);
    }));
});

Les Services Workers utilisent une nouvelle API de cache pour stocker les réponses, en utilisant la requête comme clé. Pour utiliser ce cache :

self.addEventListener('fetch', function(event) {
    event.respondWith(caches.match(event.request).then(function(response) {
        return response || event.default();
    }).catch(function() {
        return caches.match('/my-blog/fallback.html');
    }));
});

Ici nous interceptons tous les fetch et répondons avec ce que nous avons en cache. Le matching se fait sur URL + méthode. Le cache ne gère pas d’expiration, ce qui est en cache y reste tant que nous ne faisons pas de suppression ou de modification. Si rien n’est en cache et que nous sommes offline, on redirige ici vers une page d’erreur (qui sera en cache car incluses dans /my-blog/).

Après cet exemple basique, vous pouvez opter pour la stratégie que vous voulez :

  • essayer le contenu online et afficher le cache en cas d’erreur,
  • prendre ce qui est en cache en premier pour optimiser les temps de chargement,
  • etc.

Mettre à jour un Service Worker

Les mises à jour d’un Service Worker sont vérifiées à chaque navigation sur une page (modifiable avec HTTP Cache-Control). Ceci permet de mettre à jour les caches que l’on veut. Le cache est testé au bit prêt, donc à la moindre modification tout est rechargé et l’event “install” est appelé. Pendant l’installation l’ancien cache est toujours disponible pour répondre aux requêtes.

Performances

Les Services Worker sont totalement asynchrones. Ils peuvent gérer plusieurs connexions en parallèle et ne sont pas constamment nécessaires. Le navigateur peut donc décider de le désallouer après chaque event ou de le conserver en fonction de sa charge mémoire. Ceci veut dire qu’on ne peut pas garder d’états dans le scope global :

var hitCounter = 0;
this.addEventListener('fetch', function(event) {
    hitCounter++;
    event.respondWith(new Response('Hit number ' + hitCounter));
});

Ceci peut répondre 1, 2, 3, 4, 1, 1, 2, 1, … Si on veut stocker des données, il faut utiliser le stockage du navigateur, comme indexedDB.

Bibliographie