HTML5

Application web offline avec le cache manifest HTML5

Tutoriel : rendre une application web accessible hors-ligne grâce au cache manifest HTML5 (AppCache), avec ses limites et l'alternative Service Workers.

Faire fonctionner un site même sans connexion fut l'une des grandes promesses de HTML5. Le mécanisme d'origine, le cache manifest (aussi appelé AppCache), permettait de déclarer les ressources à conserver en local pour les servir hors-ligne. Cette technologie est aujourd'hui dépréciée et retirée des navigateurs, mais la comprendre reste utile pour saisir comment on est arrivé aux Service Workers actuels. Si vous débutez, lisez d'abord l'introduction à HTML5 pour situer ces API dans l'écosystème.

Le principe d'une application web hors-ligne

Une application web hors-ligne est un site capable de s'afficher et de fonctionner alors que le navigateur n'a aucun accès au réseau. Le principe est simple : le navigateur conserve une copie locale de certaines ressources (pages HTML, feuilles de style, scripts, images) afin de les recharger depuis le disque plutôt que depuis le serveur.

Mettre des ressources en cache répond à plusieurs besoins :

  • Disponibilité : l'utilisateur garde accès au contenu dans le métro, en avion ou en zone mal couverte.
  • Vitesse : servir un fichier local est beaucoup plus rapide qu'un aller-retour réseau.
  • Économie de données : on évite de retélécharger des fichiers qui n'ont pas changé.

AppCache a été le premier standard à offrir ce comportement de façon déclarative, sans écrire la moindre ligne de code serveur. Il suffisait d'un fichier texte listant les ressources à garder.

Déclarer le manifeste

Pour activer le cache, on ajoutait l'attribut manifest sur la balise <html> de chaque page concernée. Sa valeur pointait vers un fichier manifeste, par convention avec l'extension .appcache.

<!DOCTYPE html>
<html manifest="/exemple.appcache">
<head>
  <meta charset="utf-8">
  <title>Mon appli hors-ligne</title>
</head>
<body>
  <p>Contenu disponible même sans réseau.</p>
</body>
</html>

Un point crucial concernait le serveur. Le fichier devait obligatoirement être servi avec le type MIME text/cache-manifest, faute de quoi le navigateur ignorait purement et simplement le manifeste. Sur Apache, on l'ajoutait par exemple ainsi :

AddType text/cache-manifest .appcache

Si le type MIME était incorrect, aucune erreur visible n'apparaissait. Le site fonctionnait en ligne mais refusait de se charger hors-ligne, ce qui rendait le débogage particulièrement déroutant.

La structure du fichier manifest

Le fichier manifeste est un simple fichier texte. Sa toute première ligne devait être exactement CACHE MANIFEST. Il s'organisait ensuite en trois sections optionnelles introduites par un mot-clé.

CACHE MANIFEST
# version 1.0.3

CACHE:
/styles/main.css
/scripts/app.js
/images/logo.png

NETWORK:
/api/
*

FALLBACK:
/ /hors-ligne.html

Voici le rôle de chaque partie :

  • CACHE : la liste explicite des ressources à télécharger et conserver en local. La page qui référence le manifeste est ajoutée automatiquement.
  • NETWORK : les ressources qui exigent toujours le réseau (appels API, contenus dynamiques). L'astérisque * autorise tout le reste à passer en ligne.
  • FALLBACK : des paires de substitution. Ici, si une page sous / est inaccessible, on sert /hors-ligne.html à la place.

La ligne # version 1.0.3 est un commentaire (tout texte après # est ignoré). Elle jouait pourtant un rôle clé : modifier ce numéro était le seul moyen fiable de signaler au navigateur qu'une nouvelle version du cache existait et devait être retéléchargée.

Piloter le cache en JavaScript

AppCache exposait un objet window.applicationCache permettant de suivre l'état du cache et de réagir à ses changements. Le scénario classique consistait à détecter qu'une nouvelle version venait d'être récupérée, puis à l'activer.

var appCache = window.applicationCache;

appCache.addEventListener('updateready', function () {
  if (appCache.status === appCache.UPDATEREADY) {
    appCache.swapCache();
    if (confirm('Une nouvelle version est disponible. Recharger ?')) {
      window.location.reload();
    }
  }
});

Les éléments importants étaient :

  • L'événement updateready, déclenché quand le navigateur a fini de télécharger un manifeste modifié.
  • La méthode swapCache(), qui bascule sur le nouveau cache. Attention, elle ne met pas à jour les ressources déjà affichées : un rechargement de page restait nécessaire pour voir les nouveautés.
  • La propriété status et ses événements (checking, downloading, cached, error) pour suivre le cycle de vie.

Cette API offrait peu de marge de manœuvre. On pouvait observer le cache, mais pas vraiment décider finement quelle ressource servir selon le contexte.

Les pièges et limites d'AppCache

AppCache a vite acquis une réputation de technologie capricieuse, au point que ses propres concepteurs ont fini par la déconseiller. Plusieurs comportements contre-intuitifs en étaient responsables.

  • Le manifeste se mettait lui-même en cache. Modifier la liste des ressources ne suffisait pas : tant que l'octet du fichier .appcache restait identique, le navigateur conservait son ancienne copie. D'où l'astuce du commentaire de version à incrémenter manuellement.
  • Mises à jour à double chargement. Un visiteur recevait la nouvelle version seulement au chargement suivant, car le premier servait l'ancien cache pendant le téléchargement du nouveau.
  • La page hôte toujours en cache. Toute page portant l'attribut manifest était mise en cache d'office, même absente de la section CACHE, ce qui surprenait beaucoup de développeurs.
  • Tout ou rien. Si une seule ressource listée échouait au téléchargement, l'ensemble de la mise à jour était abandonné.

Le standard AppCache est officiellement déprécié. Les navigateurs modernes l'ont retiré (Chrome depuis la version 95, Firefox également). N'utilisez plus l'attribut manifest ni les fichiers .appcache dans un nouveau projet, ils n'auront aucun effet.

L'alternative moderne : Service Workers et l'API Cache

La solution actuelle repose sur les Service Workers couplés à l'API Cache. Un service worker est un script qui s'exécute en arrière-plan, indépendamment de la page, et qui peut intercepter chaque requête réseau pour décider comment y répondre. Contrairement à AppCache, vous gardez un contrôle total et programmatique sur le cache.

On l'enregistre depuis la page principale :

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(function () { console.log('Service worker enregistré.'); })
    .catch(function (err) { console.error(err); });
}

Le fichier sw.js écoute ensuite deux événements clés. À l'installation (install), il remplit le cache avec les ressources de base. À chaque requête (fetch), il applique une stratégie, par exemple cache-first : servir depuis le cache si disponible, sinon aller chercher sur le réseau.

var CACHE = 'site-v1';
var RESSOURCES = ['/', '/styles/main.css', '/scripts/app.js'];

self.addEventListener('install', function (event) {
  event.waitUntil(
    caches.open(CACHE).then(function (cache) {
      return cache.addAll(RESSOURCES);
    })
  );
});

self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.match(event.request).then(function (reponse) {
      return reponse || fetch(event.request);
    })
  );
});

Ce modèle apporte des avantages décisifs :

  • Chaque version de cache porte un nom (site-v1), ce qui rend les mises à jour explicites et fiables.
  • On choisit librement la stratégie : cache-first, network-first, stale-while-revalidate, selon le type de ressource.
  • L'API caches permet d'ouvrir, lire, écrire et supprimer des caches nommés sans aucune ambiguïté.

Enfin, les Service Workers sont la brique technique au cœur des Progressive Web Apps (PWA). Associés à un manifeste d'application web (un fichier manifest.json qui décrit nom, icônes et écran de démarrage), ils permettent d'installer le site comme une véritable application, lançable depuis l'écran d'accueil et fonctionnelle hors-ligne. C'est exactement ce qu'AppCache promettait, mais réalisé de façon robuste et maîtrisable.