Archives de Tag: ipad

Html5 offline + Rails : épisode 1 Monsieur Manifest

Dans le cadre de mon boulot diurne, je développe en ce moment une application web pour iPad. Cette dernière doit pouvoir continuer à fonctionner sans connexions, c’est à dire passer en mode déconnecté.

Notre choix s’est porté sur les technologies offerte par l’HTML5, pour une raison évidente : Steve Jobs aime l’HTML5. Donc les utilisateurs de la tablette à la pomme aime l’HTML5. Ne cherchez pas Steve a raison.

Je ne vais pas vous donner un cours, des liens vers des spécs, mais plus vous donner du code que j’ai mis en place pour démarrer le projet. C’est loin d’être parfait et tant qu’à faire toute amélioration (avec du code, le blabla j’m’en scotche) est vivement encouragée.

Sinon je vous recommande http://www.html5rocks.org, c’est une bonne ressource pour débuter avec les notions d’appli web offline.

Bref, pour que notre application fonctionne, nous devons stocker en local, l’ensemble des fichiers dont elle aura besoin pour avoir une tronche acceptable.

Ce qu’il faut savoir :

  • dès que le manifest change d’un octet il est considéré comme modifié et donc l’API d’applicationCache permettra de lancer la mise à jour
  • le manifest doit être servit au format manifest et surtout pas du text
  • utiliser un FALLBACK permet d’éviter à l’API JS de péter lors de la récupérations des fichiers. En cas réels avec un ensemble d’assets important, il est dommage de bloquer la synchronisation pour un fichier.
  • le manifest permet de définir des zones où la connexion est obligatoire, celà évite de se faire chier à gérer le cas de l’authentification partout
  • le chemin vers le manifest de la page doit être ajouté dans la balise html

Chez Novelys, nous utilisons Ruby on rails pour la partie serveur de notre application, nous avons rajouté le mime type manifest, afin d’avoir une API Rest qui sache servir ce nouveau format.

Le code suivant dans un initializer fait bien le boulot :

Mime::Type.register_alias "text/cache-manifest", :manifest

Bien, à ce stade, il faut générer notre Manifest.

Dans une applications Rails, nous allons devoir coller dans l’applicationCache : les feuilles de styles, les lib JS, les assets et suivant les cas les fichiers uploadés.

Si un fichier est modifié, il faudra impérativement que le manifest soit modifié, pour que ce fichier soit par la suite téléchargé en local.

Nous avons donc opté pour intégrer dans un commentaire une date formatée, celle de la dernière modification opérée sur les fichiers du manifest. Ainsi on est sûr que le manifest soit bien considéré comme mis à jour et que les fichiers soit téléchargé par les iPads. (nous sommes riches).

Voici le helper qui aidera à accomplir la partie chiante :

def manifest_files(options = {})
    timestamp_format = "%Y%m%d%H%M%S"

    #Assets
    assets = options[:assets] || []
    max_time = options[:max_time] || DateTime.now

    # Interface
    stylesheets = Dir["public/stylesheets/**/*.*"].map{|entry| entry.gsub(/public/, "") }
    javascripts = Dir["public/javascripts/**/*.*"].map{|entry| entry.gsub(/public/, "") }
    images      = Dir["public/images/**/*.*"].map{|entry| entry.gsub(/public/, "") }
    interface = stylesheets + javascripts + images

    if interface.present?
      max_mtime = interface.map{|file| File.mtime("public"+file)}.max
      max_time = [max_time, max_mtime].max
    end

    max_mtime = max_time.strftime(timestamp_format)

    manifest_files = (assets + interface).flatten.uniq
    return manifest_files, max_mtime
  end

Le helper exploite deux options :

  1. max_time : un timestamp arbitraire qui sera tout de même comparés au plus récent des fichiers
  2. une liste des chemins des chiers à rajouter dans le manifest (typiquement les fichiers que vous gérer avec Paperclip dans vos models).

Et on récupère :

  1. la liste des fichiers
  2. la date formatée avec le timestamp le plus récent, c’est à dire la version de note Manifest

Une actions :

def mister_manifest
    @manifest_files, @max_mtime = manifest_files()

    respond_to do |format|
      format.manifest
    end
  end

Une vue ERB:

CACHE MANIFEST
# version <%= @max_mtime %>
/
<%= @manifest_files.join("\n") %>
FALLBACK:/ /offline.html

Le manifest est maintenant généré, reste maintenant à l’intégrer dans l’application côté client.

Primo rajoutons le chemin vers le manifest, voici un exemple dans notre layout général :

!!!
%html{:manifest => application_offline_manifest_path(:format => :manifest) }
  %head
    %title MyApp
    != stylesheet_link_tag %w(sensassional)
    != javascript_include_tag %w(jquery sensassional application_cache)
    = csrf_meta_tag
  %body
    = yield

Là tout doux, maintenant il faut taper dans les API Javascripts pour synchroniser le cache en locale. Voici le code que j’ai rapidement mise en oeuvre. Rassurez vous c’est une solution temporaire (permanente) :

var req=0;
var manifest_size = 0;

var cacheStatusValues = [];
cacheStatusValues[0] = 'uncached';
cacheStatusValues[1] = 'idle';
cacheStatusValues[2] = 'checking';
cacheStatusValues[3] = 'downloading';
cacheStatusValues[4] = 'updateready';
cacheStatusValues[5] = 'obsolete';

var cache = window.applicationCache;

console.log(window);

function logEvent(e) {
  var online, status, type, message;
  req ++;
  online = (navigator.onLine) ? 'yes' : 'no';
  status = cacheStatusValues[cache.status];
  type = e.type;
  message = '' + type;
  message+= '/' + status;
  message+= '/' + req  + '/' + manifest_size ;

  if (type == 'error' && navigator.onLine) {
      message+= ' (probably a syntax error in manifest)';
      console.log(message);
      console.dir(e);
  }

}

cache.addEventListener('loadstart', logEvent, false);
cache.addEventListener('stalled', logEvent, false);
cache.addEventListener('load', logEvent, false);
cache.addEventListener('loadend', logEvent, false);
cache.addEventListener('cached', logEvent, false);
cache.addEventListener('checking', logEvent, false);
cache.addEventListener('downloading', logEvent, false);
cache.addEventListener('error', logEvent, false);
cache.addEventListener('noupdate', logEvent, false);
cache.addEventListener('obsolete', logEvent, false);
cache.addEventListener('progress', logEvent, false);
cache.addEventListener('updateready', logEvent, false);

Nous devrons sans doute encore l’enrichir, mais ça sera pour un prochain billet 🙂

Maintenant débranchez votre RJ45 ou votre connexion Wifi et magie !

Vous allez maintenant pouvoir frimer avec votre iPad sans connexion 3G.

Le prochain épisode : Backbone.js et localStorage sont dans un bateau.