WebDev-FR

Archive du Mercredi 05/11/2008

Eviter à coup sûr les problèmes de cache sur les CSS et les JS

par Loïc le Mercredi 05/11/2008 à 00:22, dans CSS, JavaScript, PHP

Une excellente méthode proposée par Le Potlatch pour éviter les problèmes de cache chez les utilisateurs lorsque les JS ou les CSS sont modifiés : plutôt que de versionner les fichiers et de devoir mettre à jour la version à chaque fois qu’on modifie le fichier (et modifier les appels aux fichiers), le Potlatch propose plutôt de mettre un paramêtre GET dans l’url du fichier, par exemple monfichier.css?param=valeur.

Et à votre avis, quelle est la valeur la plus adaptée ? La date de dernière modification bien sûr ! On peut la récupérer en php via la méthode filemtime().

Autrement dit, lorsqu’on inclus un CSS ou un JS dans une page, on ne met plus monFichier.css comme nom de fichier mais monFichier.css<?php echo filemtime("monFichier.css"); ?>

Laissez un commentaire :, , , plus...

Testeur d’expressions régulières en JavaScript

par Loïc le Mercredi 05/11/2008 à 00:12, dans JavaScript

Une petite appli web bien pratique permettant de tester ses expressions régulières : ça se passe ici.

Laissez un commentaire :, , , plus...

Le micro-templating JavaScript de John Resig à la loupe

par Loïc le Mercredi 05/11/2008 à 00:06, dans JavaScript, Tutoriel

Le templating JavaScript est une technique récente dont l’objectif est l’application du principe de templates dans une page web en les gérant via le JavaScript.

Cette technique trouve tout particulièrement un grand intérêt dans le cadre de communication asynchrones (XML ou JSON) avec la création de toute une partie du DOM à la réception de la réponse.

Le principe est relativement simple : on prépare une structure HTML contenant des mots-clef aux endroits où devront se trouver les données récupérées de façon asynchrones. A la réponse des données en question, le système de templating JavaScript se charge de récupérer le contenu de ce HTML sous forme d’une chaîne de texte et de remplacer les mots-clef par les données adéquates, puis de réinjecter ce nouveau contenu à l’endroit voulu dans le DOM de la page.

Les solutions de templating JavaScript sont encore rares et il est encore plus difficile d’en trouver des efficaces donnant une grande latitude lors de la création d’un template. En juillet dernier, John Resig a publié un script de templating sur son blog offrant la possibilité d’utiliser n’importe quelle fonctionnalité JavaScript dans le template. Etant donné que son code est assez obscur, je me propose de le décortiquer avec vous pour en comprendre le fonctionnement.

Pour décortiquer ce script, nous allons utiliser un exemple. Supposons que nous récupérions en JSON asynchrone (ce sera réalisé avec jQuery) un bloc de données caractérisant un groupe de personnes. Ce groupe est caractérisé par un nom et chaque personne est caractérisée par un ID, une image, un nom et un prénom.

Notre template aura la forme suivante :

<script type="text/html" id="groupTemplate">
   <h3><%= name %></h3>
   <ul id="users-list">
      <% for (i = 0; i < users.length; i++) { %>
         <li id="<%= users[i].ID %>">
            <img src="<%= users[i].photo %>"/>
            <span class="firstname"><%= users[i].firstname %></span>
            <span class="name"><%= users[i].name %></span>
         </li>
      <% } %>
   </ul>
</script>

Le template est encapsulé dans un script de type “text/html”. C’est un type MIME inexistant et nous aurions pu mettre n’importe quel type, par exemple “template/html”. L’essentiel est de mettre un type que le navigateur ne comprendra pas et donc ne cherchera pas à traiter.

Notre code JavaScript sera le suivant :

$(document).ready(function() {
   $.getJSON("http://webdev-fr.com/examples/templating-js/json.php", null, function(data) {
      var mygroup = document.getElementById("my-group");
      mygroup.innerHTML = tmpl("groupTemplate", data);
   });
});

Le fonctionnement est le suivant : au chargement de la page, on fait un appel asynchrone pour récupérer du code JSON. Ce code est passé au à la méthode javascript de templating à laquelle on passe également l’ID de notre template. Le résultat du templating est inséré dans un DIV ayant pour ID “my-group”.

La structure renvoyée par l’appel JSON est la suivante :

{
   name:"Mon super groupe",
   users:[
      {ID:"u0",firstname:"firstname1",name:"name1",photo:"http://webdev-fr.com/examples/templating-js/img/photo.gif"},
      {ID:"u1",firstname:"firstname2",name:"name2",photo:"http://webdev-fr.com/examples/templating-js/img/photo.gif"},
      {ID:"u2",firstname:"firstname3",name:"name3",photo:"http://webdev-fr.com/examples/templating-js/img/photo.gif"}
   ]
}

Voir l’exemple.

Bien, ça semble marcher correctement, décortiquons un peu ce qui se passe derrière la fonction tmpl() de John Resig, qui se trouve dans le fichier templating.js

Le code de John Resig est le suivant :

// Simple JavaScript Templating
// John Resig - http://ejohn.org/ - MIT Licensed
(function(){
  var cache = {};

  this.tmpl = function tmpl(str, data){
    // Figure out if we're getting a template, or if we need to
    // load the template - and be sure to cache the result.
    var fn = !/\W/.test(str) ?
      cache[str] = cache[str] ||
        tmpl(document.getElementById(str).innerHTML) :

      // Generate a reusable function that will serve as a template
      // generator (and which will be cached).
      new Function("obj",
        "var p=[],print=function(){p.push.apply(p,arguments);};" +

        // Introduce the data as local variables using with(){}
        "with(obj){p.push('" +

        // Convert the template into pure JavaScript
        str
          .replace(/[\r\t\n]/g, " ")
          .split("<%").join("\t")
          .replace(/((^|%>)[^\t]*)'/g, "$1\r")
          .replace(/\t=(.*?)%>/g, "',$1,'")
          .split("\t").join("');")
          .split("%>").join("p.push('")
          .split("\r").join("\\'")
      + "');}return p.join('');");

    // Provide some basic currying to the user
    return data ? fn( data ) : fn;
  };
})();

Vu qu’il n’est pas forcément évident de comprendre le fonctionnement de ce code, je me suis permis de le récrire dans un second fichier, templating2.js :

(function(){
   var cache = {};

   this.tmpl = function tmpl(str, data) {
      if (!/\W/.test(str)) {

         if (cache[str]) {
            var fn = cache[str];
         } else {
            var fn = tmpl(document.getElementById(str).innerHTML);
         }

      } else {
         var functionCode = "";
         functionCode += "var p = [],";
         functionCode += "print = function() {";
         functionCode += "   p.push.apply(p, arguments);";
         functionCode += "};";

         functionCode += "with(obj) {";
         functionCode += "   p.push('";

         var jsCode = str;
         jsCode = jsCode.replace(/[\r\t\n]/g, " ");
         jsCode = jsCode.split("<%");
         jsCode = jsCode.join("\t");
         jsCode = jsCode.replace(/((^|%>)[^\t]*)'/g, "$1\r");
         jsCode = jsCode.replace(/\t=(.*?)%>/g, "',$1,'");
         jsCode = jsCode.split("\t");
         jsCode = jsCode.join("');");
         jsCode = jsCode.split("%>");
         jsCode = jsCode.join("p.push('");
         jsCode = jsCode.split("\r");
         jsCode = jsCode.join("\\'");

         functionCode += jsCode + "');";
         functionCode += "}";
         functionCode += "return p.join('');";

         var fn = new Function("obj", functionCode);
      }

      if (data) {
         return fn(data);
      } else {
         return fn;
      }
   };
})();

Je n’ai pas changé grand chose en fait, j’ai surtout supprimé l’utilisation d’opérateurs ternaires pour les remplacer par des if classiques et j’ai modifié la grosse suite de regexp afin de rendre le pas à pas plus facile à expliquer.

Ce nouveau script fonctionne parfaitement, on peut le tester sur notre précédent exemple en ramplaçant le script original par le nouveau.

Comme vous avez pu le constater plus haut, la méthode tmpl() prend deux paramètres :

  • le premier paramètre peut être l’ID d’un template ou bien directement une chaîne de caractères correspondant au template au question. Passer un ID a plus d’intérêt car cela permet de mettre en cache la fonction de transformation correspondant à ce template. Néanmoins, si le template n’est utilisé qu’une seule fois ou bien s’il est dynamique, il peut être intéressant de le passer directement à la fonction.
  • le second paramètre est optionnel. C’est l’objet qui correspond aux données qui vont être templatées. Si cet objet est passé en paramètre alors tmpl() retourne un code HTML (qu’on injecte dans notre conteneur résultat via sa propriété innerHTML). Si ce paramètre est absent, la méthode tmpl() retourne la fonction de transformation correspondant à notre template, cette fonction peut ensuite être utilisée en lui passant un objet. Ca peut avoir de l’intérêt si on compte passer plusieurs objets de données à la suite dans un même template : on récupère la fonction de transformation et on l’applique sur les objets en question dans une boucle.

La regexp /\W/.test(str) a pour rôle de tester si le premier paramètre passé à la fonction est un ID ou bien un template sous forme de ligne de texte. Pour rappel, \W correspond à n’importe quel caractère non-alphanumérique ou underscode. Autrement dit, tout sauf “a-z”,”A-Z” et “_”.

En conséquent, le test peut se lire de la façon suivante :

if (!/\W/.test(str)) {
   // str ne possède pas de caractère non-alphanumérique, c'est donc un ID
} else {
   // str possède un caractère non-alphanumérique, c'est donc un template sous forme de
   // chaîne de caractères
}

Si le premier paramètre est un ID (on est alors dans le premier cas du test), alors la méthode est rappelée de façon récursive en lui passant, cette fois-ci, le contenu de la balise dont l’ID correspond au paramètre, autrement dit le contenu de notre template. Le résultat de ce second appel est placé en cache, ce qui permet par la suite de ne pas effectuer le second appel récursif mais de retourner directement le contenu mis en cache.

Le second appel récursif va retourner une fonction de transformation, c’est la raison pour laquelle l’objet de données n’est pas passé en paramètre sur ce second appel.

A la fin de la fonction tmpl(), un test est effectué pour savoir si le deuxième argument a été fourni ou non. Si c’est le cas, on applique la fonction de transformation sur ce second argument, sinon on retourne simplement la fonction de transformation.

if (data) {
   return fn(data);
} else {
   return fn;
}

En résumé, nous avons 4 cas d’appel pour notre fonction tmpl() :

le premier argument est un ID, le second argument est fourni
=> on effectue un appel récursif en lui passant le innerHTML correspondant à l’ID, on obtient ainsi une fonction de transformation qu’on applique au second paramètre afin d’obtenir un code HTML qu’on injecte dans notre conteneur résultat.
le premier argument est un template sous forme de chaîne de caractères, le second argument est fourni
=> pas d’appel récursif effectué, on obtient directement une fonction de transformation à partir de notre template en paramètre et on applique cette fonction au second arguement pour récupérer le code HTML qu’on injecte dans le conteneur résultat.
le premier argument est un ID, pas de second argument
=> on effectue un appel récursif en lui passant le innerHTML correspondant à notre ID, on obtient une fonction de transformation qu’on retourne
le premier argument est un template sous forme de chaîne de caractères, pas de second argument
=> on n’effectue pas d’appel récursif, on génère une fonction de transformation à partir de notre template et on retourne cette fonction (c’est ce cas qui se produit dans le cas d’un appel récursif).

Bien, maintenant qu’on sait comment fonctionne l’algorithme, il est temps de nous pencher sur la fonction de transformation et de sa batterie de regexp…

Le plus simple est de nous baser sur notre cas concret en utilisant notre template. L’objectif de la fonction de transformation est de retourner une fonction de la forme suivante :

function fn(data) {
   var str = '';
   str += '<h3>' + data.name + '</h3>';
   str += '<ul id="users-list'>;

   for (i = 0; i < data.users.length; i++) {
      str += '<li id="' + data.users[i].ID + '">';
      str += '<img src="' + data.users[i].photo + '"/>';
      str += '<span class="firstname">' + data.users[i].firstname + '</span>';
      str += '<span class="name">' + data.users[i].name + '</span>';
      str += '</li>';
   }
   str += '</ul>';
}

Pour arriver à ce résultat, le système de templating utilise quelques subtilités :

  • la création d’une fonction via le constructeur Fonction() pour avoir un code dynamique (qui dépend du template). Ce constructeur prend N paramètres : les premiers paramètres correspondent aux paramètres de la fonction, le dernier paramètre correspond au code de la fonction. Par exemple :
    var displayAlert = new Function("msg", "alert(msg); return false;");
    displayAllert();
    

    Le code précédent est équivalent à :

    var displayAlert = function(msg) {
       alert(msg);
       return false;
    }
    displayAlert();
    
  • l’utilisation du mot clef with. Par exemple le code suivant :
    with(data) {
       field1 = "val1";
       field2 = "val2";
    }
    

    est équivalent au code suivant :

    data.field1 = "val1";
    data.field2 = "val2";
    

    Ce mécanisme a l’avantage de nous éviter de devoir utiliser le même nom d’objet dans notre fonction de transformation et dans notre template.

  • l’utilisation intelligente des regexp afin de découper le template en fonction des balises spécifiques au template (<% … %>). Nous les décortiquerons plus loin.

Plus exactement, la fonction de transformation visée aura le code suivant :

function fn(obj) {
   var p = [],
   print = function() {
      p.push.apply(p, arguments);
   };

   with(obj) {
      p.push('<h3>',
             name,
             '</h3><ul id="users-list">');
      for (i = 0; i < users.length; i++) {
         p.push('<li id="',
                users[i].ID,
                '"><img src="',
                users[i].photo,
                '"/><span class="firstname">',
                users[i].firstname,
                '</span><span class="name">'
                 users[i].name,
                '</span></li>');
      }
      p.push('</ul>');
      return p.join('');
   }
}

Etudions les regexp appliquées une à une, c’est la partie vraiment indigeste du code…

Avant la première regexp, notre variable jsCode a la valeur de str, autrement dit :

<h3><%= name %></h3>
   <ul id="users-list">
      <% for (i = 0; i < users.length; i++) { %>
         <li id="<%= users[i].ID %>">
            <img src="<%= users[i].photo %>"/>
            <span class="firstname"><%= users[i].firstname %></span>
            <span class="name"><%= users[i].name %></span>
         </li>
      <% } %>
   </ul>

La première manipulation est la suivante : jsCode = jsCode.replace(/[\r\t\n]/g, ” “);, autrement dit elle remplace tous les retours chariot (\r), les tabulations (\t) et les retours à la ligne (\n) par des espaces.

Notre variable jsCode aura donc la valeur suivante (je supprime les espaces superflus) :

<h3><%=name%></h3><ul id="users-list"><%for(i=0;i<users.length;i++){%><li id="
<%=users[i].ID%>"><img src="<%=users[i].photo%>"/><span class="firstname">
<%=users[i].firstname%></span><span class="name"><%=users[i].name%></span></li><%}
%></ul>

La seconde regexp fait une découpe sur le séparateur “<%” (le séparateur n’est pas conservé) afin de récupérer le tableau suivant :

jsCode = [
   '<h3>',
   '=name%></h3><ul id="users-list">',
   'for(i=0;i<users.length;i++){%><li id="',
   '=users[i].ID%>"><img src="',
   '=users[i].photo%>"/><span class="firstname">',
   '=users[i].firstname%></span><span class="name">',
   '=users[i].name%></span></li>',
   '}%></ul>'
]

La commande suivante est l’inverse du split : un join pour réassembler le tableau en une chaîne de caractère en insérant des tabulation entre ses éléments.

   jsCode = '<h3>    =name%></h3><ul id="users-list">    for(i=0;i<users.length;i++){%><li id="
    =users[i].ID%>"><img src="    =users[i].photo%>"/><span class="firstname">
    =users[i].firstname%></span><span class="name">    =users[i].name%></span></li>
    }%></ul>'

La regexp qui suit est un poil plus compliquée : jsCode = jsCode.replace(/((^|%>)[^\t]*)’/g, “$1\r”);. Disons simplement qu’elle va rajouter des \r là où cela s’avère nécessaire.

La regexp qui suit (jsCode = jsCode.replace(/\t=(.*?)%>/g, "',$1,'"); va remplacer les =champs%> par simplement "', champs, '".

Ensuite on effectue de nouveau un split de notre chaîne, cette fois-ci sur les \t :

jsCode = [
   "<h3>', name ,'</h3><ul id="users-list">",
   "for(i=0;i<users.length;i++){%><li id="',users[i].ID,'"><img src="',users[i].photo,'"/>
    <span class="firstname">',users[i].firstname,'</span><span class="name">',users[i].name,'</span></li>",
   "}%></ul>"
]

Le join("');") suivant va permettre de fermer les p.push() pour la fonction.

"<h3>', name ,'</h3><ul id="users-list">"');" for (i = 0; i < users.length; i++) { %><li id="', users[i].ID ,'">
 <img src="', users[i].photo ,'"/><span class="firstname">', users[i].firstname ,'</span>
 <span class="name">', users[i].name ,'</span></li>"');\" } %></ul>"

Un nouveau split sur "%>" :

jsCode = [
   "<h3>', name ,'</h3><ul id="users-list">"');" for (i = 0; i < users.length; i++) { ",
   "<li id="',users[i].ID,'"><img src="',users[i].photo,'"/><span class="firstname">',users[i].firstname,'</span>
    <span class="name">',users[i].name,'</span></li>"');"}",
   "</ul>"
]

On enchaîne avec un nouveau join(), cette fois-ci pour insérer les p.push() :

"<h3>',name,'</h3><ul id="users-list">"');" for (i = 0; i < users.length; i++) { "p.push('"
 <li id="',users[i].ID,'"><img src="',users[i].photo,'"/><span class="firstname">',users[i].firstname,
 '</span><span class="name">',users[i].name,'</span></li>"');"}"p.push('"</ul>"

Oui, se perd facilement dans tous ces split() et ces join() successifs. On tient néanmoins le bon bout, on enchaîne avec un nouveau split(), sur \r cette fois, mais celle-ci n’a aucun effet dans notre cas.

Enfin, on termine par un join() avec "\\'" :

"<h3>',name,'</h3><ul id="users-list">"');"for(i=0;i<users.length;i++){"p.push('"<li id="',users[i].ID,'">
 <img src="',users[i].photo,'"/><span class="firstname">',users[i].firstname,'</span><span class="name">', users[i].name ,'</span>
 </li>"');"}"p.push('"</ul>"

Pour finir, on insère tout ça dans le début et la fin de notre fonction de transformation, pour obtenir en définitive le code suivant pour la fonction :

"var p = [],print = function() { p.push.apply(p, arguments);};with(obj) { p.push('<h3>', name ,'</h3><ul id="users-list">');
 for (i = 0; i < users.length; i++) { p.push('<li id="',users[i].ID,'"><img src="',users[i].photo,'"/>
 <span class="firstname">',users[i].firstname,'</span><span class="name">',users[i].name,'</span></li>');}p.push('</ul>');}
 return p.join('');"

On obtient bien notre fonction de transformation…

6 Commentaires :, , , , plus...

Vous cherchez quelque chose de particulier ?

Utilisez le formulaire ci-dessous pour chercher sur le site :

Vous ne trouvez toujours pas ce que vous cherchez ? Laissez un commentaire sur un billet ou bien contactez-moi et nous tâcherons de trouver ensemble !

Ils peuvent également vous intéresser...

Quelques blogs très intéressants que j'essaye de suivre...

Archives

Tous les billets, classés de manière chronologique...