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 fichiercommon.json
est ouvert avec les méthodesopen
etsend
de cet objet. - La fin du chargement est détectée avec la méthode
onreadystatechange
de l’objetxmlhttp
, 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 avecJSON.parse
et stockées dans l’objet Javascriptdataprops
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.
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 fonctionload_global_properties
à l’issue du chargement du fichiercommon.json
. - Les données propres à la page sont stockées dans la variable
v_gi_local
avec la fonctionload_local_properties
à l’issue du chargement du fichierconception-json-fonction-generique-js-synchrone-asynchrone.json
. - La fonction
foo
quant à elle remplit le blocdiv
ayant l’idbox
avec le code Google analytics récupéré depuis le fichiercommon.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) :
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 fonctionload_global_properties
. - L’appel de la fonction
foo
est déplacé dans la fonctionload_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).
Les outils pour valider, tester le format JSON, migrer du format XML vers le format JSON
Une erreur de syntaxe qui se glisse, et plus rien ne fonctionne. De nombreux outils en ligne existent pour valider, tester les formats JSON. Parmi eux citons les nombreux outils du site extendsclass.com, un site écrit en anglais mais l’auteur est en France :
Parmi ces outils, le plus intéressant : l’outil de conversion du format XML au format JSON, bien pratique quand on envisage de migrer tous ses fichiers XML, qu’il s’agisse de fichiers de configuration ou de fichiers d’échange de données :
Le résultat peut nécessiter des ajustements, mais cet outil est un excellent point de départ dans la définition de la migration et quitter par exemple la librairie SimpleXML utilisée avec PHP :
<?xml version="1.0" encoding="utf-8" ?>
<config>
<dbproperties>
<property name="DbType" caption="Type" type="select#MYISAM,MySQL;MYSQLI,MySQLi;SYBASE,SYBASE">MYSQLI</property>
<property name="DbServer" caption="Serveur" type="">localhost%3A45000</property>
<property name="DbUser" caption="User" type="">sqlpac</property>
<property name="DbPwd" caption="Mot de passe" type="crypt">I2Zhc3BhcyM5MjQ%3D</property>
<property name="DbDb" caption="Base de donnees" type="">mydb</property>
<property name="DbTableAuth" caption="Table authentification" type=""></property>
</dbproperties>
</config>
Conversion
{
"config":{"dbproperties":{"property":[
{
"@name":"DbType",
"@caption":"Type",
"@type":"select#MYISAM,MySQL;MYSQLI,MySQLi;SYBASE,SYBASE",
"#text":"MYSQLI"
},
{
"@name":"DbServer",
"@caption":"Serveur",
"@type":"",
"#text":"localhost%3A45000"
},
{
"@name":"DbUser",
"@caption":"User",
"@type":"",
"#text":"sqlpac"
},
{
"@name":"DbPwd",
"@caption":"Mot de passe",
"@type":"crypt",
"#text":"I2Zhc3BhcyM5MjQ%3D"
},
{
"@name":"DbDb",
"@caption":"Base de donnees",
"@type":"",
"#text":"mydb"
},
{
"@name":"DbTableAuth",
"@caption":"Table authentification",
"@type":""
}
]}}
}
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.