Faire fonctionner les applications mobiles en mode hors-ligne avec HTML 5

Article d'origine: 

Permettre aux applications de fonctionner avec ou sans connexion Internet

Résumé: 

Une partie de l'intérêt des applications mobiles réside dans la possibilité de garder l'application et ses données avec soi partout. Mais le fait est que la mobilité impose, de temps à autre, la perte de l'accès à la connexion Internet. Cela pourrait sembler un problème insurmontable pour les applications Web mobiles. Cependant, les applications Web ont évolué et sont devenues capables de travailler hors-ligne. Dans cet article, vous apprendrez comment permettre à votre application Web mobile de travailler hors-ligne et à détecter quand l'application passe du mode connecté au mode déconnecté et vice-versa.

Prérequis

Au cours de cet article, vous allez développer des applications Web utilisant les technologies les plus récentes. Le plus gros du code est constitué de HTML, de Javascript et de CSS - les technologies de base pour tout développeur web. Ce dont vous aurez vraiment besoin, ce sont des navigateurs pour tester tout çà. La majeure partie du code de cet article tournera dans les dernières versions des navigateurs classiques, à quelques exceptions d'importance. Bien sûr, il faut tester sur des navigateurs mobiles également, et vous vous servirez des dernières versions des SDKs iPhone et Android pour ce faire. Pour cet article, les SDKs iPhone 3.1.3 et Android 2.1 ont été utilisés. Voir les Ressources pour les liens.

Pourquoi prévoir un mode hors-ligne ?

Les applications Web hors-ligne sont attrayantes pour les utilisateurs aussi bien que les développeurs pour plusieurs raisons. Beaucoup de développeurs aimeraient pouvoir écrire une application Web qui fonctionne sur tous les smartphones les plus populaires au lieu d'écrire une application native pour chaque plateforme. Mais le fait que ce soit plus pratique pour le développeur n'en fait pas nécessairement quelque chose de souhaitable pour l'utilisateur. Pour que ce soit le cas, les applications Web mobiles doivent fournir la plupart des fonctionnalités que l'on trouve dans une application mobile native. Le fait de pouvoir travailler sans connexion est sans aucun doute une de ces fonctionnalités. Certaines applications font un usage intensif de données et de services disponibles sur Internet — qu'il s'agisse d'applications mobiles Web ou natives. Pour ces applications, il est compréhensible que les fonctionnalités puissent être réduites si l'utilisateur ne dispose pas d'une bonne connexion Internet. Néanmoins, l'application ne peut pas flancher complètement simplement parce que la connexion n'est pas bonne. Avec une application Web traditionnelle, c'est exactement ce qui arriverait.

La capacité de fonctionner en mode déconnecté rapproche un peu plus les applications Web des applications natives. Mais il y d'autres avantages. Les navigateurs Web ont depuis toujours mis en cache des ressources statiques. Ils se basent sur les métadonnées des entêtes des réponses HTTP que leurs envoient les serveurs Web pour récupérer le HTML, le Javascript, la CSS et les images nécessaires au rendu de la page. Si tout ce dont la page a besoin est en cache, elle se charge très rapidement. Cependant, si quelque chose n'est pas dans le cache, cela peut ralentir considérablement l'ensemble. Cela arrive plus souvent que vous ne pourriez le croire. Peut-être qu'un fichier CSS avait un entête Cache-Control différent des autres fichiers, ou bien que le navigateur l'a supprimé du cache parce qu'il manquait de place.

Avec les applications déconnectées, vous êtes sur que tout est en cache. Les navigateurs vont toujours tout charger du cache, bien que vous puissiez également spécifier ce qui ne devrait pas pris dedans. Une astuce Ajax courante consiste à ajouter un paramètre d'horodatage supplémentaires aux requêtes GET (ou pire à faire un POST alors qu'un GET serait plus approprié) pour éviter aux navigateurs de mettre la réponse en cache. Vous n'aurez pas besoin de cette astuce dans des applications Web gérant un mode hors-ligne.

Les applications hors-ligne ont l'air très intéressantes, donc il doit être complexe d'en créer, n'est-ce-pas ? En fait, c'est assez simple. Il faut faire trois choses:

  1. Créer un fichier de manifeste pour le mode hors-ligne
  2. Mettre le navigateur au courant de l'existence de ce fichier
  3. Positionner le type MIME sur le serveur

 

Le manifeste du mode hors-ligne

Le fichier clé est donc le manifeste de cache de l'application. Ce fichier indique précisément au navigateur ce qu'il doit mettre en cache (et éventuellement, ce qu'il ne doit pas y mettre). Cela est fondamental pour l'application. Le Listing 1 donne un exemple de manifeste de cache simple.

Listing 1. Manifeste de cache simple

CACHE MANIFEST
# Version 0.1
offline.html
/iui/iui.js
/iui/iui.css
/iui/loading.gif
/iui/backButton.png
/iui/blueButton.png
/iui/cancel.png
/iui/grayButton.png
/iui/listArrow.png
/iui/listArrowSel.png
/iui/listGroup.png
/iui/pinstripes.png
/iui/redButton.png
/iui/selection.png
/iui/thumb.png
/iui/toggle.png
/iui/toggleOn.png
/iui/toolbar.png
/iui/whiteButton.png
/images/gymnastics.jpg
/images/soccer.png
/images/gym.jpg
/images/soccer.jpg

Ce fichier liste tous les fichiers dont l'application a besoin pour fonctionner correctement. Cela comprend les fichiers HTML aussi bien que les fichiers Javascript, CSS et les images. Il pourrait aussi référencer des fichiers vidéos, PDF, XML et ainsi de suite. Notez que toutes les URLs de cet exemple sont relatives. Toute URL relative l'est  par rapport au fichier de manifeste de cache. Dans ce cas, le fichier de manifeste de cache se trouve à la racine de l'application Web. Comparez la structure des répertoires du Listing 2 avec les URLs relatives du Listing 1.

Listing 2. Version textuelle de la structure des répertoires de l'application Web

Name
V images
    gymnastics.jpg
    soccer.png
V iui
    backButton.png
    blueButton.png
    cancel.png
    grayButton.png
    iui.css-logo-touch-icon.png
    iui.css
    iui.js
    iuix.css
    iuix.js
    listArrow.png
    listArrowSel.png
    listGroup.png
    loading.gif
    pinstripes.png
    redButton.png
    selection.png
    thumb.png
    toggle.png
    toggleOn.png
    toolbar.png
    toolButton.png
    whiteButton.png
  manifest.mf
  offline.html
> WEB-INF

Vous avez peut-être remarqué que cette application utilise le framework iUI. C'est un kit Javascript + CSS populaire qui permet de donner aux applications Web mobiles le look and feel (ndT: désolé pour l'anglicisme!) des applications iPhone natives. Comme vous pouvez le constater à la lecture des Listings 1 et 2, ce framework utilise quelques images en plus de fichiers Javascript et CSS. Néanmoins, tous ces fichiers seront mis en cache par le navigateur et seront disponibles en mode hors-ligne aussi longtemps qu'ils seront listés dans le manifeste.

Une autre chose critique que l'on peut noter dans le Listing 1 est l'information de version. Ca ne fait pas partie de la spécification. En fait, il  s'agit juste d'un commentaire dans le fichier. Cependant, c'est la clé qui va permettre d'indiquer au navigateur qu'il existe une nouvelle version de l'application. Supposons que l'on aie modifié du HTML, du Javascript ou une simple image. Si l'on ne change pas le manifeste, le navigateur n'ira jamais charger la nouvelle version de la ressource modifiée. Il n'existe pas de date d'expiration dans un manifeste de cache, donc tout reste en cache jusqu'à ce que l'utilisateur le vide ou que le fichier de manifeste change. Le navigateur vérifie s'il existe un nouveau fichier de manifeste. Pour indiquer qu'il s'agit d'un nouveau manifeste, il suffit de modifier quelque chose (n'importe quoi) dans le fichier de manifeste existant. Pour revenir à l'exemple dans lequel on modifier le HTML de la page, si l'on a également modifié la chaine de version dans le manifeste, le navigateur saura que les ressources ont été modifiées et qu'il doit les télécharger à nouveau. Mettre un numéro de version en commentaire est un moyen simple pour gérer ce cycle de vie.

Signaler au navigateur l'existence du manifeste

Il manque encore une pièce pour activer le cache hors-ligne de l'application Web. Le navigateur doit être au courant que l'on souhaite activer le cache et où trouver le fichier de manifeste de cache. Le Listing 3 montre commenet faire cela très facilement.

Listing 3. Page Web gérant le mode déconnecté

<!DOCTYPE html>
<html>
<html manifest="manifest.mf">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <meta name="viewport" content="width=device-width; initial-scale=1.0; 
        maximum-scale=1.0; user-scalable=0;"/>
    <meta name="apple-touch-fullscreen" content="YES" />
    <link rel="apple-touch-icon" href="/iui/iui-logo-touch-icon.png" />
    <style type="text/css" media="screen">@import "/iui/iui.css";</style>
    <script type="application/x-javascript" src="/iui/iui.js"></script>
    <title>Let's do it offline</title>
</head>
<body>
    <div class="toolbar">
        <h1 id="pageTitle">Going offline</h1>
        <a id="backButton" class="button" href="#"></a>
    </div>
    <ul id="menu" title="Sports" selected="true">
        <li><a href="#gym"><img height="80" width="80" 
src="/images/gym.jpg" align="middle"/>
            <span style="display:inline-block;
 vertical-align:middle">Gymnastics</span></a></li>
        <li><a href="#soccer"><img src="/images/soccer.jpg" 
align="middle"/>
        <span style="display:inline-block; 
vertical-align:middle">Soccer</span></a></li>
    </ul>
    <div id="gym" title="Gymnastics" class="panel">
        <img src="/images/gymnastics.jpg" alt="Boys Gymnastics"/>
    </div> 
    <div id="soccer" title="Soccer" class="panel">
        <img src="/images/soccer.png" alt="Boys Soccer"/>
    </div>
</body>
</html>

La caractéristique la plus importante de ce HTML est l'élément racine html. Notez qu'il a maintenant un attribut nommé manifest. C'est ce qui indique au navigateur que cette page web peut fonctionner hors-ligne. La valeur du paramètre manifest est l'URL du fichier de manifeste de cache pour la page Web. Une fois de plus, ce peut être une URL complète, bien que dans ce cas ce soit une URL relative (à la page Web). Une autre chose à noter est la DOCTYPE de la page. Il s'agit de la déclaration de type de document canonique pour les pages HTML 5. La spécification du mode déconnecté des applications Web ne vous oblige pas à utiliser cette DOCTYPE, cependant il est recommandé de le faire. Sinon, certains navigateurs pourraient ne pas identifier cette page à une page HTML 5 et ignorer le manifeste de cache. Le reste de ce code HTML est juste un exemple simple d'utilisation de iUI. La Figure 1 montre à quoi cette page ressemble sur le simulateur iPhone.

Figure 1. Application Web hors-ligne sur le simulateur iPhone

Application Web hors-ligne sur le simulateur iPhone

Tester une application hors-ligne peut être un peu compliqué. Si vous le pouvez, le moyen le plus simple est de déployer l'application sur un serveur Web. Puis vous accédez à la page une seule fois, vous coupez la connexion Internet, et essayez d'y accéder à nouveau. Si quelque chose cloche, alors il se pourrait que vous ayez oubliez de spécifier certains fichiers dans le manifeste de cache. Avant d'essayer cela, il vous faudra effectuer une configuration de première importance sur le serveur Web.

Configuration du serveur Web

Le Listing a montré que l'on indique où se trouve le fichier de manifeste de cache en utilisant l'attribut manifest de l'élément racine de la page Web. Cependant, la spécification des manifestes de cache impose au navigateur d'effectuer une étape supplémentaire de validation quand il télécharge et traite un manifeste de cache. Il doit vérifier que le type MIME de ce fichier est bien text/cache-manifest. Cela signifie en règle générale qu'il faut configurer le serveur Web de façon à positionner ce type MIME sur un certain fichier de manière statique, ou bien qu'il faut écrire du code qui crée dynamiquement le fichier et positionne le type MIME. La première solution est certainement la plus efficace, mais parfois il faut passer par la seconde option si on n'a pas la main sur la configuration du serveur (par exemple pour un serveur partagé ou hébergé). Si vous contrôlez le serveur et si vous utilisez un serveur d'application Java™, vous pouvez configurer cela dans fichier web.xml de l'application Web. Le Listing 4 en montre un exemple.

Listing 4. Configuration du web.xml pour positionner les types MIME

<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<!-- Servlets go here -->
    <mime-mapping>
        <extension>mf</extension>
        <mime-type>text/cache-manifest</mime-type>
    </mime-mapping>    
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
</web-app>

Ici, la section clé est bien entendu l'élément mime-mapping. Dans notre cas, on spécifie que tous les fichiers finissant par l'extension .mf auront le type MIME text/cache-manifest. Bien sûr, il sera encore plus efficace de servir ces fichiers au moyen d'un serveur dédié à la fourniture de contenu statique comme un serveur Web Apache. Pour une installation Apache classique, il faudra simplement modifier le fichier mime.types se trouvant dans le répertoire httpd/conf comme dans le Listing 5.

Listing 5. Positionnemen du type MIME dans mime.types

# This file controls what Internet media types are sent to the client for
# given file extension(s).  Sending the correct media type to the client
# is important so they know how to handle the content of the file.
# Extra types can either be added here or by using an AddType directive
# in your config files. For more information about Internet media types,
# please read RFC 2045, 2046, 2047, 2048, and 2077.  The Internet media type
# registry is at <http://www.iana.org/assignments/media-types/>.

# MIME type                     Extensions
text/cache-manifest             mf
# many more mappings...

Dans ces deux exemples, on utilise l'extension mf pour le fichier de manifeste, puisqu'il s'appelait manifest.mf. C'est complètement arbitraire. On aurait pu l'appeler .manifest ou .foo, tant que l'extension du fichier de manifeste correspond à l'extension définie dans le mapping du fichier de configuration. Notez que d'autres applications ou serveurs Web peuvent avoir des mécanismes de configuration différents. Maintenant que vous avez vu les composantes de base de la création du mode hors-ligne des applications Web au moyen de HTML 5, voyons un exemple plus compliqué afin d'explorer plus avant les capacités des applications Web mobiles hors-ligne.

Exemple avancé

Dans l'exemple précédent, tout le contenu était statique. C'était sympathique de pouvoir visualiser l'ensemble en mode hors-ligne. Une application un peu plus représentative devrait lire des données dynamiquement depuis son serveur et des services Web. Pour rendre l'exemple plus réaliste, ajoutons-y des données provenant de Twitter. Si vous avez lu les précédents articles de la série, cela vous sera familier. Pour commencer, regardons le code HTML modifié pour cet exemple dans le Listing 6.

Listing 6. Code HTML modifié

<body onload="init()">
    <div class="toolbar">
        <h1 id="pageTitle">Going offline</h1>
        <a id="backButton" class="button" href="#"></a>
    </div>
    <ul id="menu" title="Sports" selected="true">
        <li><a href="#gym">
            <img height="80" width="80" src="/images/gym.jpg" align="middle"/>
            <span style="display:inline-block; vertical-align:middle">Gymnastics</span>
        </a></li>
        <li><a href="#soccer"><img src="/images/soccer.jpg" align="middle"/>
            <span style="display:inline-block; vertical-align:middle">Soccer</span>
        </a></li>
        <li id="online" style="display: none"><img src="/images/online.jpg"/></li>
    </ul>
    <ul id="gym" title="Gymnastics"></ul> 
    <ul id="soccer" title="Soccer"></ul>
</body>

La principale différence est que les éléments gym et soccer sont maintenant des listes, et sont vides. Nous les remplirons avec des tweets sur la gymnastique et le football. Notez également un item de liste avec l'id online qui contient une image servant à indiquer à l'utilisateur si l'application est connectée ou pas. Cependant cet élément est caché par défaut, ce qui veut dire que le mode par défaut est le mode déconnecté. L'élément body spécifie une fonction init qui doit être appelée lors du chargement du corps de la page. Le Listing 7 montre cette fonction.

Listing 7. Code Javascript d'initialisation de la page

function init(){
    if (navigator.onLine){
        searchTwitter("gymnastics", "showGymTweets");
        searchTwitter("soccer", "showSoccerTweets");
        $("online").style.display = "inline";
    } 
    gymTweets = localStorage.getItem("gymnastics");
    if (gymTweets){
        gymTweets = JSON.parse(gymTweets);
        showGymTweets();
    }
    soccerTweets = localStorage.getItem("soccer");
    if (soccerTweets){
        soccerTweets = JSON.parse(soccerTweets);
        showSoccerTweets();
    }
    document.body.addEventListener("online", function() {
            $("online").style.display= "inline";
            applicationCache.update();
            applicationCache.addEventListener("updateready", function() {
                applicationCache.swapCache();
            }, false);
        }, false);
    document.body.addEventListener("offline", function() {
        $("online").style.display = "none";
    }, false);        
}

La première chose que fait ce code est de tester si l'on est connecté ou pas. Si on est connecté, alors l'image de connexion est affichée. Plus important encore, si on est connecté les données sont chargées depuis Twitter via l'appel de la fonction searchTwitter. Encore une fois, il s'agit d'une technique (expliquée dans les articles précédents de cette série) qui permet de faire une recherche Twitter directement dans le navigateur en utilisant JSONP. Ensuite, on essaie de charger les tweets depuis localStorage. Si vous ne connaissez pas bien localStorage, il s'agit d'une autre fonctionnalité de HTML 5 qui complète assez bien le mode hors-ligne. Consultez le second article de la série pour en savoir plus à ce sujet. Remarquez qu'après les deux nouvelles recherches (qui se déclenchent si on détecte le mode connecté) et le chargement des tweets sauvegardés en local, les fonctions showGymTweets et showSoccerTweets sont invoquées. Ces fonctions se ressemblent, et le Listing 8 donne le code de showGymTweets.

Listing 8. Afiicher les tweets concernant la gymnastique

function showGymTweets(response){
    var gymList = $("gym");
    gymList.innerHTML = "";
    if (gymTweets){
        if (response){
            gymTweets = response.results.reverse().concat(gymTweets);
        }
    } else {
        gymTweets = response.results.reverse();
    }
    showTweets(gymTweets, gymList);
    localStorage.setItem("gymnastics", JSON.stringify(gymTweets));
}

Cette fonction peut afficher des tweets sauvegardés en local, des nouveaux tweets envoyés par Twitter, ou une combinaison des deux si les deux existent. Plus important, elle sauvegarde tout localement, construisant ainsi un cache local de tweets. Il s'agit là de code très classique pour gérer à la fois les données du cache local et des données récentes venant des serveurs. Il permet à l'application de réagir correctement que l'on soit connecté ou pas.

Si nous revenons au Listing 7, la dernière chose que l'on fait est d'enregistrer des gestionnaires d'événements. On s'enregistre pour les événements d'entrée en mode connecté et déconnecté. L'application sera avertie quand l'état en ligne ou hors-ligne du navigateur change. A tout le moins, on peut afficher ou pas l'image indiquant que l'on est connecté. Dans le cas où l'application passe en mode connecté après avoir été déconnectée, on accède à l'objet applicationCache. C'est un objet représentant toutes les ressources qui ont été mises en cache suite à leur déclaration dans le manifeste de cache. Dans ce cas, on appelle sa méthode update. Elle demande au navigateur de vérifier s'il détecte une mise à jour d'applicationCache. Comme nous l'avons dit précédemment, le navigateur regarde d'abord si le fichier de manifeste de cache a été modifié. On ajoute un autre écouteur d'événement pour être mis au courant de la disponibilité d'une mise à jour du cache. Dans ce cas, on appelle la méthode swapCache d'applicationCache. Cela rechargera les fichiers spécifiés dans le manifeste de cache.

A propose du manifeste de cache, il reste un dernier point à voir dans cet exemple avancé. Le manifeste de cache doit être modifié pour correspondre au Listing 9.

Listing 9. Manifeste de cache modifié

CACHE MANIFEST
# Version 0.2
CACHE:
offline.html
json2.js
/iui/iui.js
/iui/iui.css
/iui/loading.gif
/iui/backButton.png
/iui/blueButton.png
/iui/cancel.png
/iui/grayButton.png
/iui/listArrow.png
/iui/listArrowSel.png
/iui/listGroup.png
/iui/pinstripes.png
/iui/redButton.png
/iui/selection.png
/iui/thumb.png
/iui/toggle.png
/iui/toggleOn.png
/iui/toolbar.png
/iui/whiteButton.png
/images/gym.jpg
/images/soccer.jpg
/images/online.jpg

NETWORK:
http://search.twitter.com/

Ici, nous avons explicitement ajouté une section CACHE au manifeste. Le manifeste peut contenir différentes sections, mais s'il n'en a qu'une, on considère alors qu'il s'agit de CACHE et elle peut donc être ôtée. La raison pour laquelle nous sommes explicites ici est que l'on a également une section NETWORK. Elle indique au navigateur que tout ce qui provient du domaine spécifié (en l'occurence search.twitter.com) devrait être récupéré du réseau et ne devrait jamais être mis en cache. Puisque l'on sauvegarde localement les résultats de recherche fournis par Twitter, on ne veut certainement pas que le navigateur fasse de manière indirecte des mises en cache des résultats des requêtes. Maintenant que tout cela est en place, l'application récupérera si elle le peut les nouveaux tweets depuis Twitter, mais elle les stockera aussi et les mettra à la disposition de l'utilisateur même si l'appareil perd la connexion.

Résumé

Les applications Web ont fait énormément de chemin depuis l'époque de Mosaic. Les applications Web mobiles ont évolué encore davantage. Le temps des téléphones WAP qui ne parlaient que le WML (Wireless Markup Language) est presque révolu. On demande maintenant aux navigateurs mobiles des choses que l'on n'aurait pas osé demandé à leurs ainés de bureau. Le mode hors-connexion en fait partie. Les standards spécifiés par HTML 5 font beaucoup pour permettre aux développeurs de fournir un mode déconnecté à leurs applications Web mobiles. Dans le prochain article de cette série nous verrons comment un autre standard de la norme HTML 5, les Web Workers, peuvent améliorer de maière impressionnante les performances des applications Web mobiles.