Attention, cet article couvre un aspect technique du langage Node.js. Nous vous recommandons tout d’abord de lire l’article de présentation de node.js
Une promesse (promise en anglais) est un objet représentant le résultat d’un appel de fonction asynchrone. Une promesse possède un état et peut être « en attente » (pending), « réalisée » (fulfilled) ou « rejetée » (rejected). Vous avez peut-être déjà entendu parler des promesses sous les formes de « futures » ou « deferred ».
Si vous voulez commencer par lire les spécifications des promesses, elles sont disponibles ici (en anglais) : Promises/A+
Les promesses permettent de résoudre un problème récurrent en Node.js, celui du « callback hell », une imbrication de fonction de callback. Étant donné que Node.js est basé sur l’Event Loop (une simple file FIFO de callback), couplé à de multiples fonctions asynchrones, on se retrouve rapidement avec du code comme ça :
var fs = require('fs') fs.readFile("mon_fichier", function readFileCallback(err, data) { if (!data) { // file not found fs.writeFile("mon_fichier", "123", function writeFileCallback(err) { if (err) throw err; fs.chmod("mon_fichier", "755", function(err) { if (err) throw err; fs.chown("mon_fichier", 501, 20, function(err) { if (err) throw err; fs.stat("mon_fichier", function(err, stats) { if (err) throw err; // Faire quelque chose avec "stats" console.log("err", err, "stats", stats); }); }); }); }); } });
Ce n’est pas le code le plus élégant au monde… Voyons à quoi cela ressemblerait avec l’aide des promesses ! Elles vont nous permettre d’applanir cette pyramide et de retrouver du code linéaire. Nous allons bien entendu utiliser un paquet npm, il en existe une multitudes, mais les plus connus sont :
J’ai choisi d’illustrer mes exemples à l’aide de la librairie « Promise »; Vous pouvez d’ores et déjà trouver de nombreux exemples utilisant Q sur Internet.
Rien de compliqué ici, mais cela reste indispensable :
# npm install -g promise var Promise = require('promise')
Vous avez 3 possibilités pour créer des promesses à l’aide de cette libraire :
Prends n’importe quelle valeur en paramètre et retourne une promesse.
Prends un tableau ou un nombre variables d’arguments (pouvant être eux même des promesses) et retourne
une promesse.
Prends en paramètre une fonction, possédant comme dernier argument un callback de type Node.js, et retourne une fonction renvoyant une promesse. Cette fonction « magique » va nous permettre d’utiliser les promesses très facilement de partout !
Une librairie de promesse doit implémenter une méthode then, qui prend 2 arguments :
Une fois votre promesse obtenue, nous pouvons utiliser la méthode then ou son alternativedone.
Prends en paramètre deux fonctions de callback. Une seule des deux fonctions sera appelée, et une seule fois Si la promesse a été « réalisée« , la première fonction onFulfilledsera appelée. Si la promesse a été « rejetée« , la seconde fonction onRejected sera appelée.
De plus, cette méthode renvoit elle même une promesse, ce qui permet de chainer des appels dethen.
Cette fonction est similaire à la méthode then mais diffère car elle ne renvoit pas de promesse mais renvoit « undefined ». De plus, toutes les exceptions déjà lancées seront relancées, afin quelles puissent être enregistrées.
Par soucis de clarté, j’ai choisi de prefixer par un « p » les fonctions renvoyant des promesses. Un peu de code vaut mieux qu’un long discours :
// Instanciation des modules var fs = require('fs') var Promise = require('promise') /* Conversion des fonctions utilisant un callback en fonctions renvoyant une promesse */ var pReadFile = Promise.denodeify(fs.readFile) var pWriteFile = Promise.denodeify(fs.writeFile) var pChmod = Promise.denodeify(fs.chmod) var pChown = Promise.denodeify(fs.chown) var pStat = Promise.denodeify(fs.stat) // Lecture du fichier var p1 = pReadFile("mon_fichier"); // Ecriture du fichier s'il n'existe pas var p2 = p1.then(function pReadFileFulfilled(data) { throw new Error('FILE_EXISTS'); }, function pReadFileRejected(err) { // Le fichier n'existe pas, créons-le ! if (err.message === "ENOENT") return pWriteFile("mon_fichier", "123"); else throw err; }); // Changement des droits d'accès fu fichier var p3 = p2.then(function pWriteFileFulfilled() { return pChmod("mon_fichier", "755"); }, function pWriteFileRejected(err) { throw err; }); // Changement du propriétaire du fichier var p4 = p3.then(function pChmodFulfilled() { return pChown("mon_fichier", 501, 20); }, function pChmodRejected(err) { throw err; }); // Récupération des stats du fichier var p5 = p4.then(function pChownFulfilled() { return pStat("mon_fichier"); }, function pChownRejected(err) { /* Dans le cas ou la promesse a été rejetée avec comme message d'erreur FILE_EXISTS. Nous pouvons appelée pStat pour quand même récupérer les stats du fichiers */ if (err.message === "FILE_EXISTS") return pStat("mon_fichier"); else throw err; }); p5.done(function pStatFulfilled(stats) { // Faire quelque chose avec "stats" console.log("stats", stats); }, function pStatRejected(err) { // Les erreurs seront remontées jusqu'à la fonction finale "promise.done()" console.log("err", err); });
Voilà à quoi ressemblerait la pile d’exécution dans les deux cas possibles :
Bien que le code soit légèrement plus long, 55 lignes (avec commentaires) contre 19 auparavant, il est désormais plus compréhensible, plus clair et plus adaptable.
Ce n’est pas plus compliqué d’utiliser les promesses. Foncez pour une refonte de votre code !