Comment développer une application web disponible hors ligne ? Installation et configuration

2022-01-17 | Loïc BOURG

image article

Cet article est une introduction au fonctionnement d'un application web hors ligne.
Si vous voulez directement voir des usages avancés rendez vous ici:
Application web hors ligne partie 2

Le besoin

Même si l'accès à internet devient de plus en plus répandu, il existe encore beaucoup de zones blanches dans le monde.

Nous avons été retenu pour le développement de l'application IFprofs et une demande importante était de garantir l'accès à l'application dans le monde même en cas de coupure réseau.

Nous avons décidé de développer l'application sous forme de progressive web app (PWA) pour fournir une expérience utilisateur optimale lors de la navigation sur ce réseau social.

Afin de fournir la meilleure expérience possible lors de la navigation sur ce réseau social, nous avons choisi d'utiliser le framework NextJS pour la partie front.
Nous avons développé la partie backend en API first afin de faciliter la maintenance et pour nous permettre de récupérer les données à afficher depuis NextJS.

Le fonctionnement

La possibilité de fournir une expérience hors ligne aux utilisateurs d’un site est fournie par une technologie nommée service worker.

Elle est disponible depuis firefox 44 ( 27 janvier 2016) et chrome 45 (2 septembre 2015).

Le service worker va agir comme un proxy qui va pouvoir intercepter toutes les requêtes envoyées par le navigateur sur un domaine pour les modifier selon le besoin.

schéma service worker

Source

Dans cet exemple on peut voir un cas d'usage classique du service worker :

  1. Le navigateur fait une requête HTTP pour récupérer le contenu d'une page
  2. La requête HTTP est transmise au réseau par le service worker mais le réseau n'est pas joignable
  3. Le service worker utilise la valeur stockée dans le cache pour créer la réponse
  4. Le navigateur reçoit la réponse créée par le service worker. De son point de vue l'appel réseau a réussi

Subtilités de fonctionnement

Exécution en dehors du thread principal

Fait assez rare pour du JavaScript côté navigateur : il s’exécute dans son propre thread et non pas sur le principal (main thread).

Ce type d’exécution a l'avantage de ne pas bloquer le rendu navigateur lors de son chargement et de son exécution, sur le même principe que les WebWorker.

Ce fonctionnement a en contrepartie le désavantage de ne pas pouvoir modifier le DOM.

Contexte sécurisé

Il faut aussi savoir que cette fonctionnalité n’est autorisée que dans un contexte sécurisé.
Il va donc falloir s'assurer que la production sera bien disponible en https par défaut.

Pour le développement local il existe plusieurs options:

Les outils

Pour faciliter la gestion de notre service worker, nous avons décidé d’utiliser Workbox.

Cette librairie développée par Google est une boîte à outils fournissant tous le nécessaire pour faciliter l’implémentation de son propre service worker (stratégies de mise en cache, precache, routing, …)

Installation

Il existe un plugin webpack pour faciliter un usage classique de workbox.

Vous pouvez également utiliser une librairie fournissant des outils pour simplifier l'intégration de Workbox avec NextJS.
Elle contient de nombreux exemples qui sont intéressants à regarder pour comprendre le fonctionnement de certaines implémentations, même si vous décidez de ne pas l'utiliser.

Nous avons fait le choix de ne pas utiliser ces outils lors de l’implémentation afin d’avoir une plus grande liberté sur le fonctionnement de notre service worker.
Cela a également permis de limiter un peu le côté “empilement de librairies” qui est une problématique récurrente en JavaScript.

Ajout des dépendances

yarn add workbox-core workbox-sw workbox-window workbox-strategies workbox-routing

Quelques explications sur l’utilité de chaque éléments:

  • workbox-core: Fournit les éléments de base de la librairie
  • workbox-sw: Fournit les fonctions permettant de charger des modules workbox dans le service worker
  • workbox-window: Permet de lancer l'exécution du service worker depuis le thread principal
  • workbox-strategies: Met à disposition les différentes stratégies de cache de workbox (cache first, nextwork first, …)
  • workbox-routing: Permet d’indiquer les urls étant associées aux différentes stratégies de cache

Configuration du chargement du service worker

Maintenant que Workbox est installé, il faut mettre à disposition les fichiers de manière publique.

La bonne nouvelle est qu’étant donné que le support des service worker est relativement récent, nous pouvons utiliser du javascript “moderne” sans avoir forcément besoin de passer par une phase de compilation.

Etant donné que nous utilisons webpack avec NextJS nous utilisons le plugin de copie

Un exemple de configuration est disponible pour indiquer la configuration à ajouter dans votre next.config.js si vous utilisez NextJS
Note: Si vous n'utilisez pas Webpack, vous pouvez utiliser n'importe quel outils de build permettant de copier des fichiers à un endroit (Makefile, gulp, grunt, ...)

Une fois cette phase de build configurée, il va falloir lancer l’exécution du service worker depuis le main thread, et donc ajouter ce bout de code dans _app.js

import { Workbox } from 'workbox-window';
// dont try to load service worker if code is executed server side
// load workbox only if browser can use service workers
if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
  let workbox = new Workbox(`/service-worker.js`);
  workbox.register();
}

Note: Si vous n'utilisez pas NextJS, vous pouvez coller ce bout de code à tous les endroits ou vous voulez que votre service worker soit exécuté

⚠ Votre fichier de service worker doit être chargé à la racine du site pour pouvoir intercepter tous les appels réseau de l'application ⚠


Ensuite il ne vous reste plus qu’à créer le fichier service-worker.js dans le dossier public

importScripts('/workbox/workbox-sw.js');

workbox.setConfig({ debug: true });
workbox.setConfig({ modulePathPrefix: '/workbox' });

En rechargeant la page, vous devriez voir votre service worker qui tourne en vous rendant sur l'onglet application des outils de développement du navigateur.

exemple dev tool chrome

Workflow de développement

Pendant votre phase de développement, pensez à cocher "Update on reload" dans l'onglet Application > Service Workers
Cela permettra de mettre à jour votre service worker lors du rechargement de la page au lieu de devoir recharger la page deux fois à chaque modification.
Plus d'informations sur le cycle de vie d'un service worker

Mise en cache

Maintenant que nous avons notre service worker qui tourne, nous pouvons activer nos stratégies de mise en cache.
Nous allons commencer par mettre en cache les pages visitées par l'utilisateur.

Pour cela, ajoutez ce bout de code dans le fichier service-worker.js

// cache html pages
workbox.routing.registerRoute(
  // use this cache strategy only if the network call request an html document
  data => {
    return (
      data.request.destination === 'document'
    );
  },
  // use network first strategy
  new workbox.strategies.NetworkFirst({
    cacheName: 'html-cache',
  })
);

// cache js 
workbox.routing.registerRoute(
  data => {
    if (data.url.pathname === '/_next/webpack-hmr') {
      return false;
    }

    if (data.url.pathname.startsWith('/_next/static/development/')) {
      return false;
    }

    return (
      data.url.pathname.substr(0, 14) === '/_next/static/' &&
      (data.request.destination === 'script' ||
        data.url.pathname.substr(-10, 10) === '.module.js')
    );
  },
  new workbox.strategies.NetworkFirst({
    cacheName: 'assets-cache',
  })
);

En rechargeant la page, vous devriez avoir des informations dans la console du navigateur indiquant que la requête chargeant la page "/" ainsi que les requêtes chargeant du javascript ont été prises en charge par Workbox

schéma service worker

Pour tester le fonctionnement de la mise hors ligne, allez dans Application > Service workers et cochez "Offline".

En rechargeant la page vous devriez avoir la page d'accueil qui s'affiche alors qu'aucun appel réseau n'a fonctionné, vous pouvez même couper votre serveur de développement pour vous en assurer.

Conclusion

Vous avez développé votre première page accessible hors ligne, bravo ! \o/
Si vous voulez voir l'implémentation en entier de ce qui est expliqué ici, un repo est à disposition

Dans la prochaine partie, nous verrons comment :

  • Gérer la mise en cache des pages provenant d'une navigation côté client (appui sur lien NextJS ou appel à router.push())
  • Prévenir l'utilisateur que les données qu'il voit proviennent du cache
  • Gérer l'expiration des données en cache
  • Afficher une page de remplacement si un utilisateur essaye de naviguer sur une page qu'il n'a jamais visité en étant hors ligne

A bientôt

Application web hors ligne partie 2

PWA Service workers Offline NextJS