Dans l’article
précédent, nous avons rendu une interface HTML disponible
hors-ligne via l’utilisation du fichier manifest de cache. L’interface
était totalement statique, ne supportait que la consultation en lecture
seule et la gestion du contenu dynamique reposait sur les différents
versions du manifest ; les nouvelles API JavaScript vont nous aider à
résoudre ces problèmes.
Avec le localStorage et l’état window.onLine, nous pourrons stocker le
contenu dynamique et différencier les traitements JavaScript selon le
mode (dé)connecté.
Cet article s’appuie sur la librairie PrototypeJS pour la majorité des
exemples de code JavaScript.
La première étape est d’afficher un bloc indiquant à l’utilisateur l’état de la connexion. Le bloc affiche online ou offline en fonction du mode actif (connecté / déconnecté).
Il faut tout d’abord créer le bloc HTML dont nous avons besoin :
1
2
3
|
< div id = "line-status"
class = "online" > Online </ div > |
Un bloc div contient le libellé associé au statut. Nous avons
positionné un id sur l’élément pour le manipuler facilement en
JavaScript et la classe CSS nous servira à changer la couleur de fond :
rouge pour connecté et vert pour déconnecté. Nous pouvons maintenant
modifier le texte et la classe du bloc en fonction de l’objet navigator.onLine
.
1
2
3
4
5
|
setInterval( function ()
{ var status
= document.getElementById( 'line-status' ); status.className = navigator.onLine ? 'online' : 'offline' ; status.innerHTML = navigator.onLine ? 'Online' : 'Offline' ; },
250); |
Le texte et la classe du bloc #line-status
seront mis
à jour toutes les 250ms en fonction de la valeur de l’objet navigator.onLine
.
Vous pouvez tester ce code en coupant la connexion wifi/3G de votre
ordinateur/téléphone (avec firefox, cliquer sur Travailler Hors
Connexion). Une exécution toutes les 250ms a tendance à être
consommatrice en ressources ; utilisons plutôt les événements associés
à cet état fournis par HTML5.
1
2
3
4
5
6
7
8
9
10
|
window.addEventListener( 'online' , function (){ var status
= document.getElementById( 'line-status' ); status.className = 'online' ; status.innerHTML = 'Online' ; },
false ); window.addEventListener( 'offline' , function (){ var status
= document.getElementById( 'line-status' ); status.className = 'offline' ; status.innerHTML = 'Offline' ; },
false ); |
Notez que l’événement est levé sur l’objet window et non sur l’objet
navigator. Avec ce code, la mise à jour du bloc est réalisée à chaque
fois que le navigateur change d’état de connexion. Dans cette version,
nous reposons entièrement sur le fonctionnement du navigateur pour
différencier le mode connecté du mode déconnecté. Si cela fonctionne
sur Webphone, sur ordinateur portable la coupure de la connexion réseau
ne change pas l’état du navigateur. Seul le passage en mode hors
connexion permet de lancer l’événement offline. Le navigateur restera
online si les requêtes partent en timeout ou finissent en ‘host not
found’ par exemple.
Pour affiner la gestion du mode hors ligne en JavaScript, nous allons
maintenir l’état de la connexion dans un objet qui réagira aux
événements du navigateur et fournira la possibilité de changer
directement l’état de la connexion.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
Utils.LinkClass
= Class.create({ // Constructeur initialize: function
(elem, config){ // Par défaut nous utilisons la valeur du
navigateur this .online
= navigator.onLine; // Récupérer la div par son id this .elem =
$(elem); // Ecouter les évènements online/offline sur
l'objet this .evtOnLine
= this .updateTagOnline.bindAsEventListener( this ); this .evtOffLine
= this .updateTagOffline.bindAsEventListener( this ); Event.observe(window, 'online ', this.evtOnLine); Event.observe(window, ' offline ',
this.evtOffLine); // Rendre les classes CSS et les libellés
paramétrables this.config = Object.extend({ offlineText:' Offline ', onlineText:' Online ', onlineClass:' online ', offlineClass:' offline ' }, config || { }); this.updateTag(this.online); }, // change l' état
et met à jour la div si l 'état
fourni est différent de celui de l' objet setLineState: function (online){ if (online
!= this .online){ this .updateTag(online); } }, // Met à jour l'état et la div sur évènement
online updateTagOnline: function (evt){ this .updateTag( true ); }, // Met à jour l'état et la div sur évènement
offline updateTagOffline: function (evt){ this .updateTag( false ); }, // Met à jour l'état et la div avec le
booléen fourni updateTag: function
(online) { this .online
= online; if (online)
{ this .elem.addClassName( this .config.onlineClass
); this .elem.update( this .config.onlineText); this .elem.removeClasseName( this .config.offlineClass); } else
{ this .elem.addClassName( this .config.offlineClass); this .elem.update( this .config.offlineText); this .elem.removeClasseName( this .config.onlineClass); } } }); // Instancier Utils.Link lorsque la page est
chargée Event.observe (window, 'load ', function (){ Utils.link = new Utils.LinkClass(' line-status'); }); |
Dans le cas où l’état de la connexion est mis à jour directement sans émettre un événement du navigateur, vous pouvez lancer un service de ping http qui ira vérifier à intervalles réguliers la disponibilité du serveur et mettra à jour l’état de la connexion.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
Utils.PingServiceClass
= Class.create({ initialize: function
(config){ this .timer
= null ; // Configuration par défaut de la fréquence
de ping en ms this .config
= Object.extend({ pingInterval: 20000, startDelay: 1000, url: null , //
Url ciblée par le ping Ajax onUplink: function (){}}, //
fonction invoquée quand le ping réussi config || {}); this .url = this .config.url; }, // méthode de ping ping: function (){ if ( this .timer
!= null ){ new Ajax.Request( this .url, { method: 'get' , onSuccess: function (transport) { if (transport.status
< 300 && transport.status >= 200){ this .timer
= null ; // appel du callback si le ping AJAX a réussi this .config.onUplink(); } else
if ( this .timer != null ){ // Planification du prochain ping en cas
d'erreur this .timer
= setTimeout( this .ping.bind( this ), this .config.pingInterval); } }, // Planification du prochain ping en cas
d'erreur onFailure: function (){ if ( this .timer
!= null ) { this .timer
= setTimeout( this .ping.bind( this ), this .config.pingInterval); } } }); } }, // Lancement du service de ping start: function (){ if ( this .timer
== null ){ this .timer
= setTimeout( this .ping.bind( this ), this .config.startDelay); } }, // Arret forcé du ping stop: function (){ if ( this .timer
!= null ){ clearTimeout( this .timer); this .timer
= null ; } } }); |
La classe PingServiceClass utilise la fonction setTimeout
pour assurer une exécution régulière et asynchrone. À chaque timeout,
elle lance une requête Ajax sur l’URL fournie en configuration. Le ping
se lance par un appel à start()
et peut être arrêté
par un appel à stop()
. Il faut aussi modifier la classe Utils.LinkClass
pour qu’elle utilise un PingService
lorsqu’un script
forcera le passage hors-ligne(Utils.link.setLineState(false);
).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
//... initialize: function(config){ //... if ( this .config.ping){
// Instanciation du
PingService si la configuration l'autorise this .ping = new Utils.PingServiceClass({ url: this .config.url, onUplink: this .updateTagOnline.bind( this ) // Passer Online quand le ping réussi }); } }, //... setLineState: function (online){ if (online != this .online){ this .updateTag(online); if (!online
&& this .config.ping){ //
Lancement du Ping HTTP sur passage forcé en mode hors-ligne this .ping.start(); } } }, updateTagOnline: function (evt){ this .updateTag( true ); // Arret du Ping HTTP si le navigateur passe en
ligne if ( this .config.ping){ this .ping.stop(); } }, //... Event.observe
(window, 'load' , function (){ //
Ajoute l'url de ping en configuration Utils.link = new
Utils.LinkClass( 'line-status' ,{url: '/ping.html' }); }); |
Nous avons maintenant un moyen de notifier l’utilisateur et de savoir à tout moment si l’application est en ligne ou non. Le service ping sera utilisé seulement sur les navigateurs ne supportant pas ou mal les événements ‘online’ et ‘offline’. Notez que le ping est exécuté sur l’URL ‘/ping.html’ qui fournit une simple page html statique hors du manifest.
La première étape, en partant d’une application existante, consiste
à ajaxifier son contenu dynamique. Pour reprendre le cas du blog, la
page d’accueil liste les en-têtes des articles les plus récents. La
première solution consiste à récupérer le bloc HTML listant les
articles en Ajax et à l’injecter dans la page. Attention pour les
moteurs de recherches et pour des questions d’accessibilité, la page
doit toujours être servie avec l’intégralité de son contenu.
Dans le cas d’une page liée au fichier manifest, le problème est de
savoir quand mettre à jour la liste. La page étant affichée à partir du
cache, son contenu ne changera qu’avec le fichier manifest. Le plus
simple est de récupérer le bloc HTML systématiquement au chargement de
la page, puis de planifier des mises à jour à intervalles réguliers
pour garantir la fraîcheur de l’information.
1
|
new
Ajax.PeriodicalUpdater( 'blocElementId' , '/articles' ,
{frequency: 30 , decay: 2 }); |
L’exemple ci-dessus crée un PeriodicalUpdater qui rechargera les
articles toutes les 30s. Si le contenu ne change pas, la prochaine
exécution sera planifiée avec un timeout de 30×2 secondes en doublant
le decay
après chaque passage.
Afin d’améliorer l’expérience utilisateur, pensez à cacher le contenu par css au chargement initial ; le JavaScript l’affichera évitant à l’utilisateur la vue de contenu obsolète à chaque affichage de la page. Pensez aussi à mettre une animation d’attente ou une barre de chargement si vous êtes courageux. Pour la touche finale, vous pourrez ajouter un effet pour faire apparaître la liste des articles.
Dans le cadre d’un blog, la page d’accueil liste les articles les
plus récents avec pour chaque article, un lien vers l’article complet
et son pitch. Sans oublier les boutons de navigation pour consulter des
articles plus anciens. Tous ces liens doivent aussi être traités en
JavaScript.
Le script devra prendre soin d’intercepter les événements ‘click’ sur
les liens d’articles et de pagination pour permettre l’exécution en
Ajax de la requête et la mise à jour de l’interface.
A l’aide d’un sélecteur CSS, nous allons retrouver les liens et
observer les événements click
en exécutant la fonction
d’affichage de l’article ou de la liste.
1
2
3
4
5
6
7
8
9
10
|
// Utilise l'URL du lien d'article pour
afficher ce dernier Blog.displayArticle
= function (evt){ var url =
evt.element().href; // ... Update Ajax de #uiBody }; // Intercepter le click sur un article et
afficher ce dernier $$( '#uiBody a.article' ).each( function (item){ item.observe( 'click' , Blog.displayArticle); }); |
Pour offrir plus de flexibilité au script, je vous recommande de
passer d’un échange Ajax en HTML brut à une donnée formatée en JSON.
Nous pourrons communiquer des informations au script, comme le jeton de
version, sans impacter le HTML. Le formatage des articles en JSON
pourra contenir la vue HTML de pré-visualisation pour la page d’accueil
et le HTML complet de l’article. L’utilisation du code HTML dans
l’objet JSON évite la création chronophage des éléments HTML par le
script.
Dans le cas de la liste d’articles, vous allez devoir gérer la
pagination de la liste à la main. Dans l’objet JSON remonté par le
serveur, pensez à intégrer les informations nécessaires à la pagination
(nombre d’éléments total de la liste, numéro de la page, nombre
d’éléments par page). La génération des boutons de navigation peut par
exemple reposer sur le plugin JQuery pagination (http://plugins.jquery.com/project/pagination).
Le format JSON nous servira aussi au stockage des données côté
navigateur. Le localStorage
que nous allons utiliser ne
supporte que des valeurs de type chaîne de caractères. Les objets y
seront donc persistés en chaînes JSON, de manière à maintenir la
structure objet sans surcoût de mapping.
Attention à l’injection de code dans vos objets JSON,
pensez à toujours passer par une librairie JavaScript comme PrototypeJS
ou JQuery pour évaluer l’arbre JSON. Elles se chargeront pour vous de
faire les contrôles de syntaxe nécessaires ainsi que la conversion
automatique du contenu JSON des réponses AJAX.
Que se passe-t-il lorsque le navigateur est hors ligne ? En l’état actuel des choses, la page reste vide ou affiche un contenu obsolète issu du premier chargement. Dans certain cas, le bloc de notification restera même désespérément ‘online’.
Le script doit traiter les erreurs AJAX, et mettre à jour le bloc de
notification en cas d’indisponibilité du serveur. Pensez à positionner
un timeout sur vos requêtes pour limiter le temps de chargement et
détecter les erreurs réseau. Attention aussi aux redirections HTTP, les
requêtes Ajax les suivent si elles sont sur le même domaine. La requête
pourra être réussie et contenir la page de login vers laquelle vous
aurez été redirigé si vous n’avez plus de session.
En cas de problème réseau, mettez à jour le bloc de notification Utils.link.setLineState(false);
et exécutez le traitement hors-ligne reposant sur les données stockées
dans le navigateur. Si vous n’utilisez pas le ping HTTP, pensez aussi à
mettre à jour le bloc de notification lorsqu’une requête Ajax réussit
pour éviter la notification hors-ligne éternelle.
Bien sûr HTML5 fournit différents supports de persistance dans le
navigateur allant de la base de données SQL au stockage dans des tables
clé-valeur en passant par les tables clé-valeur indexées. Aujourd’hui
seul le stockage de type clé-valeur (localStorage
, sessionStorage
)
est supporté par tous les navigateurs. Les autres API sont
intéressantes et pourront faire l’objet d’études futures mais pour cet
article, je me suis contenté d’utiliser le localStorage
directement disponible sur tous les navigateurs.
Attention, le localStorage
ne convient pas à la
persistance d’une grande quantité de tuples. Ne comptez pas non plus
fournir des fonctionnalités de recherche hors-ligne. L’API du localStorage
permet simplement de lister les clés, de récupérer la chaîne associée à
une clé et d’associer une chaîne à une clé. Nous pourrons aussi
supprimer une clé et sa valeur de la table ou vider l’intégralité de la
table. C’est le script qui va devoir combler les manques de l’API en
maintenant ses propres index.
Choisissons maintenant le support que nous allons fournir:
Vous l’aurez compris, c’est la synchronisation automatique que nous allons retenir pour assurer l’accès au blog hors-ligne et en lecture seule.
Nous voulons lister des articles pour pouvoir afficher une
introduction dans la liste des articles et le contenu intégral dans la
page d’article. Nous allons donc créer une vue logique servie par un
DAO article qui se chargera de persister les articles, et de les
lister. Le DAO pourra par exemple, maintenir la liste des identifiants
d’article dans un tableau stocké avec la clé article.index
.
Cette liste sera triée par ordre de date de publication décroissante
pour éviter d’avoir à réaliser des tris couteux en JavaScript.
Chaque article sera stocké avec une clé de type article.id
en remplaçant id
par l’identifiant de l’article. Pour
limiter le temps de traitement l’introduction et le contenu serront
stockés séparément. Cela évite le chargement des articles complets pour
toute les pages affichant une liste d’articles (pagination, tri,
recherche, …). La clé article.index
contiendra en plus
des identifiants les introductions de chaque article.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
var Article
= {}; Article.DAOLocalStorageClass
= Class.create( { initialize: function (){ var articles
= window.localStorage.getItem( "article.index" ); if (articles
!= null && articles.isJSON()){ this .articles=
articles.evalJSON(); } else
{ this .articles
= []; } }, getArticleList: function (){ return this .articles; }, getArticle: function (id){ var article
= window.localStorage.getItem( "article." +id); if (article
!= null && article.isJSON()){ return article.evalJSON(); } return null ; }, setArticleList: function (list){ if (list
!= null ){ window.localStorage.setItem( "article.index" , list.toJSON()); this .articles
= list; } else { this .articles
= []; } }, setArticle: function
(article){ if (article
!= null ){ window.localStorage.setItem( "article." +article.id,
article.toJSON()); } }, removeArticle: function (id){ window.localStorage.removeItem( "article." +id); }, clear: function (){ this .articles.each( function (item){ window.localStorage.removeItem( 'article.' +item.id); }); window.localStorage.removeItem( 'article.index' ); this .articles
=[]; } }); Article.DAO = new Article.DAOLocalStorageClass(); |
Avec ce DAO nous sommes capables de gérer une petite liste
d’articles persistés dans le navigateur. Si le navigateur implémente le
localStorage
et autorise le site à l’utiliser, notre DAO
fonctionnera sans problème. Mais le DAO ne supporte pas de dégradation
en cas d’indisponibilité du localStorage
. La solution
retenue est d’utiliser un bouchon à la place du localStorage
dans ce cas.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
initialize:
function (){ this .enabled
= false ; this .storage
= this .getStorage(); // ... }, getStorage: function (){ if (window.localStorage){ this .enabled
= true ; return window.localStorage; } else
{ return { setItem: function
(){}, getItem: function
(){ return null ;}, removeItem: function
() {} }; } }, |
La méthode getStorage()
fournit un bouchon et
positionne la propriété this.enabled
à true
si le navigateur supporte bien le localStorage
. Le reste
du DAO doit être modifié pour utiliser this.localStorage
au lieu de window.localStorage
.
Passons maintenant à la récupération des données à persister. Nous allons développer un service de synchronisation des articles les plus récents pour renseigner notre base d’articles locale.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
new Ajax.Request( 'sync' , { method: 'get' , onSuccess: function (transport) { if (transport.responseJSON
!= null ){ Article.DAO.clear();
Article.DAO.setArticleList(transport.responseJSON.articles); transport.responseJSON.articles.each( function (pitch){ new Ajax.Request( '/article/' +pitch.id,
{ onSuccess: function (transport) { if (transport.responseJSON
!= null ){ Article.DAO.setArticle(transport.responseJSON); } } }); }); } } } }); |
Le code ci-dessus invoque un web service JSON qui lui retourne la
liste des articles les plus récents avec les pitchs à la manière du
service listant les articles. La base locale est d’abord vidée
intégralement, puis la liste est persistée. Pour chaque article listé,
une requête Ajax est lancée pour en récupérer le contenu et le stocker.
Bien que la méthode soit assez consommatrice en ressources, elle a le
mérite de fonctionner. Une bonne amélioration à réaliser serait de ne
récupérer que les nouveaux articles tout en évitant de vider la base à
chaque synchronisation.
Une fois la liste récupérée, il faut supprimer de la base les articles
qui ne sont plus listés et télécharger tous les nouveaux ainsi que ceux
nécessitants une mise à jour (date ou version différente).
N’oublions pas la règle d’or de la notification utilisateur. Nous
devons montrer à l’utilisateur l’état de la synchronisation. Il suffit
de stocker dans un contexte un booléen à true si la synchronisation est
en cours et un compteur des requêtes Ajax lancées et non terminées.
Lorsque la derniere requête se terminera elle décrémentera le compteur
à zéro, passera le booléen à false et pourra alors notifier
l’utilisateur. Pour le contrôle d’erreur on ajoutera un champs error
initialement vide dans le contexte qui sera renseigné en cas d’erreur
Ajax.
1
|
var syncCtx
= { running: false , requestCount: 0, error: null }; |
Il faut lancer la synchronisation au chargement de la page et à
chaque fois que le navigateur passe en ligne. En principe pour un blog
cela devrait suffire car les utilisateurs ne restent pas connectés très
longtemps et le contenu change peu. Dans un site où le contenu évolue
rapidement, pensez à exécuter la synchronisation à intervalle régulier.
Attention, il est inutile de lancer ce nouveau service si !Article.DAO.enabled
,
puisqu’aucun article ne sera persisté dans ce cas.
Les fonctions d’affichage précédemment créées doivent-être modifiées pour utiliser le DAO lorsque le navigateur est hors-ligne. Comme vous avez pu le constater de nombreux cas d’erreur sont envisageables : veillez à afficher un contenu alternatif en cas d’indisponibilité des données.
Les requetes POST sont toujours envoyées sur le serveur sans tenir compte du cache manifest. Lors d’un POST redirect le navigateur pourra afficher la page depuis le cache. Si la réponse du POST contient la page elle sera affichée telle quelle par le navigateur. Seules les requêtes de type GET sont élligibles au cache. Quand le navigateur est hors-ligne, les formulaires utilisant la méthode POST vont afficher le message d’erreur HTTP habituel. Si toutefois vous utilisez la méthode GET, le manifest pourra être utilisé pour fournir une version hors-ligne de la page. Pour permettre l’utilisation d’un formulaire hors-ligne nous allons le soumettre via une requête AJAX en captant l’évènement ‘submit’ du formulaire. Le handler sérialisera les données du formulaire dans un objet JSON ou directement dans la requête AJAX à soumettre sous forme de POST HTTP au serveur.
1
2
3
4
5
6
7
8
9
10
11
12
|
// Ecouter le submit du formulaire $( 'form' ).observe( 'submit' , function (evt){ // Soumettre le formulaire en AJAX $( 'form' ).request({ method: 'post' , onComplete:
function () { // Affiche alerte 'Post Submitted' alert( 'Post
submitted !' ); }}); // Stopper l'évènement submit pour que le
navigateur ne le traite pas evt.stop(); }); |
Ajoutons un peu de traitement pour les cas d’erreur (indisponibilité
du serveur, authentification requise) et le mode hors-ligne. Dans les
deux cas, le principe sera d’enregistrer la modification dans le localStorage
sous la forme d’un évènement qui pourra être synchronisé lorsque la
connexion le permettra.
1
2
3
4
5
6
7
8
9
10
11
|
addEvent:
function (evt){ this .events.push(evt); this .storage.setItem( 'events' , this .events.toJSON()); }, getEvents:
function (){ return this .events; }, clearEvents:
function (){ this .events
= []; this .storage.removeItem( 'events' ); } |
Sur POST d’un nouveau commentaire, l’évènement est enregistré dans la base locale.
1
|
Article.DAO.addEvent({
type: 'comment' , comment: $( 'form' ).serialize( true )}); |
Pour indiquer à l’utilisateur que le commentaire est bien pris en
compte, pensez à vider les champs du formulaire et à afficher un
message d’information par exemple. Ajoutez aussi un élément à
l’interface pour indiquer à l’utilisateur qu’il y a des données à
synchroniser avec le serveur. Cela pourra être un bouton ou un lien qui
permettra de soumettre au serveur les modifications réalisées
hors-ligne ou hors session. Dans le cas des commentaires, il est
possible de prévoir une synchronisation automatique quand le navigateur
passe en ligne et au chargement de la page. En revanche cette pratique
est fortement déconseillée pour des évènements concernant des articles
modifiés.
Vous l’aurez compris tous les évènements qui nécessitent une
authentification de l’utilisateur doivent être synchronisés
manuellement à la demande de ce dernier. Prévoyez une interface
d’administration permettant de lister, éditer et supprimer les
évènements à synchroniser.
Avec l’utilisation conjointe du localStorage
et du
MANIFEST, nous pouvons fournir une application qui fonctionnera hors
ligne. Les étapes que nous avons suivies prises séparément sont assez
simples. Mais les impacts sont nombreux et plusieurs refactoring seront
nécessaires sur le serveur. Le passage au mode hors ligne ne se fera
pas sans mal. L’API localStorage
n’est pas destinée à
stocker et manipuler un grand nombre d’objets. Elle ne sera d’aucun
secours pour la recherche, et le tri des listes par exemple. En bref
les limitations du localStorage
compliquent fortement les
développements. Pour créer une application hors-ligne capable de gérer
un grand nombre de données et affichant des fonctionnalités égales à
des applications de bureau, vous devrez passer par l’utilisation d’un
autre mode de stockage comme l’API WebSqlDatabase
pour travailler en SQL ou bien l’API WebIndexedDatabase
qui répondent à ces besoins. Notez tout de même que WebSQLDatabase
n’est supportée ni par IE, ni par Firefox. L’API IndexedDB n’est de son
côté supportée que par Firefox et Chrome. Pour un support tout
navigateur vous devrez passer par l’utilisation des API fournies par Google Gears.
L’inconvénient étant que Gears n’implémente pas les API du standard en
cours d’écriture HTML5. À terme, l’extension risque même de disparaître
au profit des implémentations natives des navigateurs HTML5.