{"id":2099,"date":"2009-02-19T23:30:00","date_gmt":"2009-02-19T23:30:00","guid":{"rendered":"http:\/\/svay.com\/blog\/?p=2099"},"modified":"2009-02-19T23:30:00","modified_gmt":"2009-02-19T23:30:00","slug":"creer-un-client-twitter-offline-pour-l-iphone-avec-html5","status":"publish","type":"post","link":"https:\/\/svay.com\/blog\/creer-un-client-twitter-offline-pour-l-iphone-avec-html5\/","title":{"rendered":"Cr\u00e9er un client Twitter offline pour l&#8217;iPhone avec HTML5"},"content":{"rendered":"<p>Cet article va expliquer comment cr\u00e9er une application Twitter simple, qui fonctionne qu\u2019on soit connect\u00e9 ou non. Pour cel\u00e0, nous allons utiliser des technologies Web, en particulier des nouveaut\u00e9s de HTML5 qui permettent de stocker des donn\u00e9es c\u00f4t\u00e9 client et de mettre des fichiers en cache. Le navigateur Safari de l\u2019iPhone se pr\u00eate bien \u00e0 l\u2019exercice, puisqu\u2019il supporte ces technologies.<\/p>\n<p>Les plus press\u00e9s peuvent tester imm\u00e9diatement la <a href=\"\/experiences\/iphone-twitter\/\">d\u00e9mo<\/a>.<\/p>\n<h2>Le squelette de l&#8217;application<\/h2>\n<p>Tout d\u2019abord, il faut cr\u00e9er le squelette HTML de notre application. Ce fichier HTML devrait faire l\u2019affaire:<\/p>\n<pre> &lt;!DOCTYPE HTML&gt; &lt;html&gt; &lt;head&gt; &lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text\/html; UTF-8&quot; \/&gt; &lt;meta http-equiv=&quot;Content-Script-Type&quot; content=&quot;text\/javascript&quot; \/&gt; &lt;meta http-equiv=&quot;Content-Style-Type&quot; content=&quot;text\/css&quot; \/&gt; &lt;meta http-equiv=&quot;Content-Language&quot; content=&quot;en&quot; \/&gt; &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=1;&quot;\/&gt; &lt;style type=&quot;text\/css&quot;&gt;     body {         font-family: sans-serif;         margin: 0;         padding: 0;     }     #toolbar {         background: #EFEFEF;         padding: 5px;         border-bottom: 1px solid #a3a3a3;     }     #toolbar button {         border: 1px solid #a1a1a1;         -webkit-border-radius: 3px;         background: #F3F3F3;         padding: 0.5em;     }     ol {         margin: 0; padding: 0;     }     ol li{         list-style: none;         border-bottom: 1px solid #CCC;         margin: 0;         padding: 0.5em;         overflow: auto;     }     span.text {         display: block;         margin-left: 29px;     }     li img {         float: left;     }     .date {         font-size: 0.8em;         color: #AAA;     } &lt;\/style&gt; &lt;title&gt;iPhone Twitter&lt;\/title&gt; &lt;\/head&gt; &lt;body&gt; &lt;div id=&quot;toolbar&quot;&gt;     &lt;button id=&quot;load&quot;&gt;&amp;#8635; Refresh&lt;\/button&gt; &lt;\/div&gt; &lt;ol id=&quot;timeline&quot;&gt;&lt;\/ol&gt; &lt;script type=&quot;text\/javascript&quot; src=&quot;twitter.js&quot;&gt;&lt;\/script&gt; &lt;script type=&quot;text\/javascript&quot;&gt; &lt;!-- window.onload = function() {     Twitter.init();     document.getElementById('load').onclick = function() {         Twitter.load();     } } --&gt; &lt;\/script&gt; &lt;\/body&gt; &lt;\/html&gt; <\/pre>\n<p>Ce fichier se d\u00e9compose de la mani\u00e8re suivante:<\/p>\n<ul>\n<li>L\u2019ent\u00eate HEAD avec le doctype HTML5, une balise META sp\u00e9cifique \u00e0 l\u2019iPhone, un peu de CSS (et m\u00eame un peu de CSS3);<\/li>\n<li>Le corps BODY qui contient une toolbar et une liste ordonn\u00e9e vide;<\/li>\n<li>L\u2019inclusion d\u2019un fichier JS et un peu de code pour d\u00e9marrer l\u2019application et ajouter un le comportement \u00e0 notre toolbar.<\/li>\n<\/ul>\n<p>Passons maintenant au fichier twitter.js<\/p>\n<h2>Acc\u00e8s \u00e0 la base de donn\u00e9es<\/h2>\n<p>La premi\u00e8re chose, est d\u2019ouvrir la base de donn\u00e9es. Si elle n\u2019existe pas, elle sera cr\u00e9\u00e9e automatiquement.<\/p>\n<pre> var db = null; try {     if (window.openDatabase) {         db = openDatabase(&quot;Twitter&quot;, &quot;1.0&quot;, &quot;Twitter Feed&quot;, 200000);     } } catch(err) {} <\/pre>\n<p>La fonction <code>openDatabase<\/code> prend 4 param\u00e8tres:<\/p>\n<ul>\n<li>le nom de la base<\/li>\n<li>la version de la base<\/li>\n<li>le nom long (ou description)<\/li>\n<li>la taille attendue de la base<\/li>\n<\/ul>\n<p>La base sera accessible \u00e0 travers la variable globale <code>db<\/code>.<\/p>\n<h2>L&#8217;objet Twitter<\/h2>\n<p>On va ensuite cr\u00e9er un objet <code>Twitter<\/code> qui va contenir le code pour le chargement et l\u2019affichage des tweets.<\/p>\n<pre> var Twitter = {     count: 20 } <\/pre>\n<p>Pour le moment il ne contient qu\u2019un attribut de configuration: le nombre de tweets \u00e0 t\u00e9l\u00e9charger \u00e0 chaque rafra\u00eechissement. Mais nous allons tout de suite lui ajouter des m\u00e9thodes dans le chapitre suivant.<\/p>\n<h2>Cr\u00e9ation de la table et affichage des tweets<\/h2>\n<p>Au chargement de l\u2019application, nous allons tenter de cr\u00e9er la table (si elle n\u2019existe pas) et lire les tweets qu\u2019elle contient pour les afficher. Comme les donn\u00e9es sont stock\u00e9e en local, les tweets s\u2019afficheront, m\u00eame si la connexion Internet est interrompue.<\/p>\n<p>En revanche, si la base de donn\u00e9es n\u2019est pas disponible, nous allons r\u00e9cup\u00e9rer les tweets directement en ligne.<\/p>\n<p>On va donc ajouter la m\u00e9thode <code>init<\/code> qui suit:<\/p>\n<p>Cr\u00e9ation de la table:<\/p>\n<pre> \/\/Create table init : function() {     console.log('init');     if (db) {         \/\/Have database? Read data         db.transaction(function(tx) {             tx.executeSql(                 &quot;CREATE TABLE IF NOT EXISTS status (id REAL UNIQUE, username TEXT, created_at TEXT, text TEXT, avatar TEXT)&quot;,                 [],                 function(result) {                     Twitter.readStatus();                 },                 function(tx, error) {                     Twitter.readStatus();                 }             );         });     } else {         \/\/No database? Just load data and display         Twitter.load();     } }, <\/pre>\n<p>Si la base de donn\u00e9es est bien l\u00e0 et la table cr\u00e9\u00e9e, nous pouvons faire une requ\u00eate pour r\u00e9cup\u00e9rer les tweets:<\/p>\n<pre> \/\/Read statuses readStatus : function() {     console.log('read');     db.transaction(function(tx) {         tx.executeSql(&quot;SELECT id, username, created_at, text, avatar FROM status ORDER BY id DESC&quot;, [], function(tx, result) {             var timeline = document.getElementById('timeline');             timeline.innerHTML = '';             for (var i = 0; i &lt; result.rows.length; ++i) {                 var row = result.rows.item(i);                 Twitter.display(row);             }         }, function(tx, error) {             \/\/ Couldn't retrieve tweets             return;         });     }); }, <\/pre>\n<p>Rien de tr\u00e8s compliqu\u00e9: la requ\u00eate de type <code>SELECT<\/code> r\u00e9cup\u00e8re les donn\u00e9es et on passe chaque ligne r\u00e9sultat \u00e0 une fonction qui va se charger de les afficher.<\/p>\n<p>L\u2019affichage d\u2019un tweet est effectu\u00e9 par une fonction qui va simplement g\u00e9n\u00e9r\u00e9 un <code>LI<\/code> pour l\u2019ajouter \u00e0 notre liste ordonn\u00e9e:<\/p>\n<pre> \/\/Display statuses display : function(row) {     console.log('display');     var timeline = document.getElementById('timeline');     var li = document.createElement('LI');     var d = new Date();     d.setTime(Date.parse(row['created_at']));     li.innerHTML =         '&lt;img src=&quot;'+row['avatar']+'&quot; alt=&quot;&quot; height=&quot;24&quot; widht=&quot;24&quot;\/&gt;'+         '&lt;span class=&quot;text&quot;&gt;'+         '&lt;strong&gt;'+row['username'] + '&lt;\/strong&gt; ' +         row['text'] +         ' &lt;span class=&quot;date&quot;&gt;' + prettyDate(d) + '&lt;\/span&gt;'+         '&lt;\/span&gt;';     timeline.appendChild(li); }, <\/pre>\n<p>Notez que cette fonction utilise une autre fonction prettyDate qui formatte la date pour la rendre lisible. Je ne documenterai pas cette fonction mais vous la trouverez dans la d\u00e9mo.<\/p>\n<h2>Remplissage de la base de donn\u00e9es<\/h2>\n<p>Pour le moment, notre table ne contient aucun tweet, donc rien ne s\u2019affiche. Nous allons charger les donn\u00e9es twitter, les stocker et les afficher.<\/p>\n<p>Pour faire simple, nous allons utiliser l\u2019API JSONP de Twitter pour charger les 20 derniers tweets. La m\u00e9thode load fait plusieurs choses:<\/p>\n<ul>\n<li>nettoyer les <code>&lt;script&gt;<\/code> pr\u00e9c\u00e9dents qui auraient pu \u00eatre ajout\u00e9 par un pr\u00e9c\u00e9dent chargement<\/li>\n<li>donn\u00e9e un feedback visuel pour indiquer que le chargement a d\u00e9marr\u00e9<\/li>\n<li>charger les tweets en ins\u00e9rer le fichier JSONP en tant que <code>&lt;script&gt;<\/code> dans le <code>&lt;head&gt;<\/code><\/li>\n<li>indiquer que le chargement est termin\u00e9 lorsque les donn\u00e9es sont l\u00e0<\/li>\n<\/ul>\n<p>Comme la timeline est prot\u00e9g\u00e9e par un login et un password, il faudra les entrer lorsque le navigateur le demande.<\/p>\n<pre> load : function() {     console.log('load');     \/\/Remove JSONP scripts     var head = document.getElementsByTagName('head')[0];     var scripts = null;     while ((scripts = head.getElementsByTagName('script')).length &gt; 0 ) {         head.removeChild(scripts[0]);     }     document.getElementById('load').innerHTML = 'Refreshing...';     var now = new Date();     var url = 'http:\/\/twitter.com\/statuses\/friends_timeline.json?callback=Twitter.twitterCallback&amp;count={count}&amp;random={random}';     var script = document.createElement('script');     script.setAttribute(         'src',         url.replace('{count}',Twitter.count)            .replace('{random}',now.getTime())     );     script.onload = function() {         document.getElementById('load').innerHTML = '&amp;#8635; Refresh';     }     console.log(script);     head.appendChild(script); }, <\/pre>\n<p>Une fois le chargement termin\u00e9, un callback est automatiquement appel\u00e9. Si la base de donn\u00e9es existe, il va simplement extraire les donn\u00e9es qui nous int\u00e9ressent et les stocker dans la base de donn\u00e9es. Sinon, il va afficher chaque tweet.<\/p>\n<pre> twitterCallback : function(obj) {     console.log('callback');     if (db) {         var inserts = [];         for (var i=0, l=obj.length; i&lt;l; i++) {             var status = [                 obj[i].id,                 obj[i].user.screen_name,                 obj[i].text,                 obj[i].created_at,                 obj[i].user.profile_image_url             ]             inserts.push(status);         }         Twitter.insert(inserts, Twitter.readStatus);     } else {         \/\/No database? just display         for (var i=0, l=obj.length; i&lt;l; i++) {             Twitter.display({                 'id' : obj[i].id,                 'username' : obj[i].user.screen_name,                 'text' : obj[i].text,                 'created_at' : obj[i].created_at,                 'avatar' : obj[i].user.profile_image_url             });         }     } }, <\/pre>\n<p>Il faut savoir que l\u2018<code>INSERT<\/code> est asynchrone et risque de lancer l\u2019affichage avant que nos donn\u00e9es soient enti\u00e8rement stock\u00e9es. Nous utilisons donc notre propre fonction <code>Twitter.insert<\/code> qui va faire les insertions et n\u2019afficher qu\u2019\u00e0 la fin.<\/p>\n<p>Voil\u00e0 notre m\u00e9thode d\u2019insertion qui simule une insertion synchrone. Elle prend deux param\u00e8tres: le tableau de donn\u00e9es et un callback \u00e0 executer en find d\u2019insertion.<\/p>\n<pre> \/\/Synchronous insert insert : function(arStatus, callback) {     var status = arStatus.pop();     var sql = &quot;INSERT INTO status (id, username, text, created_at, avatar) VALUES (?,?,?,?,?)&quot;;     db.transaction(         function (tx) {             tx.executeSql(                 sql,                 status,                 function(tx, result){                     if (arStatus.length &gt; 0) {                         Twitter.insert(arStatus, callback);                     } else {                         callback();                     }                 },                 function(tx, error){                     if (arStatus.length &gt; 0) {                         Twitter.insert(arStatus, callback);                     } else {                         callback();                     }                 }             );         }     ); } <\/pre>\n<p>Voil\u00e0, nous avons tout le code n\u00e9cessaire pour r\u00e9cup\u00e9rer des tweets, les stockers localement et les afficher sans repasser par le serveur.<\/p>\n<h2>Cache d&#8217;applications offline<\/h2>\n<p>Jusque l\u00e0, nous avons cr\u00e9\u00e9 une application web qui conserve des donn\u00e9es en local. Une connexion reste n\u00e9anmoins n\u00e9cessaire pour afficher la page HTML et charger le javascript. Heureusement, HTML5 offre un m\u00e9canisme qui permet de garder ces deux fichiers en cache et d\u2019y acc\u00e9der m\u00eame sans connexion Internet. Cette fonctionnalit\u00e9 est offerte par le <code>cache-manifest<\/code>, un fichier qui indique au navigateur la liste des fichiers \u00e0 garder en cache. Ces fichiers ne seront plus jamais rafraichis, \u00e0 moins que le <code>cache-manifest<\/code> ne change.<\/p>\n<p>Ce fichier doit \u00eatre envoy\u00e9 avec le bon type MIME <code>Content-type: text\/cache-manifest<\/code> (avec PHP par exemple) pour fonctionner. Notez qu\u2019on inclue un commentaire avec la date pour versionner ce fichier. Il suffira de changer la date pour forcer un rechargement complet de l\u2019application.<\/p>\n<pre> CACHE MANIFEST #20090201-1731 index.html twitter.js <\/pre>\n<p>Un fois ce fichier cr\u00e9\u00e9, il suffit de l\u2019inclure dans le fichier HTML en modifiant la balise HTML:<\/p>\n<pre> &lt;html manifest=&quot;cache-manifest.php&quot;&gt; <\/pre>\n<h2>Conclusion<\/h2>\n<p>Si vous avez bien suivi toutes les \u00e9tapes (et si j\u2019ai bien expliqu\u00e9), vous devriez avoir une application qui fonctionne totallement offline. Lors de la premi\u00e8re connexion, la page HTML et le JS se chargent et sont mis en cache. Ils pourront \u00eatre acc\u00e9d\u00e9s n\u2019importe quand par la suite, m\u00eame sans connexion. L\u2019application permet ensuite d\u2019afficher des tweets stock\u00e9s localement, sans t\u00e9l\u00e9chargement quelque donn\u00e9e.<\/p>\n<p>Pour t\u00e9l\u00e9charger les fichiers complet et tester l\u2019application, tout est disponible \u00e0 cette adresse: <a href=\"\/experiences\/iphone-twitter\/\">http:\/\/svay.com\/experiences\/iphone-twitter\/<\/a><\/p>\n<p>Cet exemple est plut\u00f4t basique et vous aurez certainement des id\u00e9es d\u2019applications plus \u00e9labor\u00e9es. D\u2019ailleurs, il semblerait que Google lance dans peu de temps une version offline de GMail pour iPhone en utilisant les m\u00eame technologies.<\/p>\n<p>Enfin, voil\u00e0 les liens qui ont servi \u00e0 pr\u00e9parer ce tuto:<\/p>\n<ul>\n<li><a href=\"http:\/\/www.w3.org\/TR\/offline-webapps\/\">Offline Web Applications<\/a> sur le site du W3C<\/li>\n<li><a href=\"http:\/\/webkit.org\/blog\/126\/webkit-does-html5-client-side-database-storage\/\">WebKit Does HTML5 Client-side Database Storage<\/a> sur le blog de Webkit<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Cet article va expliquer comment cr\u00e9er une application Twitter simple, qui fonctionne qu\u2019on soit connect\u00e9 ou non. Pour cel\u00e0, nous allons utiliser des technologies Web, en particulier des nouveaut\u00e9s de HTML5 qui permettent de stocker des donn\u00e9es c\u00f4t\u00e9 client et de mettre des fichiers en cache. Le navigateur Safari de l\u2019iPhone se pr\u00eate bien \u00e0 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[27],"tags":[],"_links":{"self":[{"href":"https:\/\/svay.com\/blog\/wp-json\/wp\/v2\/posts\/2099"}],"collection":[{"href":"https:\/\/svay.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/svay.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/svay.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/svay.com\/blog\/wp-json\/wp\/v2\/comments?post=2099"}],"version-history":[{"count":0,"href":"https:\/\/svay.com\/blog\/wp-json\/wp\/v2\/posts\/2099\/revisions"}],"wp:attachment":[{"href":"https:\/\/svay.com\/blog\/wp-json\/wp\/v2\/media?parent=2099"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/svay.com\/blog\/wp-json\/wp\/v2\/categories?post=2099"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/svay.com\/blog\/wp-json\/wp\/v2\/tags?post=2099"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}