Concrètement, les promesses vont permettre plusieurs choses :
- Ne plus se perdre dans les callbacks imbriqués
- Pouvoir faire des traitements asynchrones de manière simultanée tout en récupérant les résultats une seule fois simplement
Par exemple, si vous souhaitez lire plusieurs fichiers JSON avec Node.js, mais que vous souhaitez les traiter en même temps, avant vous auriez fait quelque chose comme ça :
var fs = require('fs'); // On charge le module filesystem classique
var files = ['fichier-1.json', 'fichiers-2.json', 'fichiers-3.json', 'fichiers-4.json'];
var filesResults = [];
try{
files.forEach(function (fileName, index) {
fs.readFile('dossier/' + fileName, { encoding: 'utf8' }, function (err, fileContent) {
if (!err || !fileContent) {
throw 'Fichier illisible';
}
var fileJson = JSON.parse(fileContent);
filesResults[index] = fileJson;
if (filesResults.length === files.length) { // On regarde si tous les fichiers ont été lus.
filesResults.forEach(function (fileJson, index) {
console.log('Contenu du fichier ' + index);
console.dir(fileJson);
});
}
});
});
}
catch (Exception err) {
console.error('Erreur lors de la lecture d\'un fichier');
}
En lisant le tutoriel, vous verrez qu’avec les promesses le code deviendra beaucoup plus clair, par exemple en passant de 5 niveaux d’indentation à seulement 2. Le code sera donc plus léger et on évitera de mélanger la lecture des fichiers avec la condition qui détermine s’ils ont tous été lus.
Mais ce n’est pas tout ! Vous verrez aussi que l’on peut faire des choses assez puissantes avec les requêtes, tout en gardant un code propre et agréable à lire.
Il y a quelques années, quand j’ai rédigé la première version de ce tutoriel, il était souvent nécessaire d’utiliser un polyfill pour importer la class Promise
. Ce n’est aujourd’hui que rarement le cas. Je vous laisse donc décider de la marche à suivre si besoin…
Créer notre propre Promise
Pour mieux comprendre comment une promesse fonctionne, le plus simple est d’en créer une. C’est en plus une base qui pourra vous servir assez régulièrement.
Pour l’exercice nous allons donc travailler côté client en apprenant à charger un fichier distant.
Créons une promesse
function loadDistantFile (url) {
return new Promise(function (resolve, reject) {
});
}
La fonction ne fait pas encore grand-chose, mais vous pouvez déjà voir qu’elle renvoie une promesse. Et vous pouvez aussi apercevoir deux variables — resolve
et reject
— qui vont permettre de déterminer si la promesse est résolue (le boulot a été fait sans accroc) ou si elle a échoué (un problème est survenu).
Une requête basique
function loadDistantFile (url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.onload = function (event) {
resolve(xhr.responseText); // Si la requête réussit, on résout la promesse en indiquant le contenu du fichier
};
xhr.onerror = function (err) {
reject(err); // Si la requête échoue, on rejette la promesse en envoyant les infos de l'erreur
}
xhr.open('GET', url, true);
xhr.send(null);
});
}
Utiliser la requête
Pour utiliser une promesse, il n’y a rien de plus simple : il y a deux méthodes then
et catch
pour gérer les possibilités :
loadDistantFile('test.txt').then(function (content) {
console.info('Fichier chargé !');
console.log(content);
}).catch(function (err) {
console.error('Erreur !');
console.dir(err);
});
Sachez que vous pouvez aussi n’utiliser que la méthode then
en lui passant deux paramètres. Le second paramètre sera alors la fonction à appeler en cas d’erreur :
loadDistantFile('test.txt').then(function (content) {
console.info('Fichier chargé !');
console.log(content);
}, function (err) {
console.error('Erreur !');
console.dir(err);
});
Enchaîner les traitements
L’un des avantages des promesses est de pouvoir enchaîner les traitements. La méthode then
est très utile dans ce cas, car elle renvoie une nouvelle promesse.
On peut donc très bien écrire le contenu de notre fichier dans un autre :
loadDistantFile('test.txt').then(function (data) {
return new Promise(function (resolve, reject) {
fs.writeFile('test-bis.txt', data, function (err) {
if (err) {
reject('Impossible d\'écrire dans le second fichier');
return;
}
resolve(data);
});
});
}).then(function (data) {
console.info('Le contenu du premier fichier a été écrit dans le second');
}).catch(function (err) {
console.error(err);
});
On peut aussi charger des fichiers les uns après les autres de la même manière :
loadDistantFile('test.txt').then(function (data) {
// La variable data correspond ici au contenu du premier fichier
return loadDistantFile('test-2.txt'); // On retourne donc une nouvelle promesse
}).then(function (data) {
// La variable data correspond donc au contenu du second fichier
console.info('Le contenu du second fichier a été chargé');
}).catch(function (err) {
console.error(err);
});
Mais les promesses ne se cantonnent pas aux traitements asynchrones ! Vous pouvez très bien les utiliser pour des traitements synchrones.
Par exemple, en reprenant notre requête précédente, on peut aussi tout simplement parser du JSON :
loadDistantFile('text.json').then(JSON.parse).then(function (data) {
console.dir(data); // On envoie notre JSON déjà parsé dans la console
}).catch(function (err) {
console.error(err); // Oups !
});
Gérer des traitement simultanés
Chose promise, chose due !
J’ai résolu pour vous la promesse faite dans l’introduction du tutoriel !
var fsp = require('fs-promise'); // On charge le module filesystem dans sa version à base de promesses (il s'agit d'un module npm indépendant, attention à ne pas vous mélanger les pinceaux)
var files = ['fichier-1.json', 'fichiers-2.json', 'fichiers-3.json', 'fichiers-4.json'];
var promesses = [];
files.forEach(function (fileName) {
var ma_promesse = fsp.readFile('dossier/' + fileName, { encoding: 'utf8' }).then(JSON.parse); // On demande une promesse sur la lecture du fichier
promesses.push(ma_promesse);
});
Promise.all(promesses).then(function (data) {
console.info('Tous les fichiers ont été lus avec succès');
data.forEach(function (fileJson, index) {
console.log('Contenu du fichier ' + index);
console.dir(fileJson);
});
}).catch(function (err) {
console.error('Une erreur est survenue lors de la lecture des fichiers');
});
Comment ça marche ?
Si vous lisez le code, vous verrez que je n’utilise pas new Promise()
mais Promise.all(promesses)
. Cette fonction renvoie en réalité une promesse qui ne sera résolue que lorsque toutes les promesses passées en paramètre (qui doit être un itérable, par exemple un tableau) sont elles-mêmes résolues, et qui échoue lorsque l’une d’elles (peu importe laquelle) échoue.
Faisons la course !
Maintenant que vous savez gérer des traitements simultanés, nous allons voir comment gérer une situation assez similaire mais pour laquelle le comportement diffère légèrement : la course.
Imaginons par exemple que vous cherchiez à savoir quel script répond le plus vite à une requête. On se fiche donc un peu du résultat des serveurs les plus lents : on veut celui du plus rapide.
On prendra donc pour l’exemple des fichiers PHP qui attendent tous un temps différent (via la fonction sleep
ou usleep
par exemple) puis renvoient leur nom. Par exemple :
<?php
sleep(2); // J'attends 2 secondes (à adapter pour chaque script)
echo 'Numéro 1 !'; // Je dis mon nom
Ensuite en JavaScript je leur demande de faire la course :
var fsp = require('fs-promise'); // On charge le module filesystem dans sa version à base de promesses (il s'agit d'un module npm indépendant, attention à ne pas vous mélanger les pinceaux)
var scripts = ['script-1.php', 'script-2.php', 'script-3.php', 'script-4.php'];
var promesses = [];
scripts.forEach(function (scriptName) {
var ma_promesse = loadDistantFile('scripts/' + scriptName);
promesses.push(ma_promesse);
});
Promise.race(promesses).then(function (resultat) {
console.info('On a un gagnant !');
console.log(resultat);
}).catch(function (err) {
console.error('Une erreur est survenue lors de l\'accès aux scripts');
});
Et voilà ! Vous savez maintenant tout (ou presque) sur les promesses en JavaScript !
N’hésitez pas à tester et à créer vos propres ressources à base de promesses.
Et si vous cherchez des exemples d’implémentations, la fonction fetch
est un très bon exemple !
Vous pouvez retrouver la version originale de ce tutoriel, rédigée en mars 2015, sur Zeste de Savoir.