HTML5 cache manifest avec Rails

Publié le 11 mars 2015 par Numa Claudel | back

Cet article est publié sous licence CC BY-NC-SA

Vous avez sans doute entendu parler de cette particularité HTML5: la mise en cache des ressources en vue d’une utilisation hors ligne.

Comment activer cette fonctionnalité ? Avec une application Rails ? Quel est le processus de mise en cache ? C’est ce que je vous propose d’explorer dans ce billet.

HTML5 cache manifest

Un fichier manifest liste les fichiers qui doivent être accessibles au travers du cache du navigateur. Ainsi le navigateur accède aux fichiers copiés localement, et la navigation du site est possible même hors ligne. Ce cache apporte également d’autres bénéfices: il améliore la vitesse de rendu des pages et réduit les échanges avec le serveur.

Pour rendre possible ce processus de mise en cache, il suffit d’ajouter l’attribut manifest à la balise html:

<!DOCTYPE html>
<html manifest="app.manifest">
</html>

Cet attribut référence un fichier manifest, son nom et son extension étant modifiables. Il correspond à un fichier servit par le serveur. Avec cet exemple, le navigateur cherchera un fichier disponible à l’url: http://”host”/app.manifest, mais ce n’est pas figé.

Ce fichier étant versionné, le navigateur ne re-téléchargera les fichiers référencés qu’en cas de changement de version. Il se servira donc en premier lieu à partir de son cache, puis ira comparer sa version locale avec celle disponible sur le serveur. Ce processus est entièrement géré par le navigateur à chaque requête au serveur.

Notez que si le manifest est supprimé du serveur, le navigateur supprimera le cache correspondant.

Voici un exemple de fichier manifest complet:

CACHE MANIFEST
# 2015-03-11:v1
index.html
style.css
script.js
image1.png

NETWORK:
/

FALLBACK:
images/large/ images/offline.png

Voici quelques règles sur la structure du fichier manifest:

  • il doit être servi avec le type MIME text/cache-manifest
  • sa première ligne est obligatoirement: CACHE MANIFEST
  • il a une section CACHE suivie des fichiers à mettre en cache (omissible car c’est la section par défaut)
  • il peut avoir une section NETWORK suivie des fichiers ou urls qui nécessitent une connexion au serveur
  • il peut avoir une section FALLBACK suivie de fichiers ou urls en correspondance: si la première partie n’est pas accessible, c’est la partie de secours indiquée qui sera utilisée
  • pour ajouter un commentaire commencer la ligne par #

Toutes les lignes d’en-tête de section doivent inclure un « : » à la fin de la section (exemple: NETWORK:).

Le commentaire en dessous de CACHE MANIFEST indique la version du fichier. La comparaison entre le manifest du navigateur et celui présent sur le serveur se faisant d’octet à octet, mettre à jour un numéro de version dans ce commentaire va initier un processus de mise à jour du cache.

Cycle de vie du cache

Le cache présent dans le navigateur comporte un état qui indique quand il doit être mis à jour (cf. Comment fonctionne AppCache):

  • UNCACHED (status == 0): une valeur spéciale qui indique qu’un object applicationCache n’est pas complètement initialisé
  • IDLE (status == 1): le cache n’est pas en cours de mise à jour
  • CHECKING (status == 2): le manifest est en train d’être contrôlé pour d’éventuelles mises à jour
  • DOWNLOADING (status == 3): des ressources sont en train d’être téléchargées pour être ajoutées au cache, dû à un changement du manifest
  • UPDATEREADY (status == 4): une nouvelle version du cache est disponible. Un évènement updateready correspondant est lancé quand une nouvelle mise à jour a été téléchargée mais pas encore activée
  • OBSOLETE (status == 5): le groupe de caches est maintenant obsolète

Cet état est actualisé à chaque requête au serveur.

Servir le manifest avec Rails

Pour servir des fichiers manifest avec une application Rails, une option possible est d’utiliser la gem rack-offline.

Cette gem s’occupera pour nous de générer et de maintenir la version du fichier manifest. Tout ce qu’il nous reste à faire, c’est lui indiquer le contenu du cache.

Même sans avoir été mise à jour depuis longtemps, si vous vous en servez pour mettre en cache des fichiers spécifiques, elle remplie toujours très bien sa fonction, même avec Rails 4. D’autant plus qu’avec Sprockets, référencer un manifest JavaScript ou CSS, reviens à référencer tous les fichiers qu’il inclus, nul besoin donc de définir des dossiers complets à mettre en cache.

Pour définir une route servant un fichier de cache manifest:

offline = Rack::Offline.configure do
  cache ActionController::Base.helpers.asset_path("application.css")
  cache ActionController::Base.helpers.asset_path("application.js")

  network "/"
end

get "/app.manifest" => offline

Cette instruction rendra un fichier manifest à l’url http://”host”/app.manifest de la forme:

CACHE MANIFEST
# bfecba583c42df59c0573cfad91afe7f96c70ec8bef90c369af1d5f6581a47e8
/assets/application.css
/assets/application.js

NETWORK:
/

On a donc assets/application.css et assets/application.js qui seront placés dans le cache du navigateur, ainsi qu’une instruction indiquant que toutes autres urls nécessitent une connexion au serveur.

Avec cette configuration, seules les polices d’écritures et les images peuvent encore nécessiter une mise en cache.

Pour mettre en cache toutes les polices d’écritures par exemple:

fonts_path = Rails.root.join("app", "assets", "fonts")

Dir[fonts_path.join("*")].each do |file|
  cache ActionController::Base.helpers.asset_path(
    Pathname.new(file).relative_path_from(fonts_path)
  )
end

Tous les fichiers présents dans app/assets/fonts seront maintenant aussi placés en cache. Attention toutefois, car cela peut représenter un poids non négligeable.

Par défaut la validité du cache est défini par rack-offline à 10 secondes, mais nous pouvons changer cette durée avec l’option :cache_interval:

# validity: 1 hour
offline = Rack::Offline.configure :cache_interval => 3600 do
.
.
end

Dans le cas d’une SPA

Il se pose un problème dans le cas d’une SPA: il n’y a pas de rechargement entre les pages. Le status du cache reste donc en IDLE (au repos). Il faut donc initier le processus de mise à jour manuellement en JavaScript.

window.applicationCache comporte plusieurs propriétés et fonctions, entre autres status et celles que nous avons vu précédemment dans le Cycle de vie du cache, ainsi que les fonctions swapCache() et update(). Avec ces outils en mains nous avons tout ce qu’il nous faut.

Je vous propose en exemple le code dont je me suis servis dans une application AngularJS:

angular.module('app', [])
.run(['$rootScope', function($rootScope) {
  var refresh = false;

  // OnCacheUpdateReady: set refresh flag to true
  applicationCache.addEventListener('updateready', function(event) {
    refresh = true;
  });

  // OnRouteChange: check cache manifest or update cache and refresh
  $rootScope.$on('$routeChangeSuccess', function(event, next, current) {

    // if IDLE or UPDATEREADY or OBSOLETE
    if(applicationCache.status == 1 || applicationCache.status > 3) {

      // if cache has been clean by a user
      // there is no applicationCache to update
      try {
        applicationCache.update();
      }
      catch(exception) {
        location.reload();
      }
    }

    if(refresh) {
      applicationCache.swapCache();
      location.reload();
    }
  });
}]);

Ce code contrôle si le cache manifest est à jour avec la fonction update() à chaque changement de route. Le fait d’initier une mise à jour, var effectuer un CHECKING, puis changer le statut de applicationCache en fonction de l’échange avec le serveur (si le serveur est disponible). Si une mise à jour s’opère, applicationCache passera par les statut DOWNLOADING et UPDATEREADY. Un observeur sur l’event updateready change la valeur de la variable refresh. Au prochain changement de route, le cache passera sur la nouvelle version et la page sera rafraîchie pour appliquer les changements.

On pourrait aussi proposer à l’utilisateur de rafraîchir manuellement la page quand une nouvelle version a été téléchargée.

Versions des assets entre les déploiements

Les fichiers d’assets étant versionnés à chaque déploiement (si les assets ont subit des modifications), il se présente un problème au moment de la mise à jour du cache. Un fichier d’assets référencé par la page peut, à ce moment précis, ne plus être servit ni par le cache qui viens d’être mis à jour, ni par le serveur sur lequel la version du fichier à changée.

Pour palier à ce problème, l’idée est de créer une route qui matche les assets désirés et de rendre les assets courants correspondants:

# config/routes.rb

get "/assets/:name.:ext", to: redirect("/assets/mon_asset.%{ext}"), constraints: { name: /mon_asset.*/, ext: /css|js/ }

Les assets sont ainsi toujours trouvés et servis par le serveur.

Conclusion

Pour plus de précisions vous pouvez consulter cette page, où tout y est bien détaillé.

Une dernière chose à noter si vous êtes amenés à mettre en place un cache HTML5, vous aurez sans doute besoin de rafraîchir la page sur laquelle vous travaillez 2/3 fois pour voir apparaître les changements (si vous n’avez pas défini de durée de validité). Il est aussi utile de pouvoir consulter et supprimer une application en cache, ce qu’il est possible de faire par exemple avec Chrome à cette url: chrome://appcache-internals.

Pour finir, dans le cas d’une SPA ayant pour but de fonctionner en ligne et hors ligne, hormis la gestions des données, votre application devrait pouvoir remplir ces fonctions.

L’équipe Synbioz.

Libres d’être ensemble.