Quelques trucs lors du développement Web en mode déconnecté (offline)

Mon monde a récemment été chamboulé en réalisant une preuve de concept pour une application Web devant fonctionner en mode déconnecté.

Pour moi, le concept d’une application Web déconnectée présentait plusieurs problèmes:

  1. Fonctionnement sans serveur Web, installation et mise à jour;
  2. Stockage local des données;
  3. Possibilité de détecter si une connexion réseau est disponible ou non afin de désactiver des fonctions dans l’application;
  4. Authentification à travers un serveur ISA pour les appels AJAX;

Et bien le Web a changé et vous devriez vous aussi!

Malheureusement, Internet Explorer a tardé à rejoindre les autres fureteurs pour supporter les applications déconnectées. Mais si vous pouvez livrer une solution avec Chrome, Firefox, Safari, Opera ou Internet Explorer 10 (bientôt disponible pour Windows 7), vous pourrez utiliser les HTML5 Offline Web Applications.

Fonctionnement sans serveur Web, installation et mise à jour

Dès lors que l’on parle d’application Web en mode déconnecté (ou hybride), on parle de HTML5 Offline Web Applications.

C’est ce qui permet à une application Web (ex. http://monserveur/monapplication/mapage.html) de fonctionner dans un fureteur même lorsqu’aucun connexion réseau n’est disponible. La version en cache de cette page (et ses dépendances) est utilisée en mode déconnecté.

Il y a beaucoup de documentation sur le sujet mais en gros, chaque page HTML spécifie un fichier Manifest à utiliser:

<html manifest= »CacheManifest.ashx »>

Dont voici un exemple:

CACHE MANIFEST
# version 1.0
CACHE:
Content/images/ajax-loader.gif
Content/images/datebox.png
Content/images/icons-18-black.png
Content/images/icons-18-white.png
Content/images/icons-36-black.png
Content/images/icons-36-white.png
Content/jquery.mobile-1.2.0.min.css
Content/jqm-datebox-1.1.0.min.css
Scripts/jqm-datebox-1.1.0.comp.calbox.min.js
Scripts/jquery-1.8.2.min.js
Scripts/jquery.mobile-1.2.0.min.js
Scripts/json2.js
NETWORK:
*

Tous le fichiers spécifiés (case sensitive) explicitement sous CACHE: seront téléchargés initialement lors de l’accès à la page HTML et à chaque fois que le fichier Manifest est modifié. Si vous modifiez un fichier dans la liste, vous devez aussi modifier le manifeste afin qu’il retélécharge ce fichier (et vous devez écrire du code dans la page).

Bref, une fois que vous avez accédé à l’application Web, vous pouvez débrancher votre connexion internet et l’application continuera de fonctionner (tant que vous ne tentez pas d’accéder à une ressource qui n’est pas en cache).

Truc #1: Chaque page HTML devrait vérifier si une nouvelle version de l’application est disponible

Une fois le manifeste en cache, le fureteur ne retourne pas sur le serveur Web. Il faut le forcer à vérifier via window.applicationCache.update(). En s’étant précédemment enregistré pour l’événement updateready de l’objet window.applicationCache, on pourra remplacer la version actuelle de l’application dans la cache et demander à l’utilisateur s’il souhaite la charger.

if (window.applicationCache) {
  $(window.applicationCache).bind('updateready', function (e) {
    if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {
      // Browser downloaded a new app cache.
      // Swap it in and reload the page to get the new hotness.
      window.applicationCache.swapCache();

      if (confirm("Une nouvelle version de l'application est disponible. Désirez-vous la charger?")) {
        window.location.reload();
      }
    }
  });

  window.applicationCache.update();
}

Truc #2: Le manifest devrait être retourné par un ASP.Net HttpHandler afin de mettre le bon Content-Type

Le manifeste peut être stocké dans un fichier de l’extension de votre choix (ex. Manifest.txt). L’important c’est qu’il soit retourné avec le Content-Type text/cache-manifest. Pourquoi ne pas créer un HttpHandler via un ASHX:

<%@ WebHandler Language= »C# » CodeBehind= »CacheManifest.ashx.cs » Class= »YouNamespace.CacheManifest » %>

——————–

namespace DisconnectedTest
{
public class CacheManifest : IHttpHandler
{
  public void ProcessRequest(HttpContext pContext)
  {
    pContext.Response.ContentType = "<strong>text/cache-manifest</strong>";
    pContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
    pContext.Response.WriteFile(pContext.Server.MapPath("CacheManifest.txt"));
  }

  public bool IsReusable
  {
    get { return false; }
  }
}
}

Stockage local des données

Il y a plusieurs standards disponibles pour le stockage des données (Web SQL Database, WebSimpleDB/IndexDB et Local Storage). Pour mon besoin, la simplicité et le support uniforme (IE8 le supporte!) pour le HTML5 Local Storage m’ont convaincu de l’utiliser.

image

En gros, il s’agit d’un mécanisme permettant de stocker à peu près 5 MB via des clé / valeurs. Vous pourriez avoir une seule clé ayant comme valeur un objet JSON contenant toutes les données requises par votre application.

Truc #3: Stockez votre modèle en JSON dans localStorage

Toute la logique applicative pourrait fonctionner en utilisant un objet JSON représentant le modèle de l’application. Cet objet JSON peut être persisté dans localStorage à chaque modification. Avec la librairie JSON2, c’est facile de transformer une chaîne de caractères en objet et vice versa.

function getOrCreateModel() {
  var sJSON = window.localStorage["mykey"];
  var pModel = {};

  if (sJSON != undefined &amp;&amp; sJSON.length &gt; 0) {
    pModel = JSON.parse(sJSON, null);
  }

  return pModel;
}

function saveModel(pModel) {
  var sJSON = JSON.stringify(pModel, null);
  window.localStorage["mykey"] = sJSON;
}

Truc #4: Créez l’interface graphique à partir du modèle

L’exemple suivant utilise JQuery et JQuery Mobile afin de créer une liste d’élément dynamiquement à partir du modèle stocké dans localStorage:

function reloadFromModel() {
  var pModel = getOrCreateModel();
  var pPage = $("#main")
  var pDiv = $("#listElements");
  var sHtml = "&lt;ul data-role='listview' data-inset='true'&gt;";

  for (var i = 0; i &lt; pModel.list.length; ++i) {
    sHtml += "&lt;li&gt;&lt;a href='#element?index=" + i + "'&gt;" + pModel.list[i].caption + "&lt;/a&gt;&lt;/li&gt;";
  }

  sHtml += "&lt;/ul&gt;";&lt;/span&gt;

  pDiv.html(sHtml);
  pPage.page();
  pDiv.find(":jqmData(role=listview)").listview();
}

Extrait du HTML:

<div data-role= »page » id= »main » data-theme= »c » data-content-theme= »c »>
<div data-role= »content »>
<h2>Elements:</h2>
  <div id= »listElements »></div>
</div>
</div>

Truc #5: Visualisez ce qui est en cache avec Google Chrome / Firefox

Chrome permet de voir les fichier mis en cache et leur taille. ll permet aussi de voir ce qui est stocké dans localStorage. Il suffit de naviger sur l’application Web puis de choisir Tools –> Developers Tools –> Resources

image

Vous pouvez aussi taper chrome://appcache-internals/ dans la barre d’adresse de Chrome (about:cache pour Firefox).

image

Détecter si une connexion réseau est disponible

La propriété navigator.onLine permet de savoir si une connexion réseau est présentement disponible. Deux événements, online et offline sont disponibles pour l’objet window afin d’être notifié lors d’un changement d’état.

Truc #6: Activez / désactivez des fonctionnalités selon la connectivité disponible

Ayez une fonction permettant d’activer / désactiver des capacités selon l’état actuel:

function enableDisableNetworkButtons() {
  if (navigator.onLine) {
    $("#cmdSubmit").removeClass('ui-disabled');
  } else {
    $("#cmdSubmit").addClass('ui-disabled');
  }
}

Au chargement de votre page, appelez la fonction et enregistrez-vous pour les changements d’état:

enableDisableNetworkButtons();
$(window).bind('offline', enableDisableNetworkButtons);
$(window).bind('online', enableDisableNetworkButtons);

Dans l’événement du bouton, on peut se protéger en vérifiant de nouveau avant d’exécuter l’action qui nécessite une connexion:

function submitModel() {
  if (navigator.onLine) {
    $.ajax({ ...
  } else {
    enableDisableNetworkButtons();
  }
}

Truc #7: Affichez un élément visuel indiquant qu’une activité réseau est en cours

Lorsque vous faites un appel $ajax, l’utilisateur devrait savoir qu’une opération est en cours. JQuery permet d’être notifié lors du démarrage et fin d’une opération AJAX en s’enregistrant aux événements ajaxStart et ajaxStop de l’objet document. JQuery Mobile offre un indicateur visuel via un appel à $.mobile.loading.

$(document).ajaxStart(function () {
$.mobile.loading('show');
});

$(document).ajaxStop(function () {
$.mobile.loading('hide');
});

Authentification à travers un serveur ISA

Dans mon scénario, l’application était publiée sur un serveur Web derrière un serveur ISA. L’authentification Windows (Kerberos) était réalisée par le serveur ISA via un formulaire.

image

Dès que le fureteur demande une ressource sur le serveur Web, le serveur ISA vérifie si on est déjà authentifié et sinon il renvoie un HTTP 302 Redirect vers la page contenant le formualire d’authentification.

Si la session expire et qu’une action AJAX est effectuée par l’application Web, celle-ci va échouer car l’objet XmlHttpRequest va suivre le Redirect et il va échouer.

Truc #8: Définissez une méthode d’un service Web permettant de vérifier si vous êtes toujours authentifié

Le serveur ne fait que retourner true. Mais si votre session a expirée, l’appel AJAX va échouer. L’application ne pourra plus être mise à jour (car le téléchargement du manifeste va échouer) et les fonctionnalités demandant une connexion vont échouer.

[WebMethod]
public bool IsAlive()
{
  return true;
}
function checkIfStillAuthenticated() {
  var bResult = false;

  if (navigator.onLine) {
    $.ajax({
      type: "POST",
      async: false,
      url: "YouWebService.asmx/IsAlive",
      data: null,
      contentType: "application/json; charset=utf-8",
      success: function (data, textStatus, jqXHR) {
        var sContentType = jqXHR.getResponseHeader("Content-Type");
        if (!sContentType || sContentType.toLowerCase().indexOf("text/html") == -1) {
          bResult = true;
        } else {
          forceLogin();
        }
      },
      error: function (jqXHR, textStatus, errorThrown) {
        forceLogin();
      }
    });
}
  return bResult;
}

Vérifiez avant tout appel vers le serveur:

  1. Mise à jour du manifeste
  2. Les fonctionnalités nécessitant une connexion vers le serveur (ex. Submit)
if (checkIfStillAuthenticated()) {
  if (window.applicationCache) {
    window.applicationCache.update();

Truc #9: Forcez la réauthentification

Lorsque la méthode précédente détermine que la session a expirée, il faut aviser l’utilisateur et forcer une réauthentification. Si on ne veut pas coder en dur l’URL de la page permettant l’authentification, on pourrait utiliser la technique suivante.

On ajoute un fichier HTML à notre application Web et ce fichier n’utilise pas le manifeste (et il n’est pas listé dedans). Notre règle NETWORK:* va faire en sorte qu’il est toujours demandé sur le réseau. En fait dans mes tests, le fureteur tente d’utiliser sa cache locale mais j’ai ajouté un GUID dans le lien: ForceLogin.html?GUID afin d’être certain qu’il demande la page au serveur.

ISA va réaliser que la session a expirée et il va demander à l’utilisateur de se réauthentifier. Une fois que c’est fait, il va aller chercher ForceLogin.html sur le serveur:

<!DOCTYPE html>
<html xmlns= »
http://www.w3.org/1999/xhtml »>
<head>
<script type= »text/javascript »>
window.location = « Main.html »;
</script>
</head>
<body>
<h3>Vous allez être redirigé automatiquement ou utilisez ce lien: <a href= »Main.html »></a>.</h3>
</body>
</html>

Cette page ne fait que retourner à Main.html mais au moins on est authentifié.

function guidGenerator() {
  var S4 = function () {
    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
  };
  return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
}

// Alert the user then browse to a page marked as NETWORK (always fetched from the server)
//
function forceLogin() {
  alert("Votre session a expirée, vous devez vous réauthentifier. L'opération n'a pas été effectuée.");
  window.location = "ForceLogin.html?" + guidGenerator();
}

Mot de la fin

Voilà, je pense que certaines de ces techniques peuvent être récupérées directement pour vos projets. Si vous avez des suggestions d’améliorations, n’hésitez pas à laisser un commentaire.

Références

  1. Can I use offline web applications?

  2. The Past, Present & Future of Local Storage for Web Applications

  3. Let’s Take This Offline

  4. Internet Explorer 10 & Offline Web Apps

  5. HTML5 Offline Applications: ‘Donut Hole’ Caching

  6. Working Off the Grid with HTML5 Offline

  7. A Beginner’s Guide to Using the Application Cache

  8. Creating HTML5 Offline Web Applications with ASP.NET

  9. Seeing the Offline Web Application in Action

  10. What is the max size of localStorage values?

  11. Developer’s Guide – Client-side Storage (Web Storage)

  12. JSON in JavaScript

3 réflexions sur “Quelques trucs lors du développement Web en mode déconnecté (offline)

  1. Bonjour,

    Très bien cet article avec de nombreuses références.

    Une petite question :
    Est-il possible d’utiliser SQLite pour mémoriser des données dans ce mode déconnecté ?

    Merci par avance pour votre réponse.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s