Fonction générique de chargement de données JSON avec Javascript. Gestion du cache du client et du mode synchrone et asynchrone

Introduction

Si le format XML et ses méthodes associées vous ont toujours rebuté pour manipuler des données et notamment les méta données des pages, essayer JSON (JavaScript Object Notation), c'est l'adopter. Pourquoi ? Les données d'un fichier JSON sont facilement lisibles et modifiables que ce soit humainement ou par des API (PHP, Python...). Même les moteurs de bases de données proposent de plus en plus le format JSON dans les retours de résultats comme PostgreSQL et la toute dernière version de MySQL 5.7 (Octobre 2015).

Qu'entend-on par méta données ? Par exemple, des propriétés globales que l'on ne souhaite surtout pas coder en dur dans les pages comme l'identifiant Google Analytics, AdSense, AddThis... ou encore des propriétés plus spécifiques à une page comme les liens externes de la bibliographie pour une page.

C'est le cas de cette page. 2 fichiers JSON sont chargés pour les propriétés. Le premier fichier JSON, common.json, contient les propriétés globales à toutes les pages :

common.json
{
  "google": {
    "analytics" :{
       "key": "UA-xxxxxx-1"
       ,"debug": false
    }
    ,"adsense" : {
       "client": "ca-pub-yyyyyyyyyyyy"
       ,"slot": "2025487690"
    }
  }
  
  "addthis": {
    "url": "http://s7.addthis.com/js/300/addthis_widget.js"
    ,"pubid": "compte_addthis"
    ,"track_addressbar_paths": [
       "/referentiel/docs/*"
       ,"/referentiel/docs-en/*"
     ]
    ,"trendingcontent" : {
       "feed": "http://q.addthis.com/feeds/1.0/trending.json"
       ,"defaultlimit":15
       ,"frequence": "week"
    }
  }
  
 ,"highlightjs": { "defaultcss" : "vs" }
}

Entre un format XML et un format JSON, le choix a été rapidement fait, même les imbrications de données ou de tableaux sont facilement lisibles avec le format JSON. La modification du fichier common.json s'appliquera à toutes les pages.

Le second fichier JSON définit les propriétés locales d'une page : le fichier a le même nom que celui de la page mais avec l'extension .json.

conception-json-fonction-generique-js-synchrone-asynchrone.json
{
  "id": "275"
  ,"date": "16 Septembre 2016"
  ,"logo": "json"
  ,"liens": [
   { "title": "JSON Formatter & Validator"
     ,"href":"https://jsonformatter.curiousconcept.com/"
	 }
   ,{ "title": "Tutoriel JSON"
      ,"href":"http://www.w3schools.com/json/"
   }
  ]
  ,"adsense": 3
}

Une fonction générique en Javascript pour charger les données des fichiers JSON, wsys_load_json, est proposée ici. La problèmatique du cache des navigateurs est abordée. L'exécution synchrone ou asynchrone du chargement des données JSON est également évoquée.

Code natif de chargement de données et manipulation de données JSON

Pour charger les données d'un fichier JSON (common.json ici), le code Javascript est relativement simple :


var xmlhttp = new XMLHttpRequest();
		
xmlhttp.onreadystatechange=function() {
  if (xmlhttp.readyState==4 && xmlhttp.status==200) {
      var dataprops = JSON.parse(xmlhttp.responseText);
  }
};

xmlhttp.open("GET","common.json",true);
xmlhttp.send();
					
  • Un objet XMLHttpRequest est créé (xmlhttp). Le fichier common.json est ouvert avec les méthodes open et send de cet objet.
  • La fin du chargement est détectée avec la méthode onreadystatechange de l'objet xmlhttp, méthode qui déclenche une fonction.
  • Lorsque la requête est reçue (xmlhttp.readyState=4) et le fichier JSON trouvé et chargé (xmlhttp.status=200), les données de la réponse (xmlhttp.responseText) sont parsées au format JSON avec JSON.parse et stockées dans l'objet Javascript dataprops

La variable dataprops est alors un objet javascript qui stocke et représente les données du fichier JSON. Sa manipulation est simple dans le code. Toujours avec l'exemple du fichier common.json, pour manipuler le code Google Analytics, on écrira en Javascript :

{
 "google": {
   "analytics" :{
      "key": "UA-xxxxxx-1" ...
}
console.log(dataprops.google.analytics.key);

Pour les tableaux, c'est tout aussi simple :

{
...
 "addthis": {
   ...
   ,"track_addressbar_paths": [
      "/referentiel/docs/*"
      ,"/referentiel/docs-en/*"
   ]
...
}
for (i=0; i < dataprops.addthis.track_addressbar_paths; i++) {
  console.log(dataprops.addthis.track_addressbar_paths[i]);
}
{
...
  ,"links": [
    { "title": "Titre 1" ,"href":"http://ur1" }
   ,{ "title": "Titre 2" ,"href":"http://url2" }
  ]
...
}
for (i=0; i < dataprops.links.length; i++) { {
  console.log(dataprops.links[i].title);
  console.log(dataprops.links[i].href);
}

La méthode classique typeof est utilisée pour détecter l'existence d'une donnée JSON :

if (typeof dataprops.adsense != "undefined") {
  console.log(dataprops.adsense);
}

Une fonction générique wsys_load_json

Bien évidemment, il n'est pas question de coder nativement chaque chargement d'un fichier JSON. Si les pages intègrent votre propre bibliothèque Javascript (lib.js pour l'exemple) :

<head>
  ...
  <script type="text/javascript"  src="../js/lib.js" </script>
  ...
</head>

Dans cette librairie, créér la fonction générique wsys_load_json avec 2 paramètres : url, pour donner l'URL du fichier JSON et callback pour définir une fonction de rappel (ou fonction callback) à exécuter à la fin du chargement des données JSON. La fonction callback prend en paramètre l'objet javascript dataprops.

wsys_load_json = function (url, callback) {
  var xmlhttp = new XMLHttpRequest();
		
  xmlhttp.onreadystatechange=function() {
    if (xmlhttp.readyState==4 && xmlhttp.status==200) {
        var dataprops = JSON.parse(xmlhttp.responseText);
        callback(dataprops);
    }
  };

  xmlhttp.open("GET",url,true);
  xmlhttp.send();
};

Les appels de fichiers JSON sont alors bien plus pratiques. Dans l'exemple ci-dessous, la fonction traitement_donnees est appelée après le chargement avec wsys_load_json du fichier common.json.

traitement_donnees = function(dataprops) {
  if (typeof dataprops.google.analytics.key != "undefined") { ... }
};

wsys_load_json("common.json",traitement_donnees);

Toutefois 2 points pour lesquels il faut porter une attention particulière, et c'est l'objet des 2 paragraphes qui suivent :

  • La gestion du cache des navigateurs pour les fichiers JSON.
  • Le mode synchrone / asynchrone du chargement de données de fichiers JSON.

Les fichiers JSON et le cache du navigateur

Des données sont modifiées dans un fichier JSON. La page est appelée depuis l'historique mais les données modifiées ne sont pas répercutées, les anciennes données apparaissent.

Ce comportement est normal, le navigateur utilise à priori les données de son cache. Pour le vérifier, ouvrir la console de développement du navigateur. Pour Chrome : Ctrl+Maj+I ou depuis le menu Plus d'outilsOutils de développement; appliquer un filtre sur le nom du fichier JSON et consulter l'onglet Network.Chrome - JSON en cache

Le statut est bien OK avec le code retour 200, mais le navigateur précise bien que les données proviennent du cache (from cache).

Tout dépend de la fréquence de mise à jour des données, mais si les données sont moins statiques que prévues dans un fichier JSON, 2 solutions sont possibles :

  • définir une expiration avec le module mod_expires du serveur Web Apache.
  • appliquer une empreinte pour forcer le navigateur à recharger le fichier JSON.

Expiration avec le module mod_expires du serveur Web Apache

Si le module mod_expires du serveur Web Apache est activé, en définissant une expiration d'accès à 0 seconde pour les fichiers *.json (application/json), le navigateur recharge systématiquement les fichiers *.json.

Il est normalement activé par défaut mais pour activer le module mod_expires, dans le fichier httpd.conf du serveur Apache :

httpd.conf
LoadModule expires_module modules/mod_expires.so

Ensuite dans un fichier .htaccess déposé dans le répertoire ou un répertoire parent contenant les fichiers *.json :

.htaccess
<IfModule mod_expires.c>
  ExpiresActive on
  ExpiresByType application/json                      "access plus 0 seconds"
</IfModule>

L'application de la directive d'expiration immédiate du fichier JSON est consultable dans les consoles des outils de développement des navigateurs. Avec Chrome : NetworkHeaders pour le fichier JSON concerné, les informations sont notifiées dans la rubrique "Response Headers" (Entêtes de réponse).

Response Headers

 ...
 Cache-Control:max-age=0
 ...
 Content-Type:application/json
 Date:Sat, 19 Sep 2016 14:54:38 GMT
 ...
 Expires:Sat, 19 Sep 2016 14:54:38 GMT
 ...

L'information Cache-Control:max-age=0 notifie l'expiration d'accès immédiate et force le navigateur à recharger la ressource JSON.

Si l'hébergeur du site n'a pas activé le module mod_expires ou n'autorise pas le dépôt d'un fichier .htaccess, l'option de l'empreinte est la seule solution.

Application d'une empreinte sur le fichier JSON

Deuxième et dernière option pour recharger systématiquement un fichier JSON et contourner le cache du navigateur : appliquer une empreinte. Avec cette méthode, au lieu d'appeler l'URL du fichier JSON :

xmlhttp.open("GET","common.json",true);

Une chaîne de 4 ou 5 caractères aléatoires est ajoutée en paramètre dans l'URL :

xmlhttp.open("GET","common.json?1bgdu",true);

Il y a de multiples possibilités pour générer une chaîne aléatoire, en voici une :

var randomKey = (0|Math.random()*9e6).toString(36);
          
wsys_load_json("common.json?" + randomKey, callback_function);

Avec cette empreinte, le rechargement systématique est garanti et le cache du navigateur est "bypassé".

Le mode synchrone / asynchrone lors des chargement de données JSON

Un sujet très intéressant : synchrone ou asynchrone. La réponse est sans appel, tout est asynchrone même si cela peut éventuellement dépendre du navigateur.

La fonction wsys_load_json est prête. À la fin du chargement de la page, la fonction process_page est appelée pour ajouter tous les éléments nécessaires (Google Analytics, AddThis...) :

lib.js
process_page = function() {
	...
};

if(window.addEventListener) {
	window.addEventListener("load",process_page, false); 
}
  • Les données globales sont stockées dans la variable v_gi_global avec la fonction load_global_properties à l'issue du chargement du fichier common.json.
  • Les données propres à la page sont stockées dans la variable v_gi_local avec la fonction load_local_properties à l'issue du chargement du fichier conception-json-fonction-generique-js-synchrone-asynchrone.json.
  • La fonction foo quant à elle remplit le bloc div ayant l'id box avec le code Google analytics récupéré depuis le fichier common.json (v_gi_global.google.analytics.key).

Tout naturellement, on code cette cinématique de la façon suivante :

load_global_properties = function(dataprops) {
  v_gi_global = dataprops;
};

load_local_properties = function(dataprops) {
  v_gi_local = dataprops;
};

foo = function() {
  v_div = document.getElementById('box');
  v_div.appendChild(document.createTextNode(v_gi_global_properties.google.analytics.key));
};

process_page = function() {
  wsys_load_json("common.json",load_global_properties);
  wsys_load_json("conception-json-fonction-generique-js-synchrone-asynchrone.json",load_local_properties);
  foo();
};

if(window.addEventListener) {
	window.addEventListener("load",process_page, false); 
}

Et là rien ne fonctionne comme prévu. La console des outils de développement donne le message "analytics undefined". Ce comportement paraît inattendu mais il est complètement normal : la cinématique est asynchrone, le moteur Javascript du navigateur n'attend pas la réponse xmlhttp et le parsing du fichier JSON pour passer à la suite, notamment la fonction foo() ici.

Pour s'en convaincre, ouvrir l'onglet Timeline de la console de développement de Chrome et réaliser un enregistrement (cliquer sur la figure 1 pour agrandir) :

Figure 1 - Mode asynchrone

La fonction foo() est exécutée à 112 ms, mais la réponse du chargement du fichier common.json est reçue à 117 ms. Lorsque la fonction foo() démarre, la variable v_gi_global ne peut pas encore être initialisée.

Si le mode synchrone est requis, il n'y a pas d'autre alternative que de coder un appel en cascade séquentiel.

  • Le chargement du fichier conception-json-fonction-generique-js-synchrone-asynchrone.json est inséré dans la fonction load_global_properties.
  • L'appel de la fonction foo est déplacé dans la fonction load_local_properties.

wsys_load_json("common.json",load_global_properties)

  load_global_properties
  
    wsys_load_json("conception-json-fonction-generique-js-synchrone-asynchrone.json",load_global_properties)

      load_local_properties
      
        foo

Le code de l'exemple devient :

load_global_properties = function(dataprops) {
  v_gi_global = dataprops;
  wsys_load_json("conception-json-fonction-generique-js-synchrone-asynchrone.json",load_local_properties);
};

load_local_properties = function(dataprops) {
  v_gi_local = dataprops;
  foo();
};

foo = function() {
  v_div = document.getElementById('box');
  v_div.appendChild(document.createTextNode(v_gi_global_properties.google.analytics.key));
};

process_page = function() {
  wsys_load_json("common.json",load_global_properties);
};

if(window.addEventListener) {
	window.addEventListener("load",process_page, false); 
}

La ligne de temps montre alors que la fonction foo est à présent bien appelé après le chargement du dernier fichier JSON, les variables v_gi_global et v_gi_local sont assurément initialisées avec les données nécessaires (cliquer sur la figure 2 pour agrandir).

Figure 2 - Mode synchrone

Conclusion

Pour bon nombre d'entre nous, XML n'a pas convaincu (syntaxes et méthodes lourdes). JSON, qui a à peine un peu plus de 10 ans, permet de renouer avec l'exploitation de données par Javascript : le code est simple, intuitif et facilement lisible. Ici les données proviennent de fichiers JSON statiques, mais ces données JSON peuvent très bien être le résultats de scripts PHP etc... Ce sujet n'a pas été abordé.

Une fonction générique de chargement des fichiers JSON (wsys_load_json), une attention particulière sur les caches des navigateurs et les exécutions synchrone / asynchrone, et la machine est lancée pour traiter et manipuler efficacement des données avec Javascript.

On devient vite très fan de ce format.