Créer des applications mobiles avec HTML 5

HTML 5 est une technologie dont on fait beaucoup de publicité, mais il y a de bonnes raisons à cela. Elle promet une révolution technologique qui apportera au navigateur web des possibilités jusque là réservées aux applications traditionnelles. Mieux, les navigateurs mobiles les plus populaires ont déjà adopté et implémenté des portions significatives de la spécification HTML 5. Dans cette série de cinq articles, vous examinerez quelques unes de ces technologies qui font partie de HTML 5, et qui peuvent avoir un impact énorme sur le développement d'applications mobiles. Dans chaque partie de cette série, vous développerez une application web mobile tout à fait viable qui illustrera une fonctionnalité de HTML 5 pouvant être utilisée dans un navigateur mobile moderne, comme ceux que l'on trouve sur les iPhones ou les matériels basés sur Android.

Combiner HTML 5, les APIs de géolocalisation et des web services pour créer des mashups mobiles

Article d'origine: 

Trouver et tracer les coordonnées géographiques pour les utiliser avec toutes sortes de web services

Résumé: 

Dans la première des cinq parties de cette série, vous utiliserez une plus populaires des nouvelles technologies disponibles pour les applications web mobiles: la géolocalisation. Les smartphones haut de gamme incluent tous un GPS, et vous verrez comment le mettre à profit dans une application web. Vous comprendrez comment vous servir de différentes facettes du standard de géolocalisation et comment l'associer à un web service bien connu pour créer un mashup mobile intéressant.

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 de tout développeur web. Ce dont vous aurez vraiment besoin, ce sont des navigateurs pour tester tout çà. Pour cet article, il est recommandé d'utiliser Mozilla Firefox 3.5+, puisque ce navigateur supporte la géolocalisation. 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.

Les bases: se repérer

La géolocalisation en elle-même est quelque chose d'assez nouveau. Cela permet de déterminer où l'utilisateur se trouve. Cependant, obtenir cette information pour la redonner à l'utilisateur ne serait pas très utile. Qui se soucie de sa latitude et de sa longitude exacte ? C'est quand on commence à l'utiliser en combinaison avec d'autres données et des services qui en font usage que l'on obtient des résultats intéressants. La plupart de ces services demandent qu'on leur fournisse entre autres la latitude et la longitude de l'utilisateur. Souvent c'est tout ce dont vous aurez besoin, voyons donc comment obtenir ces informations. Le Listing 1 montre l'API Javascript standard pour ceci.

Listing 1. Repérer un utilisateur : getCurrentPosition

navigator.geolocation.getCurrentPosition(
    successCallback, errorCallback, options);

C'est l'API de base de la géolocalisation. Pour un grand nombre d'applications, c'est la seule chose dont vous aurez jamais besoin. L'objet geolocation est partie intégrante de l'objet navigator standard. Il expose quelques méthodes, mais la plus utilisée est getCurrentPosition. Accéder à la position d'un utilisateur est une opération couteuse (il est possible que vous deviez contacter un satellite pour cela !) et qui requiert le consentement de l'utilisateur, c'est pourquoi il s'agit d'une opération asynchrone. Ses paramètres sont des callbacks: un pour le succès, l'autre pour l'échec.

La fonction appelée en cas de succès sera appelée avec un seul paramètre de type Position. Cet objet a deux propriétés: une propriété d' horodatage et une propriété coords de type Coordinates. Un objet Coordinates possède plusieurs propriétés:

Toutes ces propriétés ne sont pas disponibles sur tous les appareils, à l'exception de latitude, longitude et accuracy. Si l'API de géolocalisation est supportée et que l'appareil arrive à déterminer sa position, vous pouvez compter sur ces trois là.

Le callback d'erreur est appelé avec un seul paramètre de type PositionError. Une instance de PositionError a deux propriétés:code et message. Le message est spécifique à l'appareil, et est utile pour débugger. Le code devrait avoir une de ces trois valeurs:

Votre application devrait se fier à ce code pour afficher à l'utilisateur un message d'erreur adapté.

Il est à noter que la spécification du W3C permet aussi de passer un troisième paramètre constitué d'options. Elles incluent des choses comme un délai maximal de réponse à la demande de géolocalisation. Cependant, ce n'est pas encore supporté par des appareils tels que l'iPhone et son utilisation n'est donc pas recommandée. Maintenant que nous avons vu l'API en détail, voyons sur un exemple simple comment l'utiliser.

Intégration avec Twitter

En ce moment, le "hello world" des mashups consiste à utiliser Twitter d'une manièr ou d'une autre. Pour ce premier exemple, nous allons utiliser l'API de recherche de Twitter. Elle permet de chercher les tweets émis dans un certain rayon autour d'une position donnée. Le Listing 2 montre comment utiliser cette recherche Twitter.

Listing 2. Recherche de tweets autour d'une position

<!DOCTYPE html>
<html>
<head>
<meta name = "viewport" content = "width = device-width"/>
<title>Local Twitter Search</title>
<script type="text/javascript">
    function startSearch(){
        var gps = navigator.geolocation;
        if (gps){
            gps.getCurrentPosition(searchTwitter, 
                   function(error){
                alert("Got an error, code: " + error.code + " message: " 
+ error.message);
             });
        } else {
            searchTwitter();
        }
    }
    function searchTwitter(position){
        var query = 
            "http://search.twitter.com/search.json?callback=showResults&q=";
        query += $("kwBox").value;
        if (position){
            var lat = position.coords.latitude;
            var long = position.coords.longitude;
            query += "&geocode=" + escape(lat + "," + long + ",50mi");
        }
        var script = document.createElement("script");
        script.src = query;
        document.getElementsByTagName("head")[0].appendChild(script);
    }
</script>
</head>
<body>
    <div id="main">
        <label for="kwBox">Search Twitter:</label>
        <input type="text" id="kwBox"/>
        <input type="button" value="Go!" onclick="startSearch()"/>
    </div>
    <div id="results">
    </div>
</body>
</html>

L'utilisateur saisit un terme à rechercher dans la zone de texte. Un clic sur le bouton appelle la fonction startSearch. C'est là que l'API de géolocalisation est utilisée. D'abord on teste sa disponibilité. Si c'est le cas, on appelle la fonction getCurrentPosition. Le callback de succès est la fonction searchTwitter. Le callback d'erreur est une simple fermeture qui affiche un message d'information.

La fonction searchTwitter est appelée quand la navigateur a réussi à déterminer la position. On utilise la position passée à la fonction pour ajouter un paramètre geocode à la requête de recherche Twitter. L'exemple du Listing 2 recherche les tweets postés à moins de 50 miles du lieu déterminé. Pour appeler Twitter, on utilise un tag de script inséré dynamiquement, une technique souvent appelée JSONP (JSON with Padding). Cette technique est supportée par l'API de recherche de Twitter, et vous permet de faire une recherche Twitter directement de votre navigateur, sans aucun appel serveur. On peut le voir au paramètre callback de la requête. Notez qu'il est positionné à showResults qui est le nom de la fonction à invoquer. On ne la voit pas dans le Listing 2, car ça fait partie de l'IHM, mais vous pouvez la trouver dans le code source de cet article (voir les Ressources). La Figure 1 est une capture d'écran du code du Listing 2 tournant sur un iPhone.

Figure 1. Recherche Twitter depuis un iPhone

Recherche Twitter depuis un iPhone

Cette application, comme beaucoup d'autres applications gérant la position, n'a besoin d'y accéder qu'une seule fois. Cependant, d'autres applications auront besoin de tracer les utilisateurs pendant leurs déplacements. Ces applications nécessiteront l'utilisation de l'autre API de géolocalisation, plus avancée.

Une utilisation plus avancée: le traçage

Parfois, l'application n'a pas seulement besoin de la position courante de l'utilisateur, mais doit aussi être avertie des déplacements de l'utilisateur. Il existe pour cela une API appelée watchPosition. Elle est très similaire à getCurrentPosition, car elle prend les mêmes paramètres. La seule différence majeure est qu'elle retourne un ID. Cet ID est utilisé en conjonction avec la dernière API de géolocalisation, clearWatch. Cette fonction prend en entrée l'ID obtenu avec watchPosition. En fait, quand on appelle watchPosition, le navigateur continue à envoyer des mises à jour au callback de succès que vous lui avez passé, jusqu'à ce que clearWatch soit appelée. Récupérer continuellement la position d'un utilisateur tire beaucoup sur la batterie d'un appareil mobile, il faut donc utiliser ces APIs avec précaution. Voyons maintenant un exemple.

Intégration avec Google Maps

Dans cet exemple, nous allons interagir avec les APIs Map de Google. Elle sont optimisées pour une utilisation à partir de mobiles, et plus particulièrement pour les plateformes iPhone et Android. Cela les rend très attrayantes pour les développeurs Web mobile, surtout si vous créez des applications utilisant la géolocalisation. L'application d'exemple va tout simplement montrer où se trouve l'utilisateur sur une carte, en la mettant à jour à chaque déplacement de l'utilisateur. Le Listing 3 montre le code qui permet cela.

Listing 3. Application de localisation sur une carte

<html> 
<head> 
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/> 
<title>I'm tracking you!</title> 
<script type="text/javascript" src="http://maps.google.com/maps/api/js?
     sensor=true"></script> 
<script type="text/javascript">
    var trackerId = 0;
    var geocoder;
    var theUser = {};
    var map = {};
    function initialize() {
        geocoder = new google.maps.Geocoder();
        if (navigator.geolocation){
            var gps = navigator.geolocation;
            gps.getCurrentPosition(function(pos){
                var latLng = new google.maps.LatLng(pos.coords.
latitude,pos.coords.longitude);
                var opts = {zoom:12, center:latLng, mapTypeId: 
google.maps.MapTypeId.ROADMAP};
                map = new google.maps.Map($("map_canvas"), opts);
                theUser = new google.maps.Marker({
                    position: latLng,
                    map: map,
                    title: "You!"
                });
                showLocation(pos);
            });
            trackerId = gps.watchPosition(function(pos){
                var latLng = new google.maps.LatLng(pos.coords.latitude,pos.
coords.longitude);
                map.setCenter(latLng);
                theUser.setPosition(latLng);
                showLocation(pos);
            });
        }
  }
</script> 
</head> 
<body style="margin:0px; padding:0px;" onload="initialize()"> 
    <div id="superbar">      
        <span class="msg">Current location: 
              <span id="location"></span>
          </span>
          <input type="button" value="Stop tracking me!" 
onclick="stopTracking()"/>
      </div>
  <div id="map_canvas" style="width:100%; height:90%; float:left; 
border: 1px solid black;">
  </div> 
</body> 
</html>

Quand le corps du document est chargé, la fonction initialize est appelée. Cette fonction vérifie que la géolocalisation est supportée par le navigateur. Si c'est le cas, elle appelle getCurrentPosition, d'une manière similaire à ce qui est fait dans l'exemple du Listing 2. Quand une position est déterminée, elle crée une carte en utilisant l'API Google Map. Remarquez que la latitude et la longitude servent à créer une instance de google.maps.LatLng. Cet objet permet de centrer la carte. Ensuite, on crée un marqueur sur la carte qui représente la position courante de l'utilisateur. Encore une fois, le marqueur utilise la latitude et la longitude transmises par l'API de géolocalisation.

Une fois qu'on a créé la carte et placé un marqueur dessus, on commence à pister l'utilisateur. On enregistre l'ID retourné par watchPosition. Quand la position a changé, on recentre la carte sur cette nouvelle position et on déplace le marqueur à cette position. Le Listing 4 montre deux fonctions supplémentaires qu'il est intéressant d'examiner.

Listing 4. Fonctions de géoréférencement et d'annulation du traçage

function showLocation(pos){
    var latLng = new google.maps.LatLng(pos.coords.latitude,pos.coords.longitude);
    if (geocoder) {
        geocoder.geocode({'latLng': latLng}, function(results, status) {
          if (status == google.maps.GeocoderStatus.OK) {
            if (results[1]) {
                $("location").innerHTML = results[1].formatted_address;
            } 
          } 
        });
      }        
}
function stopTracking(){
    if (trackerId){
        navigator.geolocation.clearWatch(trackerId);
    }
}

Dans le Listing 3, la fonction showLocation est appelée quand la carte est dessinée la première fois et quand on reçoit une mise à jour de la position de l'utilisateur. On voit cette fonction dans le Listing 4. Elle utilise une instance de google.maps.Geocoder (créée au début de la fonction initialize du Listing 3). Cette API sert à faire du géoréférencement, c'est à dire transformer une adresse en coordonnées géographiques (latitude et longitude). Elle sait aussi faire du géoréférencement inverse — prendre des coordonnées géographiques et retourner une adresse physique. Dans notre cas, nous utilisons l'API de Google Maps pour faire du géoréférencement inverse à partir des coordonnées produites par l'API de géolocalisation. Le résultat est ensuite affiché à l'écran.

Dans le Listing 4, on voit en dernier lieu la fonction stopTracking. Elle est appelée quand l'utilisateur clique sur le bouton créé par le code HTML du Listing 3. On utilise ici le trackerId  obtenu lors de l'appel précédent à la fonction watchPosition. On le passe simplement à la fonction clearWatch et à partir de là le navigateur cessera de récupérer la position de l'utilisateur et d'appeler le code Javascript. La Figure 2 montre une capture d'écran de l'application de traçage en fonctionnement.

Figure 2. Application de traçage

Application de traçage

Bien entendu, pour vraiment tester le traçage, il faut se déplacer. Google App Engine peut être un outil utile pour cela, car il permet de transférer l'application web à un endroit atteignable par tous. Vous pouvez ensuite tester directement depuis votre appareil pourvu que vous disposiez d'une bonne connexion. A cette condition, vous pouvez prendre les transports publics ou vous faire conduire par quelqu'un et regarder votre application web répondre à vos déplacements.

Conclusion

Cet article vous a montré comment utiliser les APIs de géolocalisation dans une application web mobile. Utiliser le GPS pourrait sembler sexy, mais compliqué. Cependant, comme vous l'avez vu, le standard du W3C pour la géolocalisation fournit une API très simple. Il est facile de récupérer la position de l'utilisateur et de le tracer dans ses déplacements. A partir de là, vous pouvez passer ces coordonnées à une grande variété de Web Services qui utilsent le positionnement, ou peut-être à votre propre service en cours de développement. Dans la seconde partie de cette série, vous verrez comment tirer parti du stockage local pour améliorer les performances des applications Web mobiles.

Utiliser le stockage local dans des applications mobiles avec HTML 5

Article d'origine: 

Accélérez vos applications mobiles avec le stockage local standardisé

Résumé: 

Un des apports les plus utiles de HTML 5 est la standardisation du stockage local. Enfin, les développeurs web vont pouvoir arrêter d'essayer de faire tenir toutes les données côté client dans des cookies de 4Ko. Désormais il est possible de stocker de grosses quantités de données sur le client avec une API simple. C'est un mécanisme parfait pour le cache, ce qui permet d'améliorer considérablement la vitesse de l'application — un facteur critique pour les applications web mobiles qui dépendent de connexions beaucoup plus lentes que leurs équivalents de bureau. Dans le second article de cette série sur HTML 5, vous verrez comment utiliser le stockage local, comment le débugger, et un florilège de façons de l'utiliser pour améliorer les applications web mobiles.

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.

ABC du stockage local

Les développeurs web se battent depuis des lustres avec le stockage de données sur le client. Les cookie HTTP ont sans doute été utilisés de manière abusive pour cela. Les développeurs ont entassé une somme de données incroyable dans les 4Ko permis par la spécification HTTP. La raison en est simple. Les applications web interactives ont besoin de stocker des données pour différentes raisons, et il est souvent inefficace, peu sûr ou inapproprié de stocker ces données sur un serveur. Au fil des ans, on a proposé plusieurs solutions à ce problème. Certains navigateurs ont introduit des APIs de stockage propriétaires. Des développeurs ont fait un usage exhaustif des capacités de stockage du Flash Player en les exposant à travers Javascript. Comme de bien entendu, des librairies Javascript ont essayer de lisser ces différences. En d'autres termes, ces librairies fournissent une API simple, et puis regardent quelles fonctions de stockage existent (que ce soit une API propriétaire du navigateur ou un plugin comme Flash).

Heureusement pour les développeurs web, la spécification HTML 5 contient enfin un standard pour le stockage local qui est implémenté par une large palette de navigateurs. En fait, ce standard a été un de ceux qui ont été le plus vite adoptés et est supporté par les dernières versions de tous les navigateurs majeurs: Microsoft® Internet Explorer®, Mozilla Firefox, Opera, Apple Safari, et Google Chrome. Plus important encore pour les développeurs d'applications mobiles, ce standard est supporté par tous les navigateurs basés sur WebKit comme ceux que l'on trouve sur iPhone ou les téléphones Android (à partir de la version 2.0), ainsi que dans d'autres navigateurs mobiles comme Mozilla Fennec. Cela étant posé, jetons un coup d'oeil à l'API.

Les APIs Storage

L'API localStorage est assez simple. En fait, dans la spécification HTML 5, elle implémente l'interface Storage du DOM. La raison de cette distinction est que HTML 5 spécifie deux objets distincts qui implémentent cette interface, localStorage et sessionStorage. L'objet sessionStorage est une implémentation de Storage qui stocke les données pendant la session uniquement. Plus précisément, dès que plus aucun script actif ne peut accéder à sessionStorage, le navigateur peut l'effacer à sa guise. Cela contraste avec localStorage, qui englobe plusieurs sessions utilisateur. Les deux objets partagent la même API, je me concentrerai donc uniquement sur localStorage.

L'API Storage manipule une structure de données classique de type clé / valeur. Les méthodes les plus couramment utilisées sont getItem(name) et setItem(name, value). Elles fonctionnent exactement comme vous vous y attendez: getItem retourne une valeur associée à ce nom ou null si rien n'existe, tandis que setItem ajoute une paire clé / valeur à localStorage ou remplace une valeur existante. Pour compléter cela, la méthode key(index) retourne le nom associé à la n-ième clé du stockage.

Avec ces APIs très simples, il est possible de faire beaucoup de choses déjà. Entre autres exemples, personnaliser un site ou tracer le comportement de l'utilisateur. Ce sont des applications importantes pour les développeurs web, mais il en existe une encore plus significative: le cache. Avec localStorage, vous pouvez facilement mettre en cache sur la machine du client des données provenant des serveurs. Ceci permet d'éviter d'attendre la réponse à des appels serveur potentiellement longs et minimise la somme de donnée fournies par les serveurs. Il est temps de prendre un exemple qui montre comment utiliser localStorage pour mettre en oeuvre ce genre de cache.

Exemple: création d'un cache avec localStorage

Cet exemple continue dans la lancée de l'exemple que vous avez commencé à développer dans la première partie de cette série. Il montrait comment effectuer une recherche Twitter en récupérant la position de l'utilisateur avec les APIs de géolocalisation. Partons de cet exemple, simplifions-le et améliorons ses performances. Pour commencer, retirons de l'exemple la géolocalisation pour faire une recherche Twitter simple. Le Listing 1 montre le résultat.

Listing 1. Recherche Twitter de base

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name = "viewport" content = "width = device-width"/>
<title>Basic Twitter Search</title>
<script type="text/javascript">
    function searchTwitter(){
        var query = "http://search.twitter.com/search.json?callback
=showResults&q=";
        query += $("kwBox").value;
        var script = document.createElement("script");
        script.src = query;
        document.getElementsByTagName("head")[0].appendChild(script);
    }
    // ui code deleted for brevity
    function showResults(response){
        var tweets = response.results;
        tweets.forEach(function(tweet){
            tweet.linkUrl = "http://twitter.com/" + tweet.from_user 
+ "/status/" + tweet.id;
        });
        makeResultsTable(tweets);
    }
</script>
<!--  CSS enlevée pour raccourcir le code -->
</head>
<body>
    <div id="main">
        <label for="kwBox">Search Twitter:</label>
        <input type="text" id="kwBox"/>
        <input type="button" value="Go!" onclick="searchTwitter()"/>
    </div>
    <div id="results">
    </div>
</body>
</html>

Dans cette application, on utilise le support de JSONP par l'API de recherche Twitter. Quand l'utilisateur veut procéder à une recherche, un appel à l'API est fait par l'ajout dynamique d'un tag script à la page et en spécifiant le nom d'un callback. Ceci permet de faire un appel à un autre domaine dans une page web. Au retour de l'appel, le callback (showResults) est invoqué. On ajoute un lien vers chaque tweet retourné puis on crée une table pour afficher les tweets. Pour accélérer cela, on peut mettre en cache les résultats d'une recherche et les réutiliser quand l'utilisateur soumet une recherche. Pour commencer, regardons comment utiliser localStorage pour stocker localement les tweets.

Sauvegarde locale

La recherche Twitter de base fournit un tableau de tweets retourné par l'API de recherche. Si l'on sauve ces tweets sur le poste local avec le mot-clé qui les a générés, alors on aura un cache utilisable. Pour sauver les tweets, il suffit de modifier le callback qui est invoquée au retour de l'appel à l'API de recherche Twitter. Le Listing 2 montre les fonctions modifiées.

Listing 1. Recherche et sauvegarde

function searchTwitter(){
    var keyword = $("kwBox").value;
    var query = "http://search.twitter.com/search.json?callback
=processResults&q=";
    query += keyword;
    var script = document.createElement("script");
    script.src = query;
    document.getElementsByTagName("head")[0].appendChild(script);
}
function processResults(response){
    var keyword = $("kwBox").value;
    var tweets = response.results;
    tweets.forEach(function(tweet){
        saveTweet(keyword, tweet);
        tweet.linkUrl = "http://twitter.com/" + tweet.from_user + "/status/" + tweet.id;
    });
    makeResultsTable();
    addTweetsToResultsTable(tweets);
}
function saveTweet(keyword, tweet){
    // teste si le navigateur supporte localStorage
    if (!window.localStorage){
        return;
    }
    if (!localStorage.getItem("tweet" + tweet.id)){
        localStorage.setItem("tweet" + tweet.id, JSON.stringify(tweet));
    }
    var index = localStorage.getItem("index::" + keyword);
    if (index){
        index = JSON.parse(index);
    } else {
        index = [];
    }
    if (!index.contains(tweet.id)){
        index.push(tweet.id);
        localStorage.setItem("index::"+keyword, JSON.stringify(index));
    } 
}

Commençons par la première fonction, searchTwitter. Elle est appelée quand l'utilisateur lance une recherche. La seule chose qui a changé par rapport au Listing 1 est le callback. Au lieu de simplement afficher les tweets retournés, il faut leur appliquer un traitement (les enregistrer et les afficher). On spécifie donc un nouveau callback, processResults. On prend chaque tweet et on appelle saveTweet. On passe également le mot-clé qui a servi à générer le résultats de la recherche, parce que l'on veut associer ces tweets à ce mot-clé.

Dans la fonction saveTweet, on commence par s'assurer que le navigateur supporte le stockage local. Comme nous l'avons vu précédemment, c'est une fonctionnalité couramment supportée à la fois par les navigateurs classiques et mobiles, mais c'est toujours une bonne idée de faire un test avant d'utiliser une de ces nouvelles fonctionnalités. S'il n'y a pas de support, on quitte simplement la fonction. Evidemment rien ne sera enregistré, mais il n'y aura pas d'erreur, simplement l'application ne disposera pas d'un cache dans ce cas. Si localStorage est supporté, on regarde d'abord si le tweet a déjà été stocké. Si ce n'est pas le cas, on le stocke localement en utilisant setItem. Ensuite, on récupère l'entrée d'index qui correspond au mot-clé. Il s'agit simplement d'un tableau des IDs des tweets associés au mot-clé. Si l'ID du tweet n'est pas déjà dans l'index, on l'ajoute et on met à jour l'index.

Notez que quand on enregistre et qu'on charge du JSON dans le Listing 3, on utilise JSON.stringify et JSON.parse. L'objet JSON (ou plus exactement window.JSON) est défini dans la spécification HTML5 comme un objet natif qui est toujours présent. Sa méthode stringify transforme un objet Javascript en une chaine sérialisée, tandis que sa méthode parse fait l'inverse: elle crée un objet Javascript à partir de sa représentation sous forme de chaine sérialisée. Ces manipulations sont nécessaires car localStorage ne stocke que des chaines de caractères. Cependant, l'objet JSON natif n'est pas aussi largement implémenté que localStorage. Par exemple, il n'est pas présent dans les derniers navigateurs Safari Mobile sur iPhone (version 3.1.3 quand l'article a été écrit), mais est supporté par les derniers navigateurs Android. Il est facile de vérifier sa présence et le cas échéant charger un fichier Javascript supplémentaire. On peut obtenir le même objet JSON que celui qui est utilisé nativement en allant sur le site json.org (voir les Ressources). Pour voir à quoi ressemblent les chaines sérialisées stockées localement, on peut utiliser un des outils qui permettent d'inspecter ce qui est stocké localement pour un site donné. La Figure 1 montre quelques tweets mis en cache localement et affichés par les outils Chrome pour développeurs.

Figure 1. Tweets mis en cache local

Tweets mis en cache local

Chrome et Safari disposent tous les deux d'outils intégrés qui permettent de voir les données enregistrées dans localStorage. Ce peut être très utile pour débugger les applications qui l'utilisent. Ces outils affichent les paires clé/valeur stockées localement sous forme de texte. Maintenant que nous avons commencé à enregistrer les tweets provenant de l'API de recherche de Twitter pour nous servir de cache, nous devons commencer à les lire depuis localStorage. Regardons dans la suite comment faire.

Chargement rapide des données

Dans le Listing 2, nous avons vu quelques exemples de lecture depuis localStorage en utilisant sa méthode getItem. Désormais quand un utilisateur lance une recherche, on peut tester si on a une correspondance dans le cache et immédiatement retourner les résultats mis en cache si c'est le cas. Bien sûr, il faudra toujours faire requêter l'API de recherche Twitter, puisque les gens tweetent en permanence et ajoutent des résultats aux recherches. Cependant, nous disposons maintenant d'un moyen de requêter plus efficacement en demandant uniquement les résultats que nous n'avons pas en cache. Le Listing 3 montre le code de recherche mis à jour.

Listing 3. Recherche  d'abord en local

function searchTwitter(){
    if ($("resultsTable")){
        $("resultsTable").innerHTML = ""; // clear results
    }
    makeResultsTable();
    var keyword = $("kwBox").value;
    var maxId = loadLocal(keyword);
    var query = "http://search.twitter.com/search.json?callback=processResults&q=";
    query += keyword;
    if (maxId){
        query += "&since_id=" + maxId;
    }
    var script = document.createElement("script");
    script.src = query;
    document.getElementsByTagName("head")[0].appendChild(script);
}
function loadLocal(keyword){
    if (!window.localStorage){
        return;
    }
    var index = localStorage.getItem("index::" + keyword);
    var tweets = [];
    var i = 0;
    var tweet = {};
    if (index){
        index = JSON.parse(index);
        for (i=0;i<index.length;i++){
            tweet = localStorage.getItem("tweet"+index[i]);
            if (tweet){
                tweet = JSON.parse(tweet);
                tweets.push(tweet);
            }
        }
    }
    if (tweets.length < 1){
        return 0;
    }
    tweets.sort(function(a,b){
        return a.id > b.id;
    });
    addTweetsToResultsTable(tweets);
    return tweets[0].id;
}

La première chose que l'on remarque, c'est que quand une recherche est lancée, on appelle d'abord la nouvelle fonction loadLocal. Cette fonction retourne un entier qui est l'ID du tweet le plus récent trouvé dans le cache. La fonction loadLocal prend en entrée un mot-clé qui est utilisé pour trouver les tweets correspondants dans le cache localStorage. Si l'on a un maxId, on l'utilise pour modifier la requête Twitter en ajoutant le paramètre since_id. Cette utilisation de l'API Twitter permet de ne retourner que les tweets qui sont plus récents que celui dont l'ID est passé en paramètre. Cela peut potentiellement réduire le nombre de résultats retournés par Twitter. A chaque fois qu'il est possible optimiser les appels serveur dans une application web mobile, cela peut vraiment améliorer l'expérience utilisateur sur des réseaux un peu lents. Regardons plus en détail ce que fait loadLocal.

Dans la fonction loadLocal, on utilise les données stockées au Listing 2. On récupère d'abord l'index associé au mot-clé en utilisant la méthode getItem. S'il n'existe pas d'index pour ce mot-clé, c'est qu'il n'y a pas de tweets en cache non plus, il n'y a donc aucune optimisation possible sur la requête (et on retourne 0 pour l'indiquer). Si un index est trouvé, on s'en sert pour récupérer une liste d'IDs. Tous ces tweets sont stockés en local, il n'y a donc qu'à utiliser la méthode getItem une fois de plus pour les charger depuis le cache. Les tweets ainsi chargés sont ensuite triés. On utilise la fonction addTweetsToResultsTable pour afficher les tweets et on retourne l'ID du tweet  le plus récent. Dans cet exemple, le code qui récupère les nouveaux tweets appelle directement les fonctions de mise à jour de l'IHM. Ceci vous hérisse peut-être car cette façon de faire couple le code permettant de stocker et de rechercher les tweets avec le code permettant de les afficher, et tout cela se retrouve dans la fonction processResults. L'utilisation d'événements liés au stockage fournit une alternative aboutissant à un couplage moindre.

Evénements liés au stockage

Nous allons maintenant ajouter à l'application la possibilité d'afficher les 10 termes de recherche ayant le plus de résultats en cache. Ce pourrait être une indication des recherches les plus fréquemment soumises par l'utilisateur. Le Listing 4 montre la fonction qui calcule ce top 10 et l'affiche.

Listing 4. Calcul des 10 recherches les plus fréquentes

function displayStats(){
    if (!window.localStorage){ return; }
    var i = 0;
    var key = "";
    var index = [];
    var cachedSearches = [];
    for (i=0;i<localStorage.length;i++){
        key = localStorage.key(i);
        if (key.indexOf("index::") == 0){
            index = JSON.parse(localStorage.getItem(key));
            cachedSearches.push (
                {keyword: key.slice(7), numResults: index.length});
        }
    }
    cachedSearches.sort(function(a,b){
        if (a.numResults == b.numResults){
            if (a.keyword.toLowerCase() < b.keyword.toLowerCase()){
                return -1;
            } else if (a.keyword.toLowerCase() > b.keyword.toLowerCase()){
                return 1;
            }
            return 0;
        }
        return b.numResults - a.numResults;
    }).slice(0,10).forEach(function(search){
        var li = document.createElement("li");
        var txt = document.createTextNode(
            search.keyword + " : " + search.numResults);
        li.appendChild(txt);
        $("stats").appendChild(li);
    });
}

Cette fonction utilise intensivement l'API de localStorage. On commence par retrouver le nombre total de clés stockées dans localStorage et on itère dessus. Si la clé correspond à un index, alors on analyse la valeur associée et on crée un objet qui représente la donnée qui va être traitée: le mot-clé et le nombre de tweets associés à l'index. Cette donnée est stockée dans un tableau nommé cachedSearches. Après cela on trie cachedSearches, d'abord sur le nombre de résultats puis, en cas d'égalité, en faisant un tri alphabétique sans souci de la casse. Enfin on prend les 10 premières recherches, on crée le code HTML pour chaque recherche et on l'ajoute à une liste ordonnée. Appelons cette fonction quand la page est chargée pour la première fois, comme dans le Listing 5.

Listing 5. Initialisation de la page

window.onload = function() {
    displayStats();
    document.body.setAttribute("onstorage", "handleOnStorage();");
}

La première ligne appelle la fonction du Listing 4 lors du chargement de la page. Dans la seconde, les choses deviennent plus intéressantes. On y crée un gestionnaire pour l'événement onstorage. Cet événement est déclenché à chaque fois qu'un appel à la fonction localStorage.setItem se termine. Ceci permet de recalculer le top 10 des recherches. Le Listing 6 montre ce gestionnaire d'événements.

Listing 6. Gestionnaire d'événement de stockage

function handleOnStorage() {
    if (window.event && window.event.key.indexOf("index::") == 0){
        $("stats").innerHTML = "";
        displayStats();
    }
}

L'événement onstorage est associé à la fenêtre. Il a plusieurs propriétés utiles: key, oldValue, et newValue. En sus de ces propriétés qui s'expliquent d'elles-mêmes, il y a également une propriété url (l'URL de la page qui a changé la valeur) et une propriété source (la fenêtre qui contient le script ayant chnagé la valeur). Ces deux dernières propriétés se révèlent utiles dans le cas où l'utilisateur a plusieurs fenêtres, onglets ou même des iFrames correspondant à l'application, ce qui n'est pas très courant pour des applications mobiles. Si nous revenons au Listing 6, la seule propriété dont on a réellement besoin est la propriété key. On l'utilise pour voir si c'est un index qui a été modifié. Si c'est le cas, on réinitialise le top 10 et on le redessine en appelant à nouveau la fonction displayStats. L'avantage de cette technique est qu'aucune autre fonction n' a connaitre l'existence de ce top 10, puisque tout est fait dans displayStats.

J'ai dit plus tôt que l'API Storage du DOM en général, qui comprend à la fois localStorage et sessionStorage, est une fonctionnalité de HTML 5 qui a été largement adoptée. Cependant, les événements liés au stockage font exception — en tous cas concernant les navigateurs classiques. Au moment où cet article a été écrit, les seuls navigateurs classiques supportant les événements liés au stockage sont Safari 4+ et Internet Explorer 8+. Ils ne sont pas supportés par Firefox, Chrome, ou Opera. Néanmoins dans l'univers mobile les choses vont un peu mieux. Les dernières versions des navigateurs sur iPhone et Android supportent complètement ces événements, et le code présenté ici y tourne parfaitement.

En résumé

En tant que développeur, on se sent libéré quand on dispose soudainement d'un énorme espace de stockage côté client. Pour les développeurs web de longue date, cela permet de faire des choses que l'on souhaitait faire depuis un moment, mais pour lesquelles il n'existait pas de bonne solution, n'exigeant pas d'astuces douteuses. Pour les développeurs mobiles, c'est encore plus excitant dans la mesure ou cela permet de mettre en cache local des données. En plus d'améliorer de manière frappante les performances de l'application, le cache local est la clé permettant d'accéder à une autre nouvelle possibilité excitante des applications web mobiles: le mode hors-ligne. Ce sera le sujet du prochain article de cette série.

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.

Développez de nouvelles fonctionnalités visuelles en HTML5

Article d'origine: 

Ajoutez Canvas, CSS3 et plus d'éléments sémantiques à vos applications mobiles

Résumé: 

HTML5 amène quantité de nouvelles fonctionnalités pour les applications Web mobiles, y compris des fonctionnalités visuelles qui ont comme toujours l'impact le plus important. Canvas est la plus captivante de ces nouvelles capacités de l'IHM, car elle donne accès à du pur graphisme 2-D dans le navigateur. Avec cet article vous apprendrez à utiliser Canvas ainsi que d'autres nouveaux éléments visuels de HTML5, plus discrets mais qui font une grande différence pour les utilisateurs mobiles.

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 de 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.

Dessiner avec Canvas

Pendant des années, les développeurs Web se sont plaints de Canvas. Mais pourquoi se plaindrait-on d'une interface permettant de dessiner nativement dans le navigateur ? Après tout, elle permet de créer le genre d'IHM qui requiert sinon un plugin quelconque (et comme les développeurs pour mobiles le savent, les plugins sont souvent indisponibles sur les navigateurs mobiles les plus populaires). La raison pour laquelle les développeurs Web se plaignaient de Canvas tenait au fait que, bien qu'elle ait été disponible sur Firefox et Safari depuis des années maintenant, elle n'a jamais été supportée par le navigateur le plus répandu, Microsoft®Internet Explorer®. Même les premières version d'IE9 ne supportent pas Canvas. Et donc pendant des années, Canvas a été un grand serpent de mer du point technologique. On trouve facilement des mises en oeuvres étonnantes de Canvas sur Internet, mais on ne peut pas les utiliser dans la plupart des applications Web à cause d'Internet Explorer. Heureusement pour les développeurs Web mobile, Canvas n'a pas cette limitation. Tous les navigateurs basés sur Webkit constituant la cible implémentent Canvas et optimisent beaucoup ses performances.

L'API Canvas est une API de bas niveau destiné au dessin. Elle permet de créer des lignes, des courbes, des polygones et des cercles puis de les remplir au moyen d'une couleur, d'un gradient, etc... On peut ajouter du texte et appliquer de nombreuses transformations géométriques sur n'importe quelle portion du Canvas. Comme vous pouvez l'imaginer, une telle API a d'innombrables applications. Voyez cette application qui crée un rapport en utilisant Canvas. La Figure 1 montre une capture d'écran de l'application, un histogramme de résultats annuels.

Figure 1. Rapport généré à l'aide de Canvas, dans un navigateur Android

Rapport généré à l'aide de Canvas, dans un navigateur Android

Ce que vous voyez dans la Figure 1 n'est pas une image statique rendue par le navigateur. Ce graphique est généré à la volée en utilisant l'API Canvas. Le Listing 1 montre le code HTML qui crée ce rapport.

Listing 1. HTML générant le rapport

<!DOCTYPE html>
<html>
<head>
    <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" />
    <title>HTML 5 Reports</title>
    <script type="text/javascript">
        function init(){
            var data = [{year : "2007",sales : 49},
                {year : "2008",sales : 131},
                {year : "2009",sales : 294}, 
                {year : "2010",sales : 405}];
            var report = {x : "year",
                    y : "sales",
                    values : data};
            graph(report, 350, 300);
        }
    </script>
</head>
<body onload="init()">
    <canvas id="graph"></canvas>
</body>
</html>

On voit ici la structure du HTML. Le corps du document contient un simple tag canvas. Dans la fonction init, qui est appelée lors du chargement du corps du document, on définit des données statiques (les données du rapport) et on les passe à la fonction graph. Bien que dans notre cas nous utilisions des données statiques, il est facile d'imaginer qu'elles soient chargées dynamiquement en utilisant Ajax. La fonction graph contient tout le code intéressant, jetons-y un coup d'oeil dans le Listing 2.

Listing 2. La fonction graph

function graph(report, maxWidth, maxHeight){
    var data = report.values;
    var canvas = document.getElementById("graph");
    var axisBuffer = 20;
    canvas.height = maxHeight + 100;
    canvas.width = maxWidth;
    var ctx = canvas.getContext("2d");

    var width = 50;
    var buffer = 20;
    var i = 0;
    var x = buffer + axisBuffer;
    ctx.font = "bold 12px sans-serif";
    ctx.textAlign = "start";
    for (i=0;i<data.length;i++){
        ctx.fillStyle = "rgba(0, 0, 200, 0.9)";
        ctx.fillRect(x, maxHeight - (data[i][report.y] / 2), 
                 width, (data[i][report.y] / 2));
        ctx.fillStyle = "rgba(0, 0, 0, 0.9)";
        ctx.fillText(data[i][report.x], x + (width / 4), maxHeight + 15);
        x += width + buffer;
    }

    // dessiner l'axe horizontal
    ctx.moveTo(axisBuffer, maxHeight);
    ctx.lineTo(axisBuffer+maxWidth, maxHeight);
    ctx.strokeStyle = "black";
    ctx.stroke();

    // dessiner l'axe vertical
    ctx.moveTo(axisBuffer,0);
    ctx.lineTo(axisBuffer,maxHeight);
    ctx.stroke();

    // dessiner la grille
    var lineSpacing = 50;
    var numLines = maxHeight/lineSpacing;
    var y = lineSpacing;
    ctx.font = "10px sans-serif";
    ctx.textBaseline = "middle";
    for (i=0;i<numLines;i++){
        ctx.strokeStyle = "rgba(0,0,0,0.25)";
        ctx.moveTo(axisBuffer, y);
        ctx.lineTo(axisBuffer + maxWidth,y);
        ctx.stroke();
        ctx.fillStyle = "rgba(0,0,0, 0.75)";
        ctx.fillText(""+(2*(maxHeight -y)), 0, y);
        y += lineSpacing; 
    }
}

Dans la première partie de la fonction, on met en place les objets nécessaires à la création du rapport, comme la largeur et la hauteur du canevas, et des variables d'espacement. On crée aussi l'objet correspondant au contexte du canevas, et ce sera l'objet utilisé pour faire les dessins. Ensuite on dessine les barres que l'on voit dans la Figure 1, en itérant sur les données du rapport. Tout d'abord, on positionne la propriété fillStyle. Ce peut être aussi simple que nommer une couleur, comme on pourrait le faire en CSS. Dans notre cas, on utilisera la notation rgba pour positionner non seulement la couleur mais aussi la valeur alpha. (il s'agit du degré de transparence de la couleur, comme nous le verrons quand je parlerai des fonctionnalités CSS 3.0 de HTML5). Après avoir donné une valeur à la propriété fillStyle, on crée une barre pour la donnée au moyen de l'API fillRect. Ici, on spécifie les coordonnées du point de départ du rectangle, sa largeur et sa hauteur. Après cela, on rédéfinit fillStyle parce que l'on souhaite écrire du texte dans le rapport. On utilise l'API fillText pour dessiner du texte sur le canevas. Cette API prend en entrée les coordonnées du point de départ et un texte. On fait cela pour chacune des données à représenter, créant ainsi un graphique avec des barres et des légendes.

Ensuite, on doit dessiner les autres parties du graphique, les axes et la grille. Tout d'abord, on dessine les axes horizontaux et verticaux. Pour chacun, on utilise l'API moveTo pour positionner le point à partir duquel on commencera à dessiner une ligne. Puis on utilise l'API lineTo pour dessiner une ligne depuis le point de départ jusqu'au point passé en paramètre lors de l'appel. Notez que cela ne dessine pas vraiment une ligne, il faut encore faire appel à l'API stroke pour que le dessin soit effectif. Après avoir dessiné les deux axes, on passe aux lignes de la grille avec leurs légendes en les espaçant uniformément et en les dessinant en utilisant la même combinaison de moveTo, lineTo, et stroke.

C'est tout le code dont vous avez besoin pour créer le graphique représentant les données du rapport. Vous avez vu une bonne partie des APIs les plus importantes et les plus couramment utilisées de l'API canvas dans cet exemple, mais il en existe d'autres (pour dessiner des courbes par exemple). On peut réaliser des choses vraiment surprenantes avec ces APIs, et cela est possible sur tous les navigateurs basés sur Webkit. Si les graphiques ne sont pas votre tasse de thé, HTML5 a encore d'autres joyeusetés visuelles à vous proposer avec CSS 3.0.

Le monde merveilleux de CSS3

Quand on parle de HTML5, on pense tout de suite aux balises HTML. Bien entendu, HTML5 définit effectivement de nouvelles balises, et nous en verrons quelques unes dans la prochaine partie. Dans la partie précédente, nous avons vu comment utiliser la balise <canvas> pour créer un objet canevas dans le DOM. Néanmoins, le plus gros du code était du Javascript. HTML n'est qu'une portion de HTML5—Javascript et CSS en sont des parties tout aussi importantes. Dans HTML5, beaucoup de nouveaux éléments d'IHM sont fournis par la dernière version du standard CSS, CSS3.0. Dans la Figure 2,on voit une page Web utilisant plusieurs nouveautés de CSS 3.0 s'afficher sur un téléphone Android et sur un iPhone.

Figure 2. Nouvelles fonctionnalités CSS des appareils mobiles

Nouvelles fonctionnalités CSS des appareils mobiles

L'image de gauche est tirée d'un appareil tournant sous Android. La raison pour laquelle elle est plus grosse est qu'il s'agit d'un Motorola Droid, qui a une résolution d'écran supérieure à celle de l'iPhone 3GS utilisé pour l'image de droite. On peut donc visualiser une plus grande portion de la page sur le Droid. Vous remarquez peut-être que le texte "The Gettysburg Address" a un reflet qui disparait progressivement sur l'iPhone mais qui ne disparait pas et empiète sur le texte suivant sur le Droid. Cela nous rappelle gentiment que même si les appareils basés sur Android et les iPhones ont des navigateurs basés sur Webkit, il existe tout de même de subtiles différences et qu'il ne faut pas lésiner sur les tests. Voyons maintenant le code (Listing 3)  qui a généré cette magnifique page, en commençant par le haut de la page.

Listing 3. Code pour la première moitié de la page

<!DOCTYPE html>
<html>
<head>
    <script type="text/javascript">
        function $(id){
            return document.getElementById(id);
        }
        function init(){
            var i=0;
            var row = {};
            var cell = {};
            var topics = ["nth-child", "gradients", "alpha", "text effects", 
                          "reflections", "transformations"];
            for (i=0;i<topics.length;i++){
                row = document.createElement("tr");
                cell = document.createElement("td");
                cell.appendChild(document.createTextNode(topics[i]));
                row.appendChild(cell);
                $("dtable").appendChild(row);
            }
        }
    </script>
    <style type="text/css">
        header > h1{
            color: yellow;
            background: -webkit-gradient(linear, left top, left bottom, 
                             from(blue), to(white))
        }
        tr:nth-child(4n+1) { color: navy; }
        tr:nth-child(4n+2) { color: green; }
        tr:nth-child(4n+3) { color: maroon; }
        tr:nth-child(4n+4) { color: purple; }

        input[type="text"]{
            background: rgba(150, 30, 30, 0.5);
        }
    </style>
</head>
<body onload="init()">
    <header>
        <h1>The World of CSS3</h1>
        <div>What kind of CSS3 does your browser support?</div>
    </header>
    <table id="dtable"></table>
    <div id="formSection">
        <label for="name">What's your name?</label>
        <input type="text" id="name"></input>
        <button id="rtBtn" onclick="rotate()">Rotate</button>
    </div>
</body>
</html>

Le code du Listing 3 dessine toute la partie de la page se trouvant au dessus de "Gettysburg Address". Vous verrez le code de la partie basse bientôt.

La première chose à examiner est l'entête de la page. Si vous regardez le corps de la page HTML, près de la fin du listing, vous verrez que cet entête se trouve littéralement dans un tag header — un des nouveaux éléments de HTML 5.

Intéressons-nous maintenant à l'élément style (au-dessus du corps du HTML dans le Listing 3). Le texte est stylé par CSS avec le sélecteur header > h1. Cette règle rend le texte jaune, mais elle lui donne également un arrière-plan bleu et blanc. Cet arrière-plan se voit appliquer un gradient. C'est le premier exemple que nous verrons de fonctionnalités CSS préfixées par -webkit. Comme vous vous en doutez, ceci lie la CSS aux navigateurs basés sur Webkit. Néanmoins, dans la plupart des cas, elles font partie du standard CSS 3.0, mais appartiennent à des zones où le standard n'est pas encore complètement figé. Le plus souvent, les navigateurs Webkit aussi bien que Mozilla Firefox ont implémenté ces fonctionnalités. Si vous devez également cibler les navigateurs Mozilla (tels que la version mobile de Firefox, nommée Fennec, qui gagne rapidement en popularité sur les smatphones Nokia d'Europe), vous pouvez d'habitude changer le préfixe -webkit en -moz.

Ensuite on affiche une liste de sujets dans la table nommée dtable. Comme vous le constatez dans la Figure 2, la couleur change d'une ligne à l'autre lors de l'affichage du contenu de la table. Cela se fait au moyen de la section CSS suivante, les déclarations tr:nth-child. On peut utiliser la règle nth-child sur tout élément qui se répète. On lui passe une formule utilisée comme un prédicat permettant de savoir si la règle est valide pour l'élément. Dans notre cas, on indique que les lignes dont le numéro est de la forme 4n+1 (1, 5, 9, etc...) seront en bleu marine, de la forme 4n+2 (2, 6, 10, etc...) en vert, et de même pour le marron et le violet. Il y a de bonnes chances que vous ayez eu à implémenter des traitements visuels similaires à des tables, des listes ou autres dans le passé, mais en vous fatigant à écrire du Javascript.

Les derniers éléments visuels de cette première partie sont le champ texte à fond rouge avec le label What's your name? et un bouton disant Rotate. Le coloriage en rouge est obtenu en utilisant un sélecteur spécifiant un certain type de zone de formulaire. En d'autres termes, cette règle CSS ne s'appliquera qu'aux éléments de saisie de type text. Et maintenant, vous vous demandez peut-être ce que fait le bouton Rotate. Vous voyez dans le Listing 4 qu'il appelle une fonction nommée rotate.

Listing 4. Fonction Javascript de rotation utilisant CSS

function rotate(){
    $("formSection").style["-webkit-transform"] = "rotateZ(-5deg)";
    $("formSection").style["-webkit-transition"] = 
          "-webkit-transform 2s ease-in-out";
    $("rtBtn").innerHTML = "Undo";
    $("rtBtn").onclick = function() {
        $("formSection").style["-webkit-transform"] = "";
        $("rtBtn").innerHTML = "Rotate";
        $("rtBtn").setAttribute("onclick", "rotate()");
    }
}

La fonction rotation utilise Javascript pour modifier le style appliqué au div nommé formSection. (Remarque: on utilise $() comme un alias de document.getElementById()). Pour faire tourner le div de cinq degrés dans le sens inverse des aiguilles d'une montre, on positionne son style -webkit-transform à rotateZ(-5deg). Puis on donne au style -webkit-transition la valeur -webkit-transform 2s ease-in-out. La rotation dure ainsi deux secondes, démarre lentement, s'accélère puis ralentit sur la fin. Dans la Figure 3, la partie gauche montre la position initiale du champ "What's your name?". La partie droite montre l'effet visuel produit par la rotation du champ et du bouton "Undo".

Figure 3. Rotation d'éléments HTML

Rotation d'éléments HTML

Suivez le lien figurant dans les Ressources pour voir cet effet en action dans un navigateur supportant HTML5 comme Chrome, Safari 4 ou Opera.

Passons maintenant à la partie inférieure de la page de la Figure 2. Nous y voyons quelques exemples intéressants d'effets sur des images et du texte, ainsi que différentes mises en page. Le code se trouve dans le Listing 5.

Listing 5. Code pour la partie inférieure de la page de la Figure 2

<!DOCTYPE html>
<html>
<head>
    <style type="text/css">
        h2 {
            -webkit-text-fill-color: blue;
            -webkit-text-stroke-color: yellow;
            -webkit-text-stroke-width: 1.5px;
            background: -webkit-gradient(radial, 430 50, 0, 430 50, 200, from(red), 
to(#000));
            -webkit-box-reflect:below 5px -webkit-gradient(linear, left top, left 
bottom, from(transparent), color-stop(0.5, transparent), to(white));
        }
        h3{
            color: rgba(0,0,255,0.75);
            background: rgba(255,255,0,0.5);
        }
        .xyz{
            text-shadow: #6374AB 4px -4px 2px;
            white-space: nowrap;
            width: 14em; 
            height: 2em; 
            overflow: hidden;
            text-overflow: ellipsis; 
            border: 1px solid #bbb; 
            border-radius: 9px;             
            background-color: #fff;
        }
        .abc {
            border: 1px solid #000;
            border-radius: 9px;        
            -webkit-column-count:4;
            -webkit-column-rule: 1px solid #a00;
            -webkit-column-gap: 0.75em;
        }
    </style>
</head>
<body onload="init()">
    <h2>The Gettysburg Address</h2>
    <h3>Abraham Lincoln, Gettysburg, PA. November 19, 1863</h3>
    <div class="xyz">
        Four score and seven years ago our fathers brought forth on this 
        continent a new nation, conceived in liberty, and dedicated to 
            the proposition that all men are created equal.
    </div>
    <div class="abc">
        Now we are engaged in a great civil war, testing whether that 
            nation, or any nation, so conceived and so dedicated, can long 
            endure. We are met on a great battle-field of that war. We have 
            come to dedicate a portion of that field, as a final resting 
            place for those who here gave their lives that that nation might 
            live. It is altogether fitting and proper that we should do this.
    </div>
</body>
</html>

Voyons élément par élément ce que fait ce code. D'abord, on crée un entête "The Gettysburg Address" et on lui ajoute plusieurs styles.

  1. En utilisant les styles -webkit-text-fill-color, -webkit-text-stroke-color, et -webkit-text-stroke-width, on crée un effet "bleu à l'intérieur du jaune".
  2. Un arrière-plan noir et rouge est mis en place au moyen du style -webkit-gradient. Remarquez qu'il s'agit ici d'un dégradé radial, alors que précédemment nous avons vu un dégradé linéaire. Les deux marchent très bien sur les smartphones.
  3. On applique un effet de reflet à l'entête par le style -webkit-box-reflect.  Ce style est paramétré pour refléter l'entête cinq pixels en dessous de sa position, avec en prime un gradient. Cette fois, le dégradé donne l'impression que le reflet s'évanouit progressivement. Si l'on revient à la Figure 2, on voit que cette combinaison n'est pas rendue sur le navigateur Android: il affiche simplement le reflet, sans aucun dégradé.

Passons maintenant à l'entête suivant, sur lequel on applique un style très simple. On utilise simplement une couleur pour le texte, et une autre pour l'arrière-plan. Ces deux couleurs sont spécifiée en utilisant la fonction pour donner les valeurs des composantes rouge, bleue et verte, ainsi qu'une valeur de transparence. La valeur 1.0 donnera un résultat complètement opaque et la valeur 0.0 un résultat transparent.

On voit ensuite dans le Listing 5 le code du premier paragraphe, dont le texte possède une bordure aux coins arrondis grâce au nouveau style border-radius. On voit ce type de coins un peu partout sur le Web maintenant, et on les obtient généralement en utilisant des images. Cela semble carrément primitif en comparaison de la facilité avec laquelle on y arrive avec CSS 3.0. On applique un ombrage au texte de ce paragraphe au moyen du style text-shadow. Enfin, notez que la zone réservée à l'affichage du paragraphe est limitée par la hauteur et la largeur données au div parent, et que le texte est trop long pour tenir. Au lieu de bêtement couper le texte comme dans les navigateurs plus anciens, on obtient une sympathique ellipse (...) en utilisant le style text-overflow.

On arrive enfin à la dernière zone de texte. Elle possède aussi une bordure, mais notez qu'elle apparait sur quatre colonnes, avec des séparateurs de colonnes. Pour ce faire, positionnons le style -webkit-column-count, ainsi que le style apparenté -webkit-column-rule pour créer les séparateurs. Imaginez seulement à quel point il est pénible d'obtenir un texte formatté de cette manière sans les nouveautés de CSS 3.0. Ce peut être aussi une fonctionnalité intéressante quand on crée des entêtes et des pieds de page simples, qui sont tous les deux de nouveaux éléments de HTML 5. Jetons-y un coup d'oeil ainsi qu'à d'autres balises introduites par HTML 5.

Nouvelles sémantiques

HTML 5 ajoute beaucoups de nouveaux éléments à la soupe des balises HTML. Certains de ces éléments demanderont aux navigateurs de fournir de nouveaux rendus. D'autres ajouteront des fonctionnalités qui pourront ensuite être manipulées via Javascript. Cependant, certaines ne font rien de tout cela. Elles auront le même aspect et seront programmées de la même manière que <span>, <div>, et <p>. Mais elles donneront de nouvelles sémantiques, qui sont importantes pour les utilisateurs non-visuels de la page, ce qui inclut les personnes utilisant des technologies d'accessibilité comme des lecteurs d'écran ainsi que les programmes tels que les moteurs de recherche. Ces nouvelles balises fournissent aussi des moyens d'écrire des sélecteurs CSS plus expressifs. La Figure 4 montre une page Web utilisant quelques-uns de ces nouveaux éléments sémantiques.

Figure 4. Nouveaux éléments HTML 5 sur iPhone

Nouveaux éléments HTML 5 sur iPhone

L'exemple de la Figure 4 contient un élément header, ainsi que de nombreux éléments nav, un article, une section, et un aside. Ces éléments ne provoquent aucun rendu particulier. Ils ajoutent simplement de la sémantique, et vous pouvez vous en servir pour écrire une CSS qui leur donne un rendu correspondant à leur valeur sémantique. Le code de cette page se trouve dans le Listing 6.

Listing 6. Nouveaux éléments sémantiques de HTML 5

<!DOCTYPE html>
<html>
<head>
<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" />
<title>Get the latest markup</title>
</head>
<body>
    <header style="border: 1px dotted #000;border-radius: 3px;">
        <hgroup align="center">
            <h1>Real documents have headers</h1>
            <h2>Even if they don't say so</h2>
        </hgroup>
        <hgroup>
        <nav style="-webkit-column-count:3;-webkit-column-rule: 1px solid #a00;">
            <a href="new-css.html">CSS3</a><br/>
            <a href="report.html">Canvas</a><br/>
            <a href="elements.html">Markup</a>
        </nav>
        </hgroup>
    </header>
    <article>
       <h1>There are a lot of new markup elements in HTML5</h1>
       <time datetime="2010-05-16" pubdate>Sunday, May 16</time>
       <section>Did you notice that we just had two H1's? 
       But it's cool!</section>
       <aside style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" >
            If this page was really popular, I'd put an ad here and make some
            serious cash
        </aside>
    </article>
</body>
</html>

Vous pouvez voir un certain nombre des nouveaux éléments mentionnés plus haut. Remarquez qu'il est également possible d'appliquer des styles pour créer une boite aux coins arrondis autour du header et pour créer des séparateurs pour le nav. On a aussi stylé la manière de gérer le débordement de texte dans le bloc aside. Ce qui est important ici, c'est que l'on peut donner beaucoup plus de sens au balisage sans travail supplémentaire, et ensuite le faire apparaitre tout comme si on avait utilisé des <div> et des <span>. Pour voir un exemple d'éléments HTML 5 qui ont un impact plus important sur les plans visuels et de la programmation, regardez la Figure 5. (Voir une version purement textuelle de la Figure 5).

Figure 5. Formulaire HTML 5 sur iPhone

Formulaire HTML 5 sur iPhone

L'écran de la Figure 5 utilise de nombreux éléments nouveaux disponibles avec HTML 5. Dans la plupart des cas, ils ressemblent aux éléments existants, mais vous pouvez vous attendre à ce que les navigateurs donnent une meilleure représentation visuelle de ces éléments de formulaire enrichis. Pour en avoir un aperçu, regardez ce que donne le formulaire précédent dans le navigateur Opera "classique" dans la Figure 6. (Voir une version purement textuelle de la Figure 6).

Figure 6. Formulaire HTML 5 dans Opera

Formulaire HTML 5 dans Opera

 Opera implémente tooujours rapidement les fonctionnalités HTML 5, et c'est tout spécialement le cas des nouveaux éléments de formulaires.Regardez maintenant le code qui a généré les Figures 5 et 6 afin de mieux comprendre pourquoi Opera a rendu les choses de cette manière. Le Listing 7 montre ce code.

Listing 7. Code utilisant les éléments de formulaires HTML 5

<<form id="settings">
<fieldset id="inputs" style="border: 1px solid #000;border-radius: 6px;">
    <legend>Settings</legend>
    <label for="name">Username</label>
    <input id="name" name="name" type="text" required autofocus /><br/>
    <label for="name">Name</label>
    <input id="name" name="name" type="text" 
          placeholder="First and last name" required /><br/>
    <label for="email">Email</label>
    <input id="email" name="email" type="email"           
           placeholder="example@domain.com" required /><br/>
    <label for="phone">Phone</label>
    <input id="phone" name="phone" type="tel" 
          placeholder="Eg. +447500000000" required /><br/>
    <label for="dob">Date of birth</label>
    <input id="dob" name="dob" type="date" required/>
    <fieldset style="border: 1px dotted #000; border-radius: 6px">
        <legend>Preferred Contact Method</legend>
        <ol>
            <li>
                <input id="emailMeth" name="contactMethod" 
                            type="radio">
                <label for="emailMeth">Email</label>
            </li>
            <li>
                <input id="phoneMeth" name="contactMethod" 
                            type="radio">
                <label for="phoneMeth">Phone</label>
            </li>
        </ol>
    </fieldset>
    <label for="climate">Preferred external temperature</label>
    <input id="climate" name="climate" type="range" min="50" 
          max="100" step="5" value="70"/><br/>
    <label for="color">Favorite color</label>
    <input id="color" name="color" type="color"/><br/>
    <label for="referrer">Where'd you hear about us?</label>
    <input type="url" name="refUrl" id="referrer" list="urls"/>
    <datalist id="urls">
        <option label="TechCrunch" value="http://www.techcrunch.com/">
        <option label="ReadWrite Web" value="http://www.readwriteweb.com/">
        <option label="Engadget" value="http://www.engadget.com/">
        <option label="Ajaxian" value="http://www.ajaxian.com/">
    </datalist><br/>
    <button type="button" onclick="checkInputs()">Save</button>
</fieldset>
</form>

Les éléments utilisés dans le formulaire du Listing 7 comprennent un bon nombre des nouvelles fonctionnalités de HTML 5. Remarquez deux des nouveaux attributs, required et autofocus. L'attribut required est utilisé lors de la validation du formulaire (nous en dirons davantage plus bas) et l'attribut autofocus permet de choisir l'élément de la page qui recevra le focus. Notez aussi que plusieurs éléments disposent d'un texte placeholder. C'est un comportement que beaucoup de sites utilisent depuis des années — donner un exemple ou une explication à l'intérieur de la zone de saisie — mais le développeur devait à chaque fois écrire le code. Vous voyez le rendu sympathique de l'iPhone à la Figure 4.

Après cela on voit quelques-uns des nouveaux types de données qui sont permis pour les éléments de saisie, comme email, phone, date, range, color, et url. Au jour d'aujourd'jui, ils sont tous rendus comme des zones de texte dans les navigateurs iPhone et Android, comme si l'on utilisait du HTML 4.0 moins correct du point de vue de la sémantique. La Figure 5 montre à quoi ils pourraient ressembler à l'avenir. Le champ date doit recevoir le focus avant de montrer sa nouvelle interface (un calendrier en pop-up) sous Opera. C'est vrai aussi pour le champ url de la Figure 7, mais ce n'est pas parce que l'on doit y saisir une URL. C'est parce qu'il a un attribut list. Il pointe vers l'élément datalist qui contient les valeurs permises pour ce champ. Quand il reçoit le focus, on voit les différentes possibilités (dans ce cas, plusieurs URLs) provenant de la liste spécifiée, de manière similaire aux champs à saisie assistée faits en Ajax qui sont si populaires. Voyez ces fonctionnalités de date et de liste de données en action dans la Figure 7.

Figure 7. Zones de saisie de dates et de choix dans une liste avec HTML 5

Zones de saisie de dates et de choix dans une liste avec HTML 5

On peut s'attendre à ce que la plupart des types d'entrée bénéficient rapidement de meilleures représentations visuelles du fait que Webkit continue à évoluer rapidement. Opera donne une bonne idée de ce que sera le futur. Une bonne partie de ces types d'entrée fournissent également une validation, et HTML 5 possède un ensemble complet de nouvelles APIs de validation. Ces fonctionnalités ne sont pas encore implémentées sur les appareils iPhone ou Android, mais elles sont présentes sur leurs équivalents classiques, on peut donc espérer les voir débarquer bientôt dans les navigateurs mobiles. Jetons un coup d'oeil à l'utilisation de ces nouvelles APIs de validation dans le Listing 8.

Listing 8. Les APIs de validation HTML 5 en action

<function checkInputs(){
    var inputs = document.getElementById("inputs").childNodes;
    var len = inputs.length;
    var i = 0;
    var input = null;
    var errors = [];
    for (i=0;i<len;i++){
        input = inputs.item(i);
        if (input.nodeName == "INPUT"){
            if (input.validity){
                if (!input.checkValidity()){
                    errors.push(input.validationMessage);
                }
            }
        }
    }
    var errMsg = "There are " + errors.length + " errors";
    var notify = function(){
        var notification = 
            webkitNotifications.createNotification(null, "Errors!", errMsg);
        notification.show();
    };
    if (window.webkitNotifications){
        if (webkitNotifications.checkPermission()){
            webkitNotifications.requestPermission(notify);
        } else {
            notify();
        }
    } else {
        alert(errMsg);
    }
}

Tous les éléments de saisie ont une propriété validity. On peut l'utiliser ou bien se servir de la fonction checkValidity() qui retourne vrai ou faux, et de la propriété validationMessage pour obtenir un message d'erreur adapté à la langue. Au moment ou ceci est écrit, les navigateurs classiques les plus récents ne retournent rien de consistent ou de standard pour validationMessage, elle est donc d'un intérêt limité. L'objet validity peut être utiliser pour tester différentes catégories d'erreur, comme valueMissing, rangeOverflow, rangeUnderflow, patternMismatch, et tooLong.Par exemple, si un élément a l'attribut required mais que l'utilisateur le laisse vide, validity.valueMissing sera vrai.

Enfin, notez dans le Listing 8 qu'après avoir détecté les erreurs de validation, on essaie d'utiliser webkitNotifications. Il s'agit d'un système de notifications sur votre ordinateur, disponible dans la dernière version de Chrome. On peut donc une fois de plus anticiper une arrivée prochaine dans les navigateurs iPhone et Android. L'utilisation de l'API est assez simple. La seule subtilité est qu'il faut tester si l'utilisateur a bien donné à votre site la permission d'utiliser cette API. Si ce n'est pas le cas, il faut la demander, en passant en paramètre la fonction qui doit être invoquée si l'utilisateur donne effectivement son accord. 

Conclusion

Dans cet article, nous avons fait un rapide tour d'horizon de la plupart des nouvelles fonctionnalités de HTML 5 liées à l'IHM, depuis les nouveaux éléments jusqu'aux nouveaux styles en passant par les canevas qui permettent de dessiner. Ces fonctionnalités (avec quelques exceptions notables vers la fin) sont toutes à votre disposition sur les navigateurs à base de Webkit que l'on trouve sur iPhone et sur Android. D'autres plateformes populaires commes les smartphones Blackberry et Nokia sont en passe d'obtenir des navigateurs plus puissants qui supportent les technologies que nous avons vues dans cet article. En tant que développeur Web mobile, vous avez la possibilité de toucher une large portion d'utilisateurs avec des fonctionnalités visuelles plus riches que tout ce à quoi vous aviez jamais eu accès avec HTML, CSS et Javascript dans les navigateurs classiques. Dans les quatres premières parties de cette série, vous avez découvert bien d'autres nouvelles technologies (comme la géolocalisation et les Web Workers) qui sont également disponibles sur ces incroyables nouveaux navigateurs mobiles. Le Web mobile n'est pas une version allégée du Web pour lequel nous avons programmé pendant des années. C'est une version plus puissante et pleine de possibilités.

Utiliser les Web Workers pour accélérer vos applications Web mobiles

Article d'origine: 

Ajouter du Javascript multi-tâches à HTML 5: un cocktail gagnant !

Résumé: 

Les applications Web ont depuis longtemps été cantonnées dans un monde mono-tâche. Ceci a réellement limité les développeurs dans leur code, puisque tout ce qui était trop compliqué risquait de geler l'IHM de l'application. Les Web Workers ont changé la donne en apportant le multi-tâches aux applications Web. C'est particulièrement utile pour les applications Web mobiles pour lesquelles la plus grande partie de la logique de l'application se trouve côté client. Dans cet article, vous apprendrez à utiliser les Web Workers et découvrirez les tâches pour lesquels ils sont surtout appropriés. Vous verrez comment les utiliser avec d'autres technologies liées à HTML 5 pour en accroitre l'efficacité.

Pour commencer

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 de 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. Les exemples de l'article utilisent également un serveur proxy pour accéder aux services distants depuis le navigateur. Ce proxy est une simple servlet Java™, mais elle peut facilement être remplacée par un proxy en PHP, Ruby ou autres. Voir les Ressources pour les liens.

Javascript multi-tâches sur les appareils mobiles

La programmation multi-tâches ou concurrente n'est absolument pas une nouveauté pour les développeurs. Elle est permise, d'une manière ou d'une autre, par la plupart des langages de programmation modernes. Cependant, Javascript fait partie des exceptions. Ses créateurs pensaient que c'était un peu trop problématique et superflu pour un langage qui a été conçu pour effectuer des tâches simples sur les pages Web. Pourtant, comme les pages Web ont évolué en applications Web, le niveau de complexité des tâches exécutées par Javascript a augmenté au point de le mettre à égalité avec les autres langages. Dans le même temps, les développeurs travaillant avec d'autres langages qui fournissent un support à la programmation concurrente ont souvent souffert à cause de la grande complexité qui accompagne l'utilisation de primitives concurrentes comme les tâches et les mutex. En fait, récemment, un certain nombre de nouveaux langages comme Scala, Clojure et F# sont nés, promettant tous de simplifier la concurrence.

La spécification Web Worker ne se contente pas d'ajouter le support de la concurrence à Javascript et aux navigateurs, elle veut le faire d'une manière intelligente qui ouvrira des possibilités aux développeurs tout en leur épargnant les problèmes liés à ce domaine. Par exemple, les développeurs d'applications classiques utilisent le multi-tâches depuis des années pour accéder à des ressources en lecture/écriture sans geler l'IHM pendant qu'on attend qu'elles soient accessibles. Néanmoins ces applications ont souvent des problèmes quand ces nombreuses tâches modifient des ressources partagées (y compris l'IHM), ce qui conduit à des gels ou même des plantages. Avec les Web Workers, cela ne peut pas arriver. Les tâches supplémentaires n'ont tout simplement pas accès aux mêmes ressources que la tâche principale de l'IHM. En fait, le code de la tâche démarrée ne peut même pas se trouver dans le même fichier que le code exécuté par la tâche principale de l'IHM.

Il faut même fournir ce fichier externe dans le constructeur, comme dans le Listing 1.

Pour atteindre ce résultat trois éléments sont liés:

  1. Le Javascript de la page Web (je l'appelerai le script de page) exécuté dans la tâche principale.
  2. L'objet Worker qui est l'objet Javascript qui sera utilisé pour exécuter la fonction du Web Worker.
  3. Le script qui sera exécuté par la tâche nouvellement créée. Je l'appelerai le script du Worker.

Regardons d'abord le script de page du Listing 1.

Listing 1. Utiliser un Web Worker dans le script de la page

var worker = new Worker("worker.js");
worker.onmessage = function(message){
    // do stuff
};
worker.postMessage(someDataToDoStuffWith);

Dans le Listing 1, on voit les trois grandes étapes dans l'utilisation des Web Workers. Tout d'abord, on crée un objet Worker et on lui passe l'URL du script qui sera exécuté par la nouvelle tâche. La totalité du code qui sera exécuté par le Worker doit se trouver dans le script du Worker, dont on passe l'URL au constructeur du Worker. L'URL du script du Worker est limitée par la règle de la même origine (ndT: Same Origin Policy), il doit provenir du même domaine que le script de page qui crée le Web Worker.

Ensuite on spécifie un gestionnaire de callback par le biais de la fonction onmessage. Ce callback sera invoqué à la fin de l'exécution du script du Worker. message est de la donnée retournée par le script du Worker et qui peut contenir tout ce que l'on veut. Le callback est exécuté par la tâche principale, il a donc accès au DOM. Le script du Worker s'exécute dans une autre tâche et n'a pas accès au DOM, on a donc besoin d'envoyer des données depuis le script du Worker vers la tâche principale, où l'on peut en toute sécurité modifier le DOM pour mettre à jour l'IHM de l'application. C'est le point clé de la conception des Web Workers où rien n'est partagé.

La dernière ligne du Listing 1 montre comment le Worker est initialisé, par un appel à la fonction postMessage. On passe ici un message (encore une fois, il s'agit d'une donnée quelconque) au Worker. Bien sûr, postMessage est une fonction asynchrone: on l'appelle et le retour est immédiat.

Maintenant, examinons le script du Worker. Le code du Listing 2 est le contenu du fichier worker.js du Listing 1.

Listing 2. Un script Worker

importScripts("utils.js");
var workerState = {};
onmessage = function(message){
     workerState = message.data;
      // do stuff with the message
    postMessage({responseXml: this.responseText});
}

On voit que le script du Worker contient une fonction onmessage. Elle est invoquée quand on appelle postMessage dans la tâche principale. Les données passées par le script de la page sont transmises à la fonction postMessage dans l'objet message. On accède à ces données en récupérant la propriété data de cet objet. Quand on a fini de traiter les données dans le script du Worker, on invoque la fonction postMessage pour renvoyer des données à la tâche principale. De la même manière, ces données sont accessibles pour la tâche principale dans la propriété data de l'objet message qu'elle reçoit.

Jusqu'ici, on a vu la sémantique à la fois simple et puissante des Web Workers. Par la suite, nous verrons comment appliquer tout cela pour accélérer les applications Web mobiles. Avant cela, il est nécessaire de parler du support par les matériels. Après tout, nous parlons d'applications Web mobiles et dans ce genre de développement il est essentiel de prendre en compte les capacités propres à chaque navigateur.

Support par les différents matériels

A partir d'Android 2.0, le navigateur supporte totalement la spécification Web Worker de HTML 5. Au moment où cet article est écrit, la plupart des matériels Android sont livrés avec Android 2.1. De plus, cette fonctionnalité est pleinement supportée par le navigateur Mozilla Fennec qui est disponible sur les appareils Nokia avec l'OS Maemo et sur les appareils Windows Mobile. La seule absence notable est donc l'iPhone. Les versions 3.1.3 et 3.2 de l'iPhone OS (les versions qui tournent sur iPad) ne supportent pas encore les Web Workers. Cependant, ils sont déjà supportés par Safari, çà ne devrait donc être qu'une question de temps avant que cela n'arrive dans le navigateur Mobile Safari. Etant donné la prédominance de l'iPhone (surtout aux USA) il vaut mieux ne pas partir du principe que les Web Workers sont disponibles et les utiliser uniquement pour améliorer les applications Web mobiles lorsque leur présence est détectée. Avec cela à l'esprit, voyons comment on peut utiliser les Web Workers pour rendre les applications Web mobiles plus rapides.

Améliorer les performances avec les Workers

Le support des Web Workers par les navigateurs des smartphones est déjà bon et va en s'améliorant. Ce fait amène à se poser cette question: quand faut-il utiliser les Web Workers dans les applications Web mobiles ? La réponse est simple: à chaque fois qu'on doit faire quelque chose qui prend beaucoup de temps. Certains exemples de Workers montrent comment les utiliser pour exécuter des calculs mathématiques onéreux, comme par exemple calculer les 10000 premières décimales de Pi. Il est très peu probable que vous ayez jamais à faire ce genre de calculs dans une application Web, et encore moins dans une application Web mobile. Par contre, récupérer des données de ressources distantes est assez courant et c'est le but de l'exemple de cet article.

Dans cet exemple, nous récupérerons une liste de Daily Deals (des bonnes affaires mises à jour quotidiennement) depuis eBay. Cette liste de bonnes affaires contient une brève description de chaque affaire. Une description plus détaillée peut être obtenue en utilisant l'API eBay's Shopping. Nous utiliserons les Web Workers pour anticiper le chargement de ces informations supplémentaires pendant que l'utilisateur navigue dans la liste des bonnes affaires à la recherche de celle qui l'intéresse. Pour accéder à ces données provenant d'eBay depuis notre application Web, nous devons prendre en compte la règle de la même origine, ce qui nous conduit à utiliser un proxy générique. Une simple servlet Java jouera ce rôle. Elle est incluse dans le code de l'article, mais n'est pas détaillée ici. Nous nous concentrerons plutôt sur le code qui utilise les Web Workers. Le Listing 3 montre la page HTML de base pour l'application.

Listing 3. Code HTML de l'application

<!DOCTYPE HTML>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <meta name = "viewport" content = "width = device-width">
    <title>Worker Deals</title>
    <script type="text/javascript" src="common.js"></script>
  </head>
  <body onload="loadDeals()">
    <h1>Deals</h1>
    <ol id="deals">
    </ol>
    <h2>More Deals</h2>
    <ul id="moreDeals">
    </ul>
  </body>
</html>

Comme vous le constatez, il s'agit de HTML très simple, juste une coquille vide en fait. On récupère les données et on génère l'IHM à l'aide de Javascript. C'est là la conception optimale pour les applications Web mobiles, puisqu'elle permet à tout le code et au balisage statique d'être mis en cache dans l'appareil, et l'utilisateur n'a à attendre que les données du serveur. Notez que, dans le Listing 3, une fois que le corps a été chargé, on appelle la fonction loadDeals (Listing 4) qui effectue le chargement initial des données de l'application.

Listing 4. La fonction loadDeals

var deals = [];
var sections = [];
var dealDetails = {};
var dealsUrl = "http://deals.ebay.com/feeds/xml";
function loadDeals(){
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
        if (this.readyState == 4 && this.status == 200){
               var i = 0;
               var j = 0;
               var dealsXml = this.responseXML.firstChild;
               var childNode = {};
               for (i=0; i< dealsXml.childNodes.length;i++){
                   childNode = dealsXml.childNodes.item(i);
                   switch(childNode.localName){
                   case 'Item': 
                       deals.push(parseDeal(childNode));
                       break;
                   case "MoreDeals":
                       for (j=0;j<childNode.childNodes.length;j++){
                           var sectionXml= childNode.childNodes.item(j);
                           if (sectionXml && sectionXml.hasChildNodes()){
                               sections.push(parseSection(sectionXml));
                           }
                       }
                       break;    
                   default:
                       break;
                   }
               }
               deals.forEach(function(deal){
                   var entry = createDealUi(deal);
                   $("deals").appendChild(entry);
               });
               loadDetails(deals);
               sections.forEach(function(section){
                   var ui = createSectionUi(section);
                   $("moreDeals").appendChild(ui);
                   loadDetails(section.deals);
               });
        }
    };
    xhr.open("GET", "proxy?url=" + escape(dealsUrl));
    xhr.send(null);
}

Dans le Listing 4 on voit la fonction loadDeals et les variables globales utilisées dans l'application. On utilise un tableau de deals et un tableau de sections. Il s'agit de groupe de bonnes affaires apparentées (par exemple, Bonnes affaires à moins de 10€). Il y a aussi un tableau associatif appelée dont les clés seront des IDs d'Items (que l'on trouvera dans les données des affaires), et dont les valeurs sont les informations plus détaillées obtenues au moyen de l'API eBay Shopping.

La première chose que l'on fait est un appel Ajax vers un proxy, qui à son tour appelle l'API REST Daily Deals de eBay. Elle fournit la liste des affaires dans un document XML. On analyse ce document dans la fonction onreadystatechange de l'objet XMLHttpRequest utilisé pour faire l'appel Ajax. On utilise deux autres fonctions, parseDeal et parseSection, pour transformer les noeuds XML en objets Javascript d'utilisation plus simple. Vous trouverez le code de ces fonctions dans l'exemple de code téléchargeable (voir les Téléchargements), mais ce sont juste des fonctions d'analyse XML fastidieuses et je ne les ai donc pas incluses ici. Finalement, après avoir analysé le XML, on utilise encore deux autres fonctions, createDealUi et createSectionUi qui modifient le DOM. Quand on a fini, l'IHM  ressemble à la Figure 1.

Figure 1. IHM de l'application

IHM de l'application

Si l'on revient au Listing 4, on note qu'une fois que les affaires sont chargées, on appelle la fonction loadDetails pour chacun d'eux. Dans cette fonction on charge les détails supplémentaires pour chaque affaire, mais uniquement si le navigateur supporte les Web Workers. Le Listing 5 montre la fonction loadDetails.

Listing 5. Préchargement du détail des affaires

function loadDetails(items){
    if (!!window.Worker){
        items.forEach(function(item){
            var xmlStr = null;
            if (window.localStorage){
                xmlStr = localStorage.getItem(item.itemId);
            }
            if (xmlStr){
                var itemDetails = parseFromXml(xmlStr);
                dealDetails[itemDetails.id] = itemDetails;
            } else {
                var worker = new Worker("details.js");
                worker.onmessage = function(message){
                    var responseXmlStr =message.data.responseXml;
                    var itemDetails=parseFromXml(responseXmlStr);
                    if (window.localStorage){
                        localStorage.setItem(
                                        itemDetails.id, responseXmlStr);
                    }
                    dealDetails[itemDetails.id] = itemDetails;
                };
                    worker.postMessage(item.itemId);
            }
        });
    }
}

Dans loadDetails, on vérifie d'abord que la fonction Worker se trouve bien dans le contexte global (l'objet window). Si ce n'est pas le cas, on ne fait rien, tout simplement. Si elle est présente, alors on cherche dans localStorage le XML correspondant au détail de cette affaire. C'est une stratégie de cache local très courante dans les applications Web mobiles, qui est décrite en détail dans la seconde partie de cette série.

Si on trouve ce XML localement, on l'analyse dans la fonction parseFromXml et on ajoute les détails à l'objet dealDetails. Si on ne le trouve pas en local, on génère un Web Worker et on lui passe l'ID d'item de l'affaire au moyen de postMessage. Une fois que le Worker a récupéré les données et rappelle la tâche principale, on analyse le XML, on ajoute le résultat à dealDetails et on stocke le XML dans localStorage. Le listing 6 montre le script du Worker, details.js.

Listing 6. Script du Worker de récupération du détail des affaires

importScripts("common.js");
onmessage = function(message){
    var itemId = message.data;
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
        if (this.readyState == 4 && this.status == 200){
            postMessage({responseXml: this.responseText});
        }
    };
    var urlStr = generateUrl(itemId);
    xhr.open("GET", "proxy?url=" + escape(urlStr));
    xhr.send(null);
}

Ce script est assez simple. On utilise Ajax pour faire un appel au proxy, qui appelle à son tour l'API eBay Shopping. Une fois que l'on a reçu le XML du proxy, on le renvoie à la tâche principale au moyen d'un objet Javascript. Remarquez que, bien que l'on puisse utiliser XMLHttpRequest dans un Worker, le résultat se trouvera dans sa propriété responseText, et jamais dans sa propriété responseXml. Ceci est dû au fait qu'il n'y a pas d'analyseur DOM dans la portée du script du Worker. Notez également que la fonction generateUrl vient du fichier common.js (voir le Listing 7). Ce script a été importé au moyen de la fonction importScripts.

Listing 7. Script importé par le Worker

function generateUrl(itemId){
    var appId = "YOUR APP ID GOES HERE";
    return "http://open.api.ebay.com/shopping?callname=GetSingleItem&"+
        "responseencoding=XML&appid=" + appId + "&siteid=0&version=665"
            +"&ItemID=" + itemId;
}

Maintenant que nous avons vu comment retrouver le détail des bonnes affaires (dans les navigateurs qui supportent les Web Workers), revenons à la Figure 1 pour comprendre comment cela est utilisé dans l'application. Remarquez qu'a côté de chaque affaire se trouve un bouton Show Details (ndT: Voir le détail). Cliquez-le pour que l'IHM soit modifiée comme dans la Figure 2.

Figure 1. Affichage du détail de l'affaire

Affichage du détail de l'affaire

Cette IHM est affichée lorsque l'on invque la fonction showDetails. Le Listing 8 donne le code de cette fonction.

Listing 8. La fonction showDetails

function showDetails(id){
    var el = $(id);
    if (el.style.display == "block"){
        el.style.display = "none";
    } else {
        el.style.display = "block";
        if (!el.innerHTML){
            var details = dealDetails[id];
            if (details){
                var ui = createDetailUi(details);
                el.appendChild(ui);
            } else {
                var itemId = id;
                var xhr = new XMLHttpRequest();
                xhr.onreadystatechange = function(){
                    if (this.readyState == 4 && 
                                      this.status == 200){
                        var itemDetails = 
                                        parseFromXml(this.responseText);
                        if (window.localStorage){
                            localStorage.setItem(
                                              itemDetails.id, 
                                              this.responseText);
                        }
                        dealDetails[id] = itemDetails;
                        var ui = createDetailUi(itemDetails);
                        el.appendChild(ui);
                    }
                };
                var urlStr = generateUrl(id);
                xhr.open("GET", "proxy?url=" + escape(urlStr));
                xhr.send(null);                        
            }
        }
    }
}

Cette fonction reçoit l'ID de l'affaire qui va être affichée et on bascule le mode d'affichage. Lors du premier appel, on regarde si les détails ont déjà été stockés dans le tableau associatif dealDetails. Si le navigateur supporte les Web Workers, ces détails ont déjà été récupérés et on crée juste le rendu visuel que l'on ajoute au DOM. Si ces détails n'ont pas encore été chargés, ou si le navigateur ne supporte pas les Web Workers, on fait un appel Ajax pour charger ces données. C'est ce qui fait que l'application marche que les Web Workers soient présents ou non. Autrement dit, si les Workers sont supportés, les données sont déjà chargées et l'IHM répondra instantanément. Sinon, l'IHM les présentera quand même, mais cela prendra quelques secondes.

En résumé

Les Web Workers peuvent sembler une nouvelle technologie assez exotique pour les développeurs Web. Mais, comme vous l'avez vu dans cet article, ils ont des applications très pratiques. C'est tout particulièrement vrai pour les applications Web mobiles. Les Workers peuvent être utilisés pour précharger des données ou exécuter d'autres opérations en avance, ce qui permet d'obtenir une IHM bien plus réactive. C'est encore plus vrai dans des applications Web mobiles qui doivent charger des données à travers un réseau potentiellement lent. Si vous combinez cela avec des stratégies de cache, vos utilisateurs seront impressionnés par la vélocité de votre application.