Affichage des articles dont le libellé est javascript. Afficher tous les articles
Affichage des articles dont le libellé est javascript. Afficher tous les articles

23 février 2012

node-xml-mapping = xml2json.js & json2xml.js

xml-mapping est un module NodeJS qui permet de transformer des données XML en une structure de données Javascript (JSON). Ce module permet également de réaliser l'opération inverse en transformant une structure de données Javascript en XML. Ce module se caractérise par quelques avantages :

  • 100% Javascript
  • Bidirectionnel
  • Prise en charge des Comments, des CDATA, des Processing Instructions

Convention

Pour transformer du XML en JSON il faut des règles de correspondance. Ce module reprend celles utilisées par Google dans son protocole GData (cf. http://code.google.com/apis/gdata/docs/json.html ). L'approche des ingénieurs Google est intéressante car elle cherche à adapter la structure des données aux caractéristiques de Javascript. Par exemple, les attributs ne sont pas dans un tableau d'attributs ou bien la valeur d'un balise est stockée dans un champ spécial ou encore les sous balises sont regroupées dans une collection et les balises multi-occurrentes sont regroupées dans un tableau.

Démonstration

Ce démonstrateur :

var xm = require('../lib/xml-mapping.js')
 ,util = require('util')
 ,assert = require('assert');

[
 '<key>value</key>'
 ,'<key attr="value1">value2</key>'
 ,'<key><key1>value1</key1><key2>value2</key2></key>'
 ,'<key><key>value1</key><key>value2</key></key>'
 ,'<key><key><key>value2</key></key></key>'
 ,'<key><![CDATA[value]]></key>'
 ,'<key><!--value--></key>'
].forEach(function(xml) {
  var json = xm.tojson(xml);
  process.stdout.write(xml);
  process.stdout.write('\n');
  process.stdout.write(util.inspect(json, false, null, true));
  process.stdout.write('\n\n');
  assert.equal(xml, xm.toxml(json));
})

affichera :

<key>value</key>
{ key: { '$t': 'value' } }

<key attr="value1">value2</key>
{ key: { attr: 'value1', '$t': 'value2' } }

<key><key1>value1</key1><key2>value2</key2></key>
{ key: 
   { key1: { '$t': 'value1' },
     key2: { '$t': 'value2' } } }

<key><key>value1</key><key>value2</key></key>
{ key: 
   { key: 
      [ { '$t': 'value1' },
        { '$t': 'value2' } ] } }

<key><key><key>value2</key></key></key>
{ key: { key: { key: { '$t': 'value2' } } } }

<key><![CDATA[value]]></key>
{ key: { '$cd': 'value' } }

<key><!--value--></key>
{ key: { '$c': 'value' } }


Installation et code source

Le code source est disponible sur GitHub : http://github.com/lindory-project/node-xml-mapping

et on peut directement l'installer avec NPM :

% npm install xml-mapping

16 novembre 2011

Lecture ligne par ligne avec NodeJS

Lire un fichier ou plus généralement un flux de données ligne par ligne est une opération triviale pour toutes les personnes connaissant la fonction fgets. Voici 4 méthodes pour résoudre cette problématique basique avec NodeJS :


0. String & Split

C'est la méthode la plus simple, il suffit de charger la totalité du fichier dans une chaîne de caractères puis découper cette chaîne en fonction du caractère \n

#!/usr/bin/env node

var buffer = '';

process.stdin.resume();
process.stdin.setEncoding('utf8');

process.stdin.on('data', function (chunk) {
  buffer += chunk;
});

process.stdin.on('end', function () {
  buffer.split('\n').forEach(function(line) {
    process.stdout.write(line);
    process.stdout.write('\n');
  });
});

1. Module byline


#!/usr/bin/env node

var byline = require('byline');

process.stdin.resume();
process.stdin.setEncoding('utf8');

stream = byline.createLineStream(process.stdin);

stream.on('data', function(line) {
  process.stdout.write(line);
  process.stdout.write('\n');
});
stream.on('end', function() {
  // Ended !
});

2. Module carrier


#!/usr/bin/env node

var carrier = require('carrier');

process.stdin.resume();
process.stdin.setEncoding('utf8');

carrier.carry(process.stdin, function(line) {
  process.stdout.write(line);
  process.stdout.write('\n');
});


3. Module line-reader


#!/usr/bin/env node

var lineReader = require('line-reader');

lineReader.eachLine('/dev/stdin', function(line, last) {
  process.stdout.write(line);
  process.stdout.write('\n');
});

4. Module node-each-line


#!/usr/bin/env node

var each_line = require('./each_line').each_line;

each_line(process.stdin, function (line) {
  process.stdout.write(line);
});


Comparaison

Pour faire son choix, il est intéressant de comparer leur performance pour cela
on commence par les installer:

npm install byline
npm install carrier
npm install line-reader
wget https://raw.github.com/aaronj1335/node-each-line/master/each_line.js

Ensuite, on utilise un fichier qui va générer quelques milliers de lignes :

#!/usr/bin/env node

for(var i = 1; i <= 100000; i++) {
 process.stdout.write(i+'');
 process.stdout.write('\n');
}

Reste à exécuter le tout :

$ time ./input.js | ./00.js >/dev/null

real 0m14.753s
user 0m14.973s
sys 0m1.288s

$ time ./input.js | ./01.js >/dev/null

real 0m11.714s
user 0m11.885s
sys 0m1.204s

$ time ./input.js | ./02.js >/dev/null

real 0m11.495s
user 0m11.761s
sys 0m1.164s

$ time ./input.js | ./03.js >/dev/null

node.js:134
        throw e; // process.nextTick error, or 'error' event on first tick
        ^
Error: Socket is not writable
    at Socket._writeOut (net.js:391:11)
    at Socket.write (net.js:377:17)
    at Object.<anonymous> (/home/thouveni/Tests/node/lines/input.js:4:17)
    at Module._compile (module.js:402:26)
    at Object..js (module.js:408:10)
    at Module.load (module.js:334:31)
    at Function._load (module.js:293:12)
    at Array.<anonymous> (module.js:421:10)
    at EventEmitter._tickCallback (node.js:126:26)

real 0m0.057s
user 0m0.068s
sys 0m0.036s

$ time ./input.js | ./04.js >/dev/null

real 0m5.096s
user 0m5.764s
sys 0m0.624s

Conclusion

Le module line-reader ne fonctionne pas, à ignorer. Les modules carrier et byline sont équivalents. Et le script node-each-line est le plus rapide mais il ne permet pas d'intercepter la fin de fichier. Le choix est donc une affaire de gout. Personnellement, j'utilise byline.

08 septembre 2011

Underscore + JSONSelect = XPath for JSON ?

XML est largement utilisé pour formater, stocker, échanger des informations structurées. Et malgré l’étendu de ses capacités, il n'en reste pas moins qu'un support. Et en tant que support JSON n'a rien à envier à XML. N'importe quelle information peut-être stocker avec JSON. Par contre l'écosystème XML est riche et on peut facilement transformer, valider, émietter nos données, ce qui semble moins évident avec JSON. Mais qui dit JSON dit Javascript et l'écosystème Javascript est également très riche et il permet de faire aussi bien voir mieux que XML dans certain domaine. Voici une technique pour picorer de l'information comme on le ferai avec XPath sur du XML mais sur un objet JSON.

JSONSelect

JSONSelect permet de picorer dans un objet javascript à l'aide de sélecteur CSS. Exemple :
#!/usr/bin/env node

var jsel = require('JSONSelect');
var doc = {
	"id": "0001",
	"type": "donut",
	"name": "Cake",
	"ppu": 0.55,
	"batters":
		{
			"batter":
				[
					{ "id": "1000", "type": "None" },
					{ "id": "1001", "type": "Regular" },
					{ "id": "1002", "type": "Chocolate" },
					{ "id": "1003", "type": "Blueberry" },
					{ "id": "1004", "type": "Devil's Food" }
				]
		},
	"topping":
		[
			{ "id": "5001", "type": "None" },
			{ "id": "5002", "type": "Glazed" },
			{ "id": "5005", "type": "Sugar" },
			{ "id": "5007", "type": "Powdered Sugar" },
			{ "id": "5006", "type": "Chocolate with Sprinkles" },
			{ "id": "5003", "type": "Chocolate" },
			{ "id": "5004", "type": "Maple" }
		]
};

console.log(jsel.match('.name', doc));
// [ 'Cake' ]
console.log(jsel.match('number', doc));
// [ 0.55 ]
console.log(jsel.match('.batter .type', doc));
// [ 'Regular', 'Chocolate', 'Blueberry', 'Devil\'s Food' ]
console.log(jsel.match('.type ~ .id:val("5005")', doc));
// [ '5005' ]
console.log(jsel.match('.type:val("None") ~ .id', doc));
// [ '1000', '5001' ]



Underscore

Underscore fournit ~ 60 fonctions orientées programmation fonctionnelle.
Et partie elles, une trentaine simplifient grandement la manipulation et le traitement des listes et des tableaux. Exemple :
#!/usr/bin/env node

var _ = require('underscore');

var arr = ["1000", "1001", "1002", "1003", "1004"];
console.log(_.first(arr));
// [ '1000' ]
console.log(_.without(arr, "1000"));
// [ '1001', '1002', '1003', '1004' ]
console.log(_(arr).chain().without(arr, "1000").first().value());
// 1001
console.log(_.map(arr, function(value) {
			return '#'+value ;
}));
// [ '#1000', '#1001', '#1002', '#1003', '#1004' ]
console.log(_(arr).chain().map(function(value) { 
			return value.substr(1, 2);
	}).uniq().first().value());
// 00


JSONSelect & Underscore

JSONSelect permet de récupérer des listes d'objet ou des tableaux de valeur, Underscore permet de manipuler des listes et des tableaux.
Coupler les deux offre donc un outil extrêmement puissant pour aller piocher et trier de l'information. Exemple :
#!/usr/bin/env node

var jsel = require('JSONSelect'),
	_ = require('underscore');

//  Underscore + JSONSelect
_.mixin({
		jsel : function(doc, sel) {
			return jsel.match(sel, doc)
		}
});

var data = [
	{ name:'Africa', type:'continent'},
	{ name:'Egypt', type:'country' },
	{ name:'Kenya', type:'country'},
	{ name:'Nairobi', type:'city' },
	{ name:'Mombasa', type:'city' },
	{ name:'Sudan', type:'country'},
	{ name:'Khartoum', type:'city' },
	{ name:'Asia', type:'continent'},
	{ name:'China', type:'country' },
	{ name:'India', type:'country' },
	{ name:'Russia', type:'country' },
	{ name:'Mongolia', type:'country' },
	{ name:'Australia', type:'continent'},
	{ name:'Commonwealth of Australia', type:'country'},
	{ name:'Europe', type:'continent'},
	{ name:'Germany', type:'country' },
	{ name:'France', type:'country' },
	{ name:'Spain', type:'country' },
	{ name:'Italy', type:'country' },
	{ name:'North America', type:'continent'},
	{ name:'Mexico', type:'country'},
	{ name:'Mexico City', type:'city'},
	{ name:'Guadalajara', type:'city'},
	{ name:'Canada', type:'country'},
	{ name:'Ottawa', type:'city'},
	{ name:'Toronto', type:'city'},
	{ name:'United States of America', type:'country' },
	{ name:'South America', type:'continent'},
	{ name:'Brazil', type:'country'},
	{ name:'Argentina', type:'country'}
];

console.log(_(data).chain().jsel('.type').uniq().value());
// [ 'continent', 'country', 'city' ]
console.log(_(data).chain().jsel('.type:val("city") ~ .name').sortBy(function(value) { 
			return value.charCodeAt(0)
	}).value());
// [ 'Guadalajara', 'Khartoum', 'Mombasa', 'Mexico City', 'Nairobi', 'Ottawa', 'Toronto' ]


06 septembre 2011

Installer NodeJS dans son HOMEDIR avec STOW

Il y a quelque temps déjà, j'avais présenté l'utilitaire stow. Mais avec les distributions Linux récentes cet utilitaire avait peu d’intérêt tant l'usage d'apt-get install suffisait la majorité du temps. Mais voilà, avec NodeJS, on ne peut pas attendre que la dernière version soit packagée pour l'utiliser. Il faut donc l'installer soit même, et si il existe plusieurs méthodes, aucune ne permet de changer de version de nodejs en un fragment de seconde. Démonstration !

STOW

Stow permet de recréer une arborescence système à l'aide de liens symboliques. Ces liens pointent sur un répertoire isolé et ils sont générés automatiquement à l'aide d'une simple commande.


Pré requis système

Pour installer et compiler NodeJS, il faut vérifier que les paquets g++ et libssl-dev sont installés.

#!/bin/sh

sudo apt-get install g++ libssl-dev


Installation de STOW

#!/bin/sh

# CHOOSE THE VERSION YOU WANT 
export STOW_VERSION=1.3.3

mkdir ~/local/stow
mkdir -p ~/local/src
mkdir ~/local/bin
mkdir ~/local/info
mkdir ~/local/etc
mkdir ~/local/include
mkdir ~/local/share
mkdir ~/local/share/man
mkdir ~/local/lib
mkdir ~/local/man
mkdir ~/local/man/man1
mkdir ~/local/man/man2
mkdir ~/local/man/man3
mkdir ~/local/man/man4
mkdir ~/local/man/man5
mkdir ~/local/man/man6
mkdir ~/local/man/man7
mkdir ~/local/man/man8

cd ~/local/src
curl http://ftp.gnu.org/gnu/stow/stow-${STOW_VERSION}.tar.gz | tar -xzf -

cd stow-${STOW_VERSION}
./configure --prefix=~/local/stow/stow-${STOW_VERSION}

make
make install

cd ~/local/stow/
./stow-1.3.3/bin/stow stow-1.3.3

echo "export PATH=\$HOME/local/bin:\${PATH:=}" >> ~/.profile
echo "export MANPATH=\$HOME/local/man:\${MANPATH:=}" >> ~/.profile
echo "export LD_LIBRARY_PATH=\$HOME/local/lib:\${LD_LIBRARY_PATH:=}" >> ~/.profile

export PATH=$HOME/local/bin:${PATH:=}
export MANPATH=$HOME/local/man:${MANPATH:=}
export LD_LIBRARY_PATH=$HOME/local/lib:${LD_LIBRARY_PATH:=}


NodeJS

NodeJS doit être compilé manuellement pour bénéficier des dernières versions. On va donc compiler et installer chaque version dans un répertoire spécifique.


Installation de NodeJS pour STOW

#!/bin/sh

# CHOOSE THE VERSION YOU WANT 
export NODEJS_VERSION=0.6.1

mkdir -p ~/local/lib/node_modules
cd ~/local/src

curl http://nodejs.org/dist/v${NODEJS_VERSION}/node-v${NODEJS_VERSION}.tar.gz | tar -xzf -
cd ~/local/src/node-v${NODEJS_VERSION}

./configure --prefix=~/local/stow/node-${NODEJS_VERSION}
make
make install

# Careful, check if stow is installed
cd ~/local/stow
stow node-${NODEJS_VERSION}

echo "export NODE_PATH=\$HOME/local:\$HOME/local/lib/node_modules" >> ~/.profile
export NODE_PATH=$HOME/local:$HOME/local/lib/node_modules


Changer de version facilement

Maintenant tester une nouveau version de NodeJS est très simple :

$ node -v
v0.4.11
$ cd ~/local/stow
$ stow -D node-0.4.11
$ stow node-0.6.1
$ node -v
v0.6.1

26 août 2011

HTTP request loop in NodeJS

Comment envoyer 200000 requêtes HTTP avec NodeJS ? A priori cette question semble facile ? Voyons si c'est le cas ?

Commençons petit

Avant d'envoyer plusieurs centaines de requêtes voyons déjà comment en envoyer une seule :

#!/usr/bin/env node

var http = require('http');

var options = {
  host: '127.0.0.1',
  port: 80,
  path: '/'
};

http.get(options, function(response) {
    console.log(response.statusCode);
});

On boucle !

#!/usr/bin/env node

var http = require('http');

var options = {
  host: '127.0.0.1',
  port: 80,
  path: '/'
};

for(var i = 0; i < 2000000; i++) {
	http.get(options, function(res) {
   	    console.log(res.statusCode);
	});
}

Résultat :


FATAL ERROR: JS Allocation failed - process out of memory

Économiser la mémoire

Ok, on y a été un peu fort, on va optimiser notre boucle en sortant le client HTTP de la boucle, histoire de l'instancier qu'une seule fois.

#!/usr/bin/env node

var http = require('http');


var client = http.createClient(80, '127.0.0.1');


for(var i = 0; i < 200000; i++) {
	var request = client.request("GET", '/')
	request.end();
	request.on('response', function(response) {
			console.log(response.statusCode);
	});
}

Résultat :


node.js:205
        throw e; // process.nextTick error, or 'error' event on first tick
              ^
Error: EMFILE, Too many open files
    at net_legacy.js:741:19
    at Object.lookup (dns_legacy.js:159:5)
    at Socket.connect (net_legacy.js:729:20)
    at Object.createConnection (net_legacy.js:268:5)
    at new ClientRequest (http2.js:1041:23)
    at Client.request (http2.js:1475:11)
    at Object.<anonymous> (/home/touv/tests/node/loop02.js:11:23)
    at Module._compile (module.js:416:26)
    at Object..js (module.js:434:10)
    at Module.load (module.js:335:31)

Oups, NodeJS est-il incapable d'exécuter un traitement aussi simple ? Ou mon code est-il mauvais ?

Pool HTTP Request

Pour lancer nos 200000 requetes, il va falloir économiser la mémoire et les descripteurs, mais surtout s'adapter au mode asynchrone et éviter de vouloir balancer 200000 requêtes en parallèle !

Pour nous aider, il existe le module node-pool qui propose :
"a small but usefull resource pooling/limiting/throttling library".
Voilà donc la solution à notre question toute simple ...

#!/usr/bin/env node

var http = require('http');

var pool = require('generic-pool').Pool({
		name     : 'http_request',
		create   : function(callback) {
			var c = http.createClient(80, '127.0.0.1');
			callback(null, c);
		},
		destroy  : function(client) { },
		max      : 10,
		idleTimeoutMillis : 300,
		log : false
});


for(var i = 0; i < 200000; i++) {
	pool.acquire(function(err, client) {
			var request = client.request("GET", '/');
			request.end();
			request.on('response', function(response) {
					console.log(response.statusCode);
					pool.release(client);
			});
	});

}

19 août 2011

Mapping XML - Array - JSON en PHP

Vouloir établir une correspondance entre JSON et XML n'est pas nouveau. (cf. Converting Between XML and JSON). D'ailleurs, le Zend Framework propose sa solution XML to JSON conversion.
Cependant,

  • le format est-il adapté ?
  • l’opération inverse est-elle possible ?

Vouloir passer du XML au JSON revient en fait à transformer du XML en une structure mémoire de type tableau. Toutes les solutions en PHP que j'ai trouvé passe par cette étape. Aucune ne transforme la chaîne de caractères XML en chaîne de caractères JSON. La véritable question est donc :
Comment faire un mapping entre XML et un tableau PHP ?
Le format JSON n'étant plus qu'une sérialisation du tableau. Du coup, l'opération inverse n'est pas de vouloir transformer du JSON en XML mais bien de vouloir transformer un tableau en XML. Le problème est donc de passer d'une structure mémoire à un fichier XML et réciproquement. CQFD ;-)
La belle affaire me direz-vous ? C'est pareil ! Oui sauf qu'en abordant le problème de cette manière, on doit produire un code capable de transformer toute structure mémoire en XML, et cela a une influence directe sur le format d’équivalence que l'on va choisir. Et à ce petit jeu là, c'est le format proposé par Google qui me semble le plus adapté ! cf. Using JSON in the Google Data Protocol

XML_Array

Voici donc une petite classe PHP, disponible au travers d'un package PEAR qui permet de transformer n'importe quel document XML en tableau PHP. Mais qui permet également l'opération inverse à savoir transformer n'importe quel tableau PHP en fichier XML.

array2xml.php

<?php
require_once 'XML/Array.php';

$array_input = array (
  'rdf:RDF' => 
  array (
    'xmlns:rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
    'xmlns' => 'http://www.exemple.com/fake#',
    'rdf:Description' => 
    array (
      0 => 
      array (
        'rdf:about' => 'urn:id:aaa',
        'name' => 
        array (
          '#text' => 'Dupont T.',
        ),
        'email' => 
        array (
          'type' => 'main',
          '#text' => '[email protected]',
        ),
      ),
      1 => 
      array (
        'rdf:about' => 'urn:id:bbb',
        'name' => 
        array (
          '#text' => 'Dupond D.',
        ),
        'email' => 
        array (
          'type' => 'main',
          '#text' => '[email protected] ',
        ),
      ),
    ),
  ),
);

$xml = XML_array::export($array_input);
$array_output = XML_array::import($xml);

assert($array_input == $array_output);

?>

xml2array.php

<?php
require_once 'XML/Array.php';
$xml_input = <<<EOT
<rdf:RDF
	xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
	xmlns="http://www.exemple.com/fake#">
	<rdf:Description rdf:about="urn:id:aaa">
		<name>Dupont T.</name>
		<email type="main">[email protected]</email>
	</rdf:Description>
	<rdf:Description rdf:about="urn:id:bbb">
		<name>Dupond D.</name>
		<email type="main">[email protected] </email>
	</rdf:Description>
</rdf:RDF>
EOT;

$array = XML_Array::import($xml_input);
$xml_output = XML_array::export($array);
 
$xml_input = preg_replace(',\s+,', '', $xml_input);
$xml_output = preg_replace(',\s+,', '', $xml_output);

assert($xml_input == $xml_output);
?>

Tables de correspondance

Règles principales
ARRAY XML
key ⇒ array( key1 ⇒ value, ) <key key1="value/>
key ⇒ array( #text ⇒ value1, ) <key>value1</key>
key ⇒ array( array(…), array(…), ) <key>…</key><key>…</key>
0 ⇒ value <![CDATA[value]]>
key ⇒ value <row key="value" />
#comment ⇒ value <!--value-→
Clefs réservées
XML values key
Text node $t, _t, _text, #text
comment node $c, _c, _comment, #comment
xml:lang xmllang, xml:lang, xml$lang
xml:space xmlspace, xml:space, xml$space
xml:id xmlid, xml:id, xml$id
xml:idref xmlidref, xml:idref, xml$idref

Benchmark

Voici une petite comparaison de performance en chargeant en mémoire séquentiellement 196867 documents XML pour un volume total de 250 Mo.


xml2str   => 53,292876 Seconds 
--------------------------------
xml2array => 178,356342 Seconds 
--------------------------------
xml2dom   => 64,135433 Seconds 

Téléchargement et code source

Le code source est disponible sur GitHub : http://github.com/touv/xml_array

ou on peut directement l'installer avec PEAR en s'abonnant au Channel Respear :


% pear channel-discover pear.respear.net
% pear install respear/xml_array

Webographie

25 mai 2009

Message d'avertissement Javascript pour IE6

Il devient usant pour un développeur Web de rendre compatible un site ou une page web avec IE6 (Microsoft Internet Explorer Version 6).

Si vous n'avez plus de temps à perdre avec ce navigateur, contentez-vous d'afficher un message d'avertissement à destination uniquement des internautes utilisant ce logiciel avarié.

Concrètement, il s'agit de faire un test en fonction du navigateur, ce test peut être fait du coté serveur (en PHP par exemple) ou coté client en Javascript.

Voici 2 méthodes pour le faire en Javascript

Prêt à l'emploi

Voici 2 petites libraires Javascript qui se chargent de faire le boulot :

Fait maison

Il existe une astuce permettant de réaliser une instruction javascript uniquement sous IE6, ensuite libre à chacun d'afficher son message.


if (typeof document.body.style.maxHeight === "undefined") {
    alert('IE6 not suported !');
    return false;
}

Il reste juste à exécuter cette instruction au chargement de la page et ça aussi ça pose problème sous ie6. Voir le blog de Dean Edwards à ce sujet

23 mai 2009

Compresser automatiquement ses fichiers javascript et CSS

Quand on développe une page ou un site Web, il arrive souvent que l'on utilise plusieurs fichiers de styles CSS et plusieurs fichiers de script (Javascript).

Les bonnes pratiques du Web recommandent d'avoir pour chaque page html un seul et unique fichier de style et un seul et unique fichier de script. Idéalement ces 2 fichiers doivent également être comprimés (cad : sans caractère inutile, sans commentaire, etc ...).

Sites Web

Plusieurs sites Web proposent un service en ligne permettant de comprimer du CSS et/ou du Javascript soit à partir de l'upload d'un fichier soit à partir d'une zone de saisie.

Programmes ou APIs

Il existe également plusieurs programme ou API plus ou moins puissante ...

Makefile ad hoc

Je souhaitais mettre en place en un minimum de temps une procédure automatique me permettant de compresser et regrouper mes fichiers javascript et css.

Après une tentative d'installation infructueuse de juicer, j'ai rapidement écrit un petit fichier Makefile qui fait parfaitement l'affaire.

Avec seulement, les commandes cat et curl je génère un fichier script.min.js et un fichier style.min.css à partir de plusieurs fichiers css et js présent dans mon répertoire.

Simple, rapide, efficace, ...


all: style.min.css script.min.js

style.css : file1.css file2.css file3.css
 cat $^ > $@

style.min.css : style.css
 curl http://www.refresh-sf.com/yui/ -F type=CSS -F compressfile[]=@$^ | \
  grep "<textarea" | grep "class=\"output\"" | \
  sed -e "s/<\/textarea>//g" \
  -e "s/<textarea rows=\"20\" cols=\"80\" class=\"output\">//g" \
  -e "s/^\s*//g" \
  -e "s/\s*$$//g" \
  -e "s/&amp;/\&/g" \
  -e "s/&lt;/</g" \
  -e "s/&gt;/>/g" \
  > $@

script.js : file1.js file2.js file3.js
 cat $^ > $@

script.min.js : script.js
 curl http://www.refresh-sf.com/yui/ -F type=JS -F compressfile[]=@$^ | \
  grep "<textarea" | grep "class=\"output\"" | \
  sed -e "s/<\/textarea>//g" \
  -e "s/<textarea rows=\"20\" cols=\"80\" class=\"output\">//g" \
  -e "s/^\s*//g" \
  -e "s/\s*$$//g" \
  -e "s/&amp;/\&/g" \
  -e "s/&lt;/</g" \
  -e "s/&gt;/>/g" \
  > $@

clean: script*.js style*.css
 rm $^



18 novembre 2008

Comment traiter du JSON pour produire du HTML en Javascript ?

Lors d'un développement de type AJAX, on se trouve rapidement confronté à la problématique suivante :

  • Je récupère des données au format JSON
  • Je les affiche en HTML

Question toute bête à priori : mais comment faire cela ?

Old School

On parcourt la structure Javascript et on produit le HTML avec des fonctions comme document.write ou element.appendChild

Exemple :


var data = [ "Meuse",   "Vosges",   "Moselle",   "Meurthe & Moselle" ];

document.write('<ul>');
for(i = 0; i < data.length; i++) {
    document.write('<li>'+data[i]+'</li>');
}
document.write('</ul>');

ou


<ol id="list">
</ol>
        
ol = document.getElementById("list");
for(i = 0; i < data.length; i++) {
    li = document.createElement("li");
    li_text = document.createTextNode(data[i]);
    li.appendChild(li_text);
    ol.appendChild(li);
}

Temps modernes : les frameworks

Bien sûr, maintenant, plus personne n'écrit du Javascript sans utiliser des frameworks comme Prototype ou JQuery.

Cependant, on écrira la même chose qu'auparavant de manière plus simple :

Avec Prototype :


<ol id="list3"> </ol>

data.each(function(item) {
    $('list3').insert(new Element('li').update(item));
});

Avec JQuery :


<ol id="list2"> </ol>


jQuery.each(data, function(i,item){
    jQuery("#list2").append(jQuery("<li>"+item+"</li>"));
});

On remarque ici la présence presque obligatoire d'une balise html d'ancrage. (ici, la balise &lt;ol id="listX"&gt; )

Le futur : les templates

A l'usage, les méthodes précédentes posent évidement le problème de la séparation du fond et de la forme : un problème bien connu en PHP et plus généralement pour la conception de site web traditionnel. (cf l'article Wikipedia sur les templates)

Alors nous voilà revenu au temps du choix d'un moteur de template !

Pour faire ma sélection dans cette liste, il y a plusieurs critères :

  • une preuve de vie (évolution des versions par exemple, activité sur le site)
  • une (bonne) documentation à jour
  • un fonctionnement multi- browser
  • une capacité à traiter des structures de données complexes

J'ai choisi de comparer :

  • JSrepeter
  • jstemplate
  • Pure

Pour chacun d'entre eux, j'essayerai de traiter le tableau de données suivant :


var data = {
   "items":[
      {
         "titre":"titre 1",
         "img":"http:\/\/localhost\/images\/01.jpg",
         "list1":[
            "row 1", 
            "row 2"
         ],
         "list2":[
            {
               "nom":"nom 1"
            },{
               "nom":"nom 2"
            }
         ]
      },
      {
         "titre":"titre 2",
         "img":"http:\/\/localhost\/images\/02.jpg",
         "list1":[
            "row A", 
            "row B"
         ],
         "list2":[
            {
               "nom":"nom A"
            },{
               "nom":"nom B"
            }
         ]
      }
   ]
}

Compiler un template

Avec jsrepeter :


$('#test').fillTemplate(data);

Avec jstemplate :


var input = new JsEvalContext(data);
var output = document.getElementById('test');
jstProcess(input, output);

Avec pure :


$('#test').autoRender(data);

Afficher une valeur dans une balise ou l'attribut d'une balise

Avec jsrepeter :


<ol id="test">
    <li context='items' >
        <img src="${img}" />
        <span>${titre}</span>
    </li>
</ol>

Avec jstemplate :


<ol id="test">
    <li jsselect="items" >
        <img jsselect="img" jsvalues="src:$this" />
        <span jscontent="titre">xxx</span>
    </li>
</ol>

Avec pure :


<ol id="test">
    <li class="items">
        <img class="img@src" src="xxx" />
        <span class="titre">XXX</span>
    </li>
</ol>        
                                                                                

Traiter les sous listes avec un index numérique

Avec jsrepeter :

Impossible à faire ?
En tout cas pour le moment, je n'ai pas réussi.

Avec jstemplate :


<ol id="test">
    <li context='items' >
        <ol>
        <li jsselect="list1" jscontent="$this">XXX</li>
        </ol>
    </li>
</ol>

Avec pure :

En théorie (cf .démo) cela semble possible, mais la syntaxe est loin d'être triviale, j'abandonne pour le moment.

Traiter les sous listes avec un index alphanumérique

Avec jsrepeter :


<ol id="test">
    <li context='items' >
        <ol>
        <li context='list2'>${nom}</li>
        </ol>
    </li>
</ol>

Avec jstemplate :


<ol id="test">
    <li context='items' >
        <ol>
        <li jsselect="list2"  jscontent="nom">XXX</li>
        </ol>
    </li>
</ol>

Avec pure :
En théorie (cf .démo) cela semble possible, mais la syntaxe est loin d'être triviale, j'abandonne pour le moment.

Conclusion

A ce jour, pour moi, le meilleur système de template est jstemplate, car il est capable de traiter n'importe quel format de données tout en restant très simple à l'usage.

Malgré une apparente facilité, Pure est compliqué à utiliser. De plus, l'usage de l'attribut class prête à confusion et rend le template au final moins lisible.

Jsreapter possède la syntaxe la plus simple et il est surement le plus facile à appréhender. Malheureusement, il semble incapable de traiter les index numériques...