Réaliser une application offline en HTML5

LEFEVRE Franck

Cet article vous permettra de faire rapidement une application HTML5 qui gérera le mode offline de votre navigateur.
Commentez

- Titre : Réaliser une application offline en HTML5

- Auteur : LEFEVRE Franck

- Parution : 29 juin 2012

- Mise à jour : 10 décembre 2012

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2012 LEFEVRE Franck. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Prérequis

- III. La gestion du cache

- III-1. Théorie

- III-2. Pratique

- IV. Le mode hors-ligne

- IV-1. Test

- IV-2. Théorie

- IV-3. Pratique

- V. Le stockage local

- V-1. Théorie

- V-2. Pratique

- VI. Conclusion

- VII. Remerciements

I - Introduction

Cet article va vous apprendre à gérer trois technologies : HTML5 gérant le mode offline, le cache du navigateur et le stockage local. Notre exemple est une gestion de « to-do list » qui utilise trois technologies : HTML5 et JavaScript/jQuery pour le côté client et PHP pour le côté serveur.

II - Prérequis

Le code est simple. La page HTML contient une liste non numérotée contenant les tâches, ainsi qu'un formulaire. Dès que celui-ci est activé, le JavaScript intercepte le formulaire et l'envoie au fichier PHP pour qu'il l'enregistre dans un fichier texte.

Code HTML :

index.html

<!DOCTYPE html>
<html lang="fr">
<head>
<title>Article sur HTML5 et le mode offline</title>
<meta charset="utf-8">
<link href="style.css" media="screen" rel="stylesheet" type="text/css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js" type="text/javascript"></script>
<script src="script.js" type="text/javascript"></script>
</head>
<body>
<div class="content">
<h1>Article sur HTML5 et le mode offline</h1>
<form method="post" action="index.html" id="frmTodo">
<div>
<label for="txtItem">Item :</label>
<input type="text" name="txtItem" id="txtItem">
</div>
<input type="submit" value="Ajouter" />
</form>

<ul></ul>

<p>Source : <br/><a href="http://icones.pro/supprimer-15-image-png.html" target="_blank">Image : <img src="delete.png" alt="Supprimer" /></a></p>
</div>
</body>
</html>

Code CSS :

style.css

body{
background-color: #E0E0E0;
color: #556677;
font-family: Arial;
}
.content{
margin:0 auto;
text-align: center;
width:300px;
}
form > div{
padding: 10px;
}
input[type="text"]{
border: 1px solid #556677;
color: #556677;
width: 150px;
}
input[type="submit"]{
border: 1px solid #556677;
color: #556677;
font-weight: bold;
padding: 5px 26px;
}
ul{
padding: 0;
text-align: left;
}
li{
background-color: #CCCCCC;
list-style-type: none;
margin: 5px;
padding: 5px;
}
li:hover{
background-color: #AAAAAA;
}
li > img{
cursor:pointer;
float: right;
height: 16px;
width: 16px;
}
p{
margin: 60px;
}
p > a{
color: #556677;
font-weight: bold;
text-decoration: none;
}
p > a > img{
height: 16px;
width: 16px;
}

Code JavaScript :

script.js

$(document).ready(function(){
var jqoTaskLi = $('<li><span class="lblTask"></span><img src="delete.png" alt="Supprimer" class="delTask"/></li>');

// Lister les tâches à l'initialisation de la page
$.ajax({
url: 'ajax.php',
dataType: 'json',
type: 'POST',
data:{
type:'list'
},
success: function(data) {
if(data.result == true){
$.each(data.data, function(key, value) {
var jqoItem = jqoTaskLi.clone();
jqoItem.find('.lblTask').html(value);
$('ul').append(jqoItem);
});
}
}
});
// Événement lors de la soumission du formulaire
$('#frmTodo').submit(function(){
// On récupère le texte nettoyé des espaces avant et après
var sVal = $('#txtItem').val();
var sVal = $.trim(sVal);
if(sVal != ''){
// On envoie une requête AJAX de type POST pour ajouter la tâche
$.ajax({
url: 'ajax.php',
dataType: 'json',
type: 'POST',
data:{
type:'add',
item:sVal
},
success: function(data) {
if(data.result == true){
var jqoItem = jqoTaskLi.clone();
jqoItem.find('.lblTask').html(value);
$('ul').append(jqoItem);
$('#txtItem').val('');
}
}
});

}
return false;
});
$('#ulTodo').on('click', '.delTask', function(){
var jqoParent = $(this).parent();
// On envoie une requête AJAX de type POST pour supprimer la tâche
$.ajax({
url: 'ajax.php',
dataType: 'json',
type: 'POST',
data:{
type:'del',
pos:$('li').index(jqoParent)
},
success: function(data) {
if(data.result == true){
jqoParent.remove();
}
}
});
});
});

Code PHP :

ajax.php

<?php

$arrAction = array('add', 'del', 'list');
$arrResult = array('result' => false);

if(isset($_POST['type']) && in_array($_POST['type'], $arrAction)){
// Si le fichier contenant les tâches n'existe pas, on le crée.
if(!file_exists('db.txt')){
file_put_contents('db.txt', '');
}

// On charge le fichier et décode la chaine JSON contenant les tâches.
$sDbTasks = file_get_contents('db.txt');
$arrTasks = json_decode($sDbTasks);

// Si l'action est un ajout…
if($_POST['type'] == 'add'){
// … on ajoute l'élément aux tableaux des tâches.
$arrTasks[] = htmlentities($_POST['item'], ENT_QUOTES);
$arrResult['result'] = true;
}

// Si l'action est une suppression…
elseif($_POST['type'] == 'del'){
// … et qu'il existe dans le tableau, on le supprime
if(is_numeric($_POST['pos'])){
if(isset($arrTasks[$_POST['pos']])){
array_splice($arrTasks, $_POST['pos'], 1);
$arrResult['result'] = true;
} else {
$arrResult['result'] = false;
}
} else {
$arrResult['result'] = false;
}
}

// Si l'action est un listing…
elseif($_POST['type'] == 'list'){
$arrResult['result'] = true;
$arrResult['data'] = $arrTasks;
}

// On encode le tableau des tâches dans une chaine JSON et on sauvegarde.
$sDbTasks = json_encode($arrTasks);
file_put_contents('db.txt', $sDbTasks);
}

echo json_encode($arrResult);
?>

III - La gestion du cache

III-1 - Théorie

Le mode hors-ligne intervient quand votre ordinateur n'a plus de réseau ou que votre téléphone se situe dans une zone sans réseau. À ce moment-là, votre application Web utilise le cache du navigateur pour stocker les fichiers dont il aura besoin pour travailler (images, JavaScript, feuilles de style et pages HTML). Ces fichiers seront déclarés dans un fichier de type MANIFEST lié à votre fichier HTML via la balise HTML comme ci-dessous :

index.html

<html manifest="filename.manifest">

Pour que ce fichier soit bien déclaré au niveau du serveur, il nous faut déclarer son MIME-TYPE via un fichier .htaccess :

.htaccess

AddType text/cache-manifest manifest

Après que le fichier a bien été déclaré au niveau du HTML et du serveur, nous allons voir comment il est structuré :

manifest.manifest

CACHE MANIFEST
# Comment

CACHE:
delete.png
index.html
script.js
style.css
# Fichier JS : jQuery
https://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js

FALLBACK:

NETWORK:
/ajax.php

Ce fichier MANIFEST est divisé en quatre parties :

- l'en-tête qui doit être déclaré comme tel : CACHE MANIFEST ;

- la zone de CACHE contenant les fichiers statiques à mettre dans le cache du navigateur ;

- la zone de FALLBACK contenant la redirection à mettre en place pour le cas où ils ne seraient pas accessibles en ligne ;

- la zone de NETWORK contenant les fichiers nécessitant une connexion internet (par exemple, les pages de connexion ou les fichiers PHP liés à de l'AJAX).

Comme vous pouvez le voir, un commentaire peut être placé à n'importe quel endroit du fichier si un dièse est placé au début de la ligne.

III-2 - Pratique

Après la théorie, la pratique. Nous mettons donc en place le fichier manifest, le .htaccess et la modification de la balise HTML. En visitant la page sous Firefox, un message s'affiche dans le haut du navigateur (figure 3.2.1).

Figure 3.2.1 : Mozilla Firefox propose le téléchargement du cache

Après autorisation, on active le travail hors connexion de Mozilla Firefox.
Actualisons la page.
Résultat : la page HTML et les contenus statiques sont là.
Maintenant, testons un ajout et une suppression de tâche : cela fonctionne.

IV - Le mode hors-ligne

IV-1 - Test

On active le travail hors connexion de Mozilla Firefox. Comme on peut le voir dans la figure IV.1.1, la requête AJAX vers le fichier ajax.php a échoué.

Figure IV.1.1 : Échec de la requête AJAX

IV-2 - Théorie

Dans ce cas, nous devons détecter quand il n'y a plus de réseau pour empêcher les requêtes AJAX, et quand le réseau est de nouveau disponible, permettre les requêtes vers le serveur. Pour cela, on utilise la propriété onLine de l'objet window.navigator (référence : https://developer.mozilla.org/en-US/docs/Web/API/window.navigator.onLine) qui retourne un booléen contenant le statut en ligne du navigateur. De plus, pour voir les changements réseaux, deux événements existent : window.onOnline et window.onOffline.

IV-3 - Pratique

Du côté pratique, il suffit d'interroger la variable navigator.onLine qui contient le statut du navigateur et de suivre via callbacks les deux événements window.onOnline et window.onOffline.

script.js

$(document).ready(function(){
var jqoTaskLi = $('<li><span class="lblTask"></span><img src="delete.png" alt="Supprimer" class="delTask"/></li>');

// Lister les tâches à l'initialisation de la page
$.ajax({
url: 'ajax.php',
dataType: 'json',
type: 'POST',
data:{
type:'list'
},
success: function(data) {
if(data.result == true){
$.each(data.data, function(key, value) {
var jqoItem = jqoTaskLi.clone();
jqoItem.find('.lblTask').html(value);
$('ul').append(jqoItem);
});
}
}
});
// Événements lors de la mise on/offline du navigateur
$(document).on('online', function() {
});
$(document).on('offline', function() {
alert('Vous êtes maintenant hors-ligne. Vos tâches ne seront pas enregistrées');
});

// Événement lors de la soumission du formulaire
$('#frmTodo').submit(function(){
// On récupère le texte nettoyé des espaces avant et après
var sVal = $('#txtItem').val();
var sVal = $.trim(sVal);
// Si on est en ligne
if(navigator.onLine){
if(sVal != ''){
// On envoie une requête AJAX de type POST pour ajouter la tâche
$.ajax({
url: 'ajax.php',
dataType: 'json',
type: 'POST',
data:{
type:'add',
item:sVal
},
success: function(data) {
if(data.result == true){
var jqoItem = jqoTaskLi.clone();
jqoItem.find('.lblTask').html(sVal);
$('ul').append(jqoItem);
$('#txtItem').val('');
}
}
});

}
}
// Si on n'est pas en ligne
else {
alert('Réseau : Offline');
}
return false;
});
$('#ulTodo').on('click', '.delTask', function(){
var jqoParent = $(this).parent();
// Si on est en ligne
if(navigator.onLine == true){
// On envoie une requête AJAX de type POST pour supprimer la tâche
$.ajax({
url: 'ajax.php',
dataType: 'json',
type: 'POST',
data:{
type:'del',
pos:$('li').index(jqoParent)
},
success: function(data) {
if(data.result == true){
jqoParent.remove();
}
}
});
}
// Si on n'est pas en ligne
else {
alert('Réseau : Offline');
}
});
});

V - Le stockage local

À ce niveau, les tâches ne sont envoyées uniquement que dans le cas où le navigateur est en ligne. Si le navigateur est hors-ligne, un simple message d'erreur nous prévient. Mais comme la technologie HTML5 est bien conçue : nous allons pouvoir stocker les actions via la technologie du stockage local, plus communément nommé localStorage.

V-1 - Théorie

Le stockage local permet de stocker facilement des informations de type clé-valeur. L'objet localStorage contient six méthodes :

- localStorage.clear() : supprime toutes les clés stockées ;

- localStorage.getItem(sKey) : retourne la valeur associée à une clé ;

- localStorage.key(iIndex) : retourne la clé d'un index ;

- localStorage.length : retourne le nombre de clés stockées ;

- localStorage.removeItem(sKey) : supprime une clé et sa valeur associée ;

- localStorage.setItem(sKey, sValue) : stocke une clé avec une valeur associée.

V-2 - Pratique

Dans notre projet, nous allons avoir besoin du stockage local à deux moments :

- en cas de navigateur hors-ligne pour stocker les actions (ajout et suppression) ;

- en cas de retour au mode en ligne pour lister et exécuter les actions stockées précédemment.

script.js

jQuery(function($){
var jqoTaskLi = $('<li><span class="lblTask"></span><img src="delete.png" alt="Supprimer" class="delTask"/></li>');

// Lister les tâches à l'initialisation de la page
$.ajax({
url: 'ajax.php',
dataType: 'json',
type: 'POST',
data:{
type:'list'
},
success: function(data) {
if(data.result == true){
$.each(data.data, function(key, value) {
var jqoItem = jqoTaskLi.clone();
jqoItem.find('.lblTask').html(value);
$('ul').append(jqoItem);
});
}
}
});

// Événements lors de la mise on/offline du navigateur
$(document).on('online', function() {
if(localStorage.getItem('numActions') == null){
localStorage.setItem('numActions', 0);
}
var numActions = localStorage.getItem('numActions');
numActions = parseInt(numActions);
if(numActions > 0){
// On parse toutes les actions
for (iInc = 0; iInc < numActions; iInc++) {
var sAction = localStorage.getItem('action'+iInc);
// On dirige en fonction des actions
if(sAction == 'add'){
// On envoie la requête AJAX d'ajout
$.ajax({
url: 'ajax.php',
dataType: 'json',
type: 'POST',
data:{
type:'add',
item:localStorage.getItem('item'+iInc)
},
success: function(data) {
if(data.result == false){
alert('Error')
}
}
});
}
else if(sAction == 'del'){
// On envoie la requête AJAX de suppression
$.ajax({
url: 'ajax.php',
dataType: 'json',
type: 'POST',
data:{
type:'del',
pos:localStorage.getItem('pos'+iInc)
},
success: function(data) {
if(data.result == false){
alert('Error')
}
}
});
}
}
// On nettoie toutes les actions
localStorage.clear();
}
});
$(document).on('offline', function() {
localStorage.setItem('numActions', 0);
});

// Événement lors de la soumission du formulaire
$('#frmTodo').submit(function(){
var sVal = $('#txtItem').val();
var sVal = $.trim(sVal);

// Si on est en ligne
if(navigator.onLine){
// On récupère le texte nettoyé des espaces avant et après
if(sVal != ''){
// On envoie une requête AJAX de type POST pour ajouter la tâche
$.ajax({
url: 'ajax.php',
dataType: 'json',
type: 'POST',
data:{
type:'add',
item:sVal
},
success: function(data) {
if(data.result == true){
var jqoItem = jqoTaskLi.clone();
jqoItem.find('.lblTask').html(sVal);
$('ul').append(jqoItem);
$('#txtItem').val('');
}
}
});
}
}
// Si on n'est pas en ligne
else {
if(localStorage.getItem('numActions') == null){
localStorage.setItem('numActions', 0);
}
var numActions = localStorage.getItem('numActions');
numActions = parseInt(numActions);
// On enregistre en local
localStorage.setItem('action'+numActions, 'add');
localStorage.setItem('item'+numActions, sVal);
localStorage.setItem('numActions', numActions + 1);
// On exécute l'action comme si on était en ligne
var jqoItem = jqoTaskLi.clone();
jqoItem.find('.lblTask').html(sVal);
$('ul').append(jqoItem);
$('#txtItem').val('');
}
return false;
});
$('#ulTodo').on('click', '.delTask', function(){
var jqoParent = $(this).parent();
// Si on est en ligne
if(navigator.onLine == true){
// On envoie une requête AJAX de type POST pour supprimer la tâche
$.ajax({
url: 'ajax.php',
dataType: 'json',
type: 'POST',
data:{
type:'del',
pos:$('li').index(jqoParent)
},
success: function(data) {
if(data.result == true){
jqoParent.remove();
}
}
});
}
// Si on n'est pas en ligne
else {
if(localStorage.getItem('numActions') == null){
localStorage.setItem('numActions', 0);
}
var numActions = localStorage.getItem('numActions');
numActions = parseInt(numActions);
// On enregistre en local
localStorage.setItem('action'+numActions, 'del');
localStorage.setItem('pos'+numActions, $('li').index(jqoParent));
localStorage.setItem('numActions', numActions + 1);
// On exécute l'action comme si on était en ligne
jqoParent.remove();
}
});
});

VI - Conclusion

Et voilà, vous savez désormais gérer la déconnexion réseau, le cache de votre application Web et même du stockage local. Ce code est améliorable sur de nombreux points dont voici quelques pistes :

- sécuriser la gestion des erreurs avec par exemple, la protection des failles XSS (essayer d'ajouter la tâche <script>alert('Test');</script>) ;

- créer une image qui sera modifiée à chaque changement de statut réseau.

Vous pouvez retrouver toutes les sources sur mon compte GitHub dédié aux articles Développez.com : https://github.com/Progi1984/DVP

VII - Remerciements

Merci à Bovino et LittleWhite pour la relecture technique.

Merci à Torgar et _Max_pour la relecture orthographique.

Merci à Developpez.com pour leur infrastructure et leur aide dans l'élaboration de mes articles.