Inclure JQuery 2.x pour les fureteurs récents et 1.x pour les récalcitrants

L’équipe derrière JQuery a pris la décision de laisser tomber le support pour Internet Explorer 6, 7 et 8 dans la branche 2.x.

Pour l’explication officielle, veuillez consulter jQuery Core: Version 1.9 and Beyond. En voici un extrait:

There’s just one thing interfering with our vision of the future, and that’s the ghost of browsers past. Internet Explorer 6, 7, and 8–collectively, oldIE–have been a thorn in the side of web developers for a decade.

L’API de la branche 2.x est compatible avec l’API de la branche JQuery 1.9+ qui supporte toujours les vielles versions.

Our goal is for 1.9 and 2.0 to be interchangeable as far as the API set they support. When 2.0 comes out, your decision on which version to choose should be as simple as this: If you need IE 6/7/8 support, choose 1.9; otherwise you can use either 1.9 or 2.0.

Cependant, pour y arriver, cette branche doit faire plusieurs pirouettes et tours de magie et cela complique le code et rend plus difficile son évolution. Ou encore mieux formulé (par l’équipe de JQuery):

If jQuery 1.9 and 2.0 are basically the same API, what makes 2.0 compelling?

Smaller size, better performance, and the lack of problems introduced by the need for oldIE support. We expect that we can improve error handling in the $.Deferred implementation in 2.0, for example, whereas we can’t do that as long as oldIE is supported.

En tant que développeur, on veut souvent profiter des dernières améliorations nous permettant d’écrire du code concis, efficace et avec le moins de défaut possible. On veut aussi minimiser le nombre de tests à effectuer. Tester un site pour IE 11, IE 10, IE 9 et Chrome est déjà beaucoup. Si en plus il faut tester avec IE 8, IE7 et IE6, cela rend les choses encore plus compliquées.

Même les grosses compagnies avec de gros comptes de banque et beaucoup d’employés préfèrent supporter les fureteurs récents. Par exemple, Google a abandonné le support pour IE9 quelque temps après la sortie de IE 11:

Google Drops Support for IE9

Alors si vous souhaitez travailler avec les dernières versions des fureteurs et la dernière version de JQuery lors du développement mais garder une porte ouverte pour supporter les vieux fureteurs si on client l’exige, voici un petite recette.

Recette pour inclure automatiquement la dernière version de JQuery dans un application ASP.Net MVC

  1. Ajoutez la dernière version de JQuery 2.x à votre projet ainsi que la dernière version de JQuery 1.ximage
  2. Dans la classe BundleConfig, ajoutez deux bundles:
    public static void RegisterBundles(BundleCollection bundles)
    {
    bundles.Add(new ScriptBundle(« ~/bundles/jquery1 »).Include(« ~/Scripts/jquery-1* »));
    bundles.Add(new ScriptBundle(« ~/bundles/jquery2 »).Include(« ~/Scripts/jquery-2* »));
    (…)
  3. Définissez une vue partielle nommée _JQueryInclude.cshtml et contenant les 5 lignes suivantes:<!– Conditionally include jQuery version based on IE version –>
    <!–[if lt IE 9]>
    @Scripts.Render(« ~/bundles/jquery1 ») <![endif]–>
    <!–[if gte IE 9]><!–>
    @Scripts.Render(« ~/bundles/jquery2 ») <!–<![endif]—>
  4. Dans votre layout page ou à l’endroit où vous désirez inclure JQuery, utilisez:@Html.Partial(« _JQueryInclude »)

Et voilà! Si vos utilisateurs utilisent IE 6, IE 7 ou IE 8, les pages HTML utiliseront la branche 1.x de JQuery. Autrement, la version moderne de la branche 2.x sera utilisée.

Caractères accentués, ASP.Net MVC Razor et boutons de dialogue JQuery UI

Développeurs MVC francophones: avez-vous déjà eu des problèmes avec des caractères accentués dans les boutons d’un dialogue JQuery UI?

Afin de développer l’application Web en anglais et français, les libellés des boutons sont stockés dans des fichiers de ressources (*.resx).

Tout fonctionnait avec des boutons comme Sauvegarder, Annuler, OK etc. Mais tout à coup, la technique habituelle échoue avec un bouton Générer …

image

Et s’il n’y avait pas de guillement pour la clé (Gén&/233;rer: function() { … ), c’est pire car cela fera planter votre script.

La solution est d’utiliser la méthode @Html.Raw et d’entourer le résultat avec des guillements:

$(« #dlgGenerator »).dialog({
  autoOpen: false,
  width: 800,
  height: 600,
  resizable: false,
  modal: true,
  open: function(event, ui) {
    // Initialization
  },
  buttons: {
    « @Html.Raw(Global.LblGenerate) »: function() {
      $(« #cmdGenerateAssignments »).click();
    },
    « @Html.Raw(Global.LblCancel) »: function() {
      $(this).dialog(« close »);
    }
  }
});

image

Il est peut-être sage de vous protéger tout de suite d’un changement de libellé éventuel! L’utilisation de Html.Raw vous protège des caractères accentués tandis que l’utilisation des guillemets empêchera les erreurs de script dûes à la présence d’un espace ou caractère illégal.

À la prochaine fois,

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