Avant l’arrivée de Node.js, les développeurs Web écrivaient du code JavaScript pour le Navigateur, ce qu’on appelle aujourd’hui le développement FrontEnd, et ils utilisaient un autre langage pour la partie BackEnd, c’est à dire le code qu’on exécute du côté serveur de l’application.
Le développement BackEnd a longtemps été dominé par des langages orientés objets comme Java JEE, ASP.NET ou PHP. Mais avec Node.js, pour la première fois il a été possible d’utiliser le même langage, le JavaScript, pour la partie BackEnd et la partie FrontEnd.
Node.js n’est donc pas vraiment un langage, mais plutôt un environnement d’exécution permettant de coder en JavaScript en dehors d’un navigateur Web. Il est open-source, multi-plateforme et dispose d’une communauté forte qui a fait de cet outil l’un des plus utilisé ces dernières années.
Architecture
Node.js s’appuie sur le moteur d’exécution Javascript V8 de Google, le même que celui du navigateur Chrome. Node.js utilise une architecture single-thread dite « non bloquante » basée sur une boucle d’événement également appelée l’Event Loop.
Imaginez que des personnes effectuent plusieurs appels vers votre serveur Web sous Node.js. Node.js va alors positionner les demandes dans une file d’attente qu’on appelle aussi l’Event Queue. C’est l’Event Loop qui est en charge de surveiller cette file. Il va traiter les requêtes en renvoyant par exemple de l’HTML ou du JSON au navigateur.
Par contre, si une requête nécessite une opération dite « bloquante » comme la lecture d’une base de données, l’Event Loop va lui affecter un thread du pool de thread. Ce pool, également appelé Worker Pool, est géré par la librairie Libuv spécialement développée pour Node.js. Libuv fourni 4 threads par défaut, mais cette valeur peut être augmentée via les variables d’environnement de Node.js.
Pendant que le thread est occupé à attendre le retour de la base de données, l’Event Loop continu de suivre les autres opérations non bloquantes et les retourne au client après traitement. Une fois l’opération bloquante terminée, elle est remise dans la file d’attente pour traitement par l’Event Loop.
Cette architecture s’avère beaucoup plus efficace pour un serveur web requérant le traitement rapide d’un grand nombre de requêtes ; et est idéal pour des applications en temps réel. Mais tout ce mécanisme reste assez transparent pour un développeur Web qui écrit pour Node.js. Contrairement aux architectures multithreads, en Node.js le développeur n’a pratiquement jamais à s’occuper d’optimiser le pool d’exécution, qui effectue le travail pour lui.
Ce qui a popularisé Node.js auprès des développeurs Web c’est cette faculté de pourvoir développer en JavaScript en dehors du navigateur. Les développeurs ont ainsi été en capacité de produire des librairies et des Framework en JavaScript et de les distribuer via un outil bien pratique : npm.
npm
npm est un gestionnaire de paquet fourni avec l’installation de Node.js. Je vous invite d’ailleurs à regarder la vidéo dédiée à cette installation.
npm automatise la gestion des dépendances d’un projet JavaScript. Il télécharge, installe et met à jour les paquets de votre projet. Par paquet j’entends les modules, les librairies ou les Framework JavaScript disponibles depuis un dépôt en ligne consultable sur npmjs.com. Pas loin de 2 millions de paquets accessibles gratuitement vous attendent, ce qui en fait le gestionnaire de paquet le plus populaire.
CLI
Nous avons donc Node.js pour exécuter du JavaScript et npm pour télécharger des librairies, mais concrètement comment ça marche ? Node.js et npm s’exécutent depuis une CLI (pour Interface en Ligne de Commande). Une fois votre installation de node.js terminée, vous serez en capacité de lancer des commandes depuis un outil comme Powershell, Git Bash ou toute autre invite de commande Shell installée sur votre machine.
Prenons un exemple concret. Depuis un fichier JavaScript que nous nommerons helloworld.js
nous allons ajouter un retour dans le terminal de l’invite de commande qui exécutera le fichier.
console.log("Hello world !");
Puis, depuis l’invite de commande, nous allons exécuter notre fichier JavaScript ce qui aura pour effet d’avoir le résultat suivant :
node helloworld.js
Hello world !
Grâce à Node.js, on vient d’exécuter en ligne de commande du JavaScript standard en dehors d’un navigateur. Essayons maintenant d’améliorer notre message en faisant en sorte de passer un argument via une saisie utilisateur. Pour cela il existe de nombreuses librairies JavaScript, mais pour les besoins de cette démo, nous allons utiliser prompt-sync.
package.json
Voyons maintenant comment utiliser npm pour injecter et utiliser prompt-sync
dans notre projet. La première chose à faire c’est de générer un fichier de configuration qui va répertorier les dépendances que l’on souhaite associer à notre fichier JavaScript. Il suffit de taper la commande :
npm init -y
On vient de créer à côté de notre fichier JavaScript un fichier de configuration nommé package.json
. Maintenant on va importer notre dépendance à prompt-sync
à l’aide de cette commande :
npm install prompt-sync
Plusieurs choses viennent de s’opérer. Déjà, notre fichier package.json
a un nouvel attribut qui regroupe la liste des dépendances au projet. Ici on remarque que la version installée de prompt-sync
est la version 4.2.0 (lors de l’écriture de cet article)
{
"name": "helloworld",
"version": "1.0.0",
"description": "",
"main": "helloworld.js",
"dependencies": {
"prompt-sync": "^4.2.0"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
npm utilise ce qu’on appelle le versionnage sémantique pour répertorier les dépendances. Le premier chiffre [4] correspond à une mise à jour majeure, le deuxième [2] à une mise à jour mineure et le dernier chiffre [0] à un patch correctif. Mais il est tout à fait possible que prompt-sync
soit mis à jour par la suite et on aimerait bien que notre projet bénéfice au moins des patchs, voir des améliorations mineures de la librairie.
Pour cela npm propose une syntaxe d’écriture à appliquer sur le versionnage sémantique des paquets. Vous trouverez une documentation des syntaxes utilisées sur le site semver.npmjs.com. Il propose une interface vous permettant de tester les différentes syntaxes sur la librairie de votre choix.
Par défaut, npm installera la version la plus récente de la liste retournée par la condition. Bien entendu il n’est pas conseillé d’autoriser l’utilisation de plusieurs versions majeures, vous risqueriez des problèmes de compatibilités, voir un crash de votre application.
Si nous avions voulu forcer la version de prompt-sync
il aurait fallu écrire la commande d’installation de cette manière :
npm install prompt-sync@4.1.7
node_modules
L’autre élément important qui a fait son apparition c’est un dossier node_modules
au même niveau que notre fichier. C’est ici qu’est importé et installé notre paquet. On remarque également la présence de deux autres modules. Ce sont les dépendances utilisées par prompt-sync
et installées automatiquement par npm.
Lors de l’exécution du fichier JavaScript, Node.js se chargera d’aller chercher dans ce dossier les objets relatifs à prompt-sync
.
Reste encore à modifier notre fichier helloworld.js
pour utiliser la dépendance. Mais avant de faire ça il y a une dernière chose à ajouter à notre fichier package.json
.
ES6
Node.js est configuré par défaut pour comprendre l’ES5. ES5, pour ECMAScript 5, est un standard de syntaxe pour l’écriture du JavaScript. Mais en 2015, un grand nombre d’ajouts et de changements sont apparus avec ES6 (ou ECMAScript 2015, on utilise les années à partir de cette version pour la numérotation). Il faut donc définir un paramètre dans le fichier package.json
pour forcer Node.js à interpréter le code JavaScript écrit en ES6. Ajoutez simplement l’attribut type
avec comme valeur module
.
{
"name": "helloworld",
"version": "1.0.0",
"description": "",
"main": "helloworld.js",
"type": "module",
"dependencies": {
"prompt-sync": "^4.2.0"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Voilà, maintenant il est possible d’écrire du JavaScript au format ES6 dans Node.js. Il est temps d’importer notre librairie et de modifier notre fichier helloworld.js
pour ajouter une saisie utilisateur.
import PromptSync from "prompt-sync";
const prompt = PromptSync();
const name = prompt("Your name:");
console.log(`Hello ${name} !`);
Reste plus qu’a exécuter le fichier et voir le résultat dans le terminal Shell :
node helloworld.js
Your name:Cyrille
Hello Cyrille !
Comme vous pouvez le voir, non seulement on peut interagir avec les utilisateurs mais il est également possible d’interagir avec le système de fichier de l’OS ce qui est impossible depuis un navigateur, pour des raisons évidentes de sécurité.
Asynchronicité
Bien, passons à la vitesse supérieure et exploitons un peu mieux l’Event Loop de Node.js.
On va simuler des appels asynchrones plus ou moins bloquant vers Node.js. Pour ça nous allons utiliser le paquet sleep-promise
, qui comme son nom l’indique va créer une temporisation et retourner une promesse que nous pourrons exploiter comme réponse d’une opération. N’oubliez pas d’installer le paquet avant :
npm install sleep-promise
Techniquement, sleep-promise
ne fait pas grand chose de plus que d’instancier une promesse et de passer la réponse au bout d’un timeout fourni en paramètre. Si nous devions écrire le code ça reviendrait à quelque chose comme ça :
const p = new Promise((resolve) => setTimeout(resolve(1000), 1000));
p.then(response => console.log(`sleep:${response}ms`);
Mais ici j’ai l’avantage de ne pas avoir à écrire le code moi-même. On va donc simuler plusieurs appels plus ou moins bloquant vers Node.js et voir comment l’Event Loop s’en sort pour traiter les demandes.
import sleep from "sleep-promise";
console.log('Je simule un appel à une BDD.');
sleep(1000).then(() => console.log("Appel à la BDD terminé."));
console.log('Je simule une opération non bloquante.');
sleep(0).then(() => console.log("Opération non bloquante terminée."));
console.log('Je simule une lecture de fichier.');
sleep(500).then(() => console.log("Lecture du fichier terminée."));
console.log('Je simule une opération complexe.');
sleep(100).then(() => console.log("Opération complexe terminée."));
Et si on exécute le code on obtient le résultat suivant dans le terminal :
node helloworld.js
Je simule un appel à une BDD.
Je simule une opération non bloquante.
Je simule une lecture de fichier.
Je simule une opération complexe.
Opération non bloquante terminée.
Lecture du fichier terminée.
Opération complexe terminée.
Appel à la BDD terminé.
On voit que Node.js a lancé les quatre opérations en même temps, ou plutôt quand il a vu qu’elles prendraient du temps, il en a profité pour exécuter ce qui pouvait l’être immédiatement. De plus au lieu de répondre après avoir attendu successivement 1 seconde, puis une demi seconde, puis 100 milliseconde, soit un total de 1,6 secondes, il va effectuer les trois opérations en même temps et être capable de répondre en une seconde, qui correspond à l’opération la plus longue, sans bloquer le résultat des autres opérations.
Async / Await
Il est bien sûr tout à fait possible d’attendre le retour d’une opération avant d’exécuter la suite de notre code, ce qui équivaut a refaire un appel synchrone. Il suffit d’utiliser la syntaxe async
/ await
.
Nous n’aurons pas a utiliser dans notre exemple async
qui a pour rôle de transformer une fonction en fonction asynchrone. Nous en utilisons déjà avec sleep-promise
. Par contre nous allons pouvoir nous servir d’await
pour temporiser et synchroniser le retour des promesses de sleep-promise
.
import sleep from "sleep-promise";
console.log('Je simule un appel à une BDD.');
await sleep(1000).then(() => console.log("Appel à la BDD terminé."));
console.log('Je simule une opération non bloquante.');
sleep(0).then(() => console.log("Opération non bloquante terminée."));
console.log('Je simule une lecture de fichier.');
sleep(500).then(() => console.log("Lecture du fichier terminée."));
console.log('Je simule une opération complexe.');
sleep(100).then(() => console.log("Opération complexe terminée."));
Et le résultat dans le terminal Shell est sans appel :
node helloworld.js
Je simule un appel à une BDD.
Appel à la BDD terminé.
Je simule une opération non bloquante.
Je simule une lecture de fichier.
Je simule une opération complexe.
Opération non bloquante terminée.
Lecture du fichier terminée.
Opération complexe terminée.
Avec await
, Node.js a bien attendu que la première opération (la plus lente) soit complètement finie avant de lancer une nouvelle opération.
Et si vous vous demandez comment on procède avec async
, il suffit de réécrire légèrement le code de l’exemple de cette manière pour effectuer la même opération :
import sleep from "sleep-promise";
async function simuleAccessBDD() {
await sleep(1000).then(() => console.log("Appel à la BDD terminé."));
}
console.log('Je simule un appel à une BDD.');
await simuleAccessBDD();
console.log('Je simule une opération non bloquante.');
sleep(0).then(() => console.log("Opération non bloquante terminée."));
console.log('Je simule une lecture de fichier.');
sleep(500).then(() => console.log("Lecture du fichier terminée."));
console.log('Je simule une opération complexe.');
sleep(100).then(() => console.log("Opération complexe terminée."));
Un conseil pour une application performante. N’utilisez await
que quand vous n’avez pas le choix. Et vous pouvez être certains que si votre appli commence à devenir lente, vous devrez probablement vous débarrasser des await
superflus pour regagner en performance.
Conclusion
Dans cet article nous avons vu pourquoi et comment Node.js s’est imposé comme un composant essentiel de l’écosystème Web. Nous avons parlé de son architecture et vu comment on peut développer en JavaScript en dehors du navigateur en utilisant la syntaxe d’écriture apportée par l’ES6.
Nous avons également abordé l’application de l’Event Loop via des appels asynchrones et vu comment on peut forcer le code à attendre un retour du serveur à l’aide de la syntaxe async
/ await
.
Un grand nombre de Framework JavaScript sont maintenant installables depuis la ligne de commande de Node.js comme Angular ou React. Le développeur Web peut ainsi démarrer un projet très facilement en quelques clics sans se soucier des dépendances et de leur version.
Il existe cependant encore un petit chemin à parcourir pour transformer notre « Helloworld.js » en un vrai serveur Web, mais cela fera l’objet d’un autre article.
Références
- Site officiel de Node.js : https://nodejs.org
- Installation de Node.js (vidéo Youtube sur inpulse.tv) : https://www.youtube.com/watch?v=vGql_816rqs&t=84s
- ECMAScript 2015 (ES6) : https://262.ecma-international.org/6.0/
- Définition du versionnage sémantique : https://semver.org/
- MDN : Faciliter la programmation asynchrone avec async et await : https://developer.mozilla.org/fr/docs/Learn/JavaScript/Asynchronous/Async_await
0 commentaire