Programmer en Node revient à programmer en Javascript. Node permet juste d’exécuter du code Javascript en dehors d’un navigateur. Voici par exemple un programme node :
const x = 3;
console.log('valeur de x:', x);
Pour exécuter le programme ci-dessus, tapez dans une console :
node n1.js
ce qui donne le résultat suivant :
valeur de x: 3
Sur votre machine, créez un répertoire backend_node
dans lequel vous placerez tous
vos fichiers node. Dans celui-ci, vous allez installer des packages npm et il faut
donc initialiser npm. Pour cela, dans votre répertoire backend_node
, tapez la
commande suivante :
npm init
Vous pouvez taper sur la touche Entrée pour chaque question qui vous est posée (à moins que vous ne souhaitiez décrire plus précisément votre projet).
La commande ci-dessus va créer le fichier package.json
qui indiquera les packages
npm contenus dans votre répertoire ainsi que leurs versions. Vous aviez déjà
rencontré ce fichier quand vous avez créé votre frontend Angular. Les packages npm
seront installés dans un sous-répertoire node_modules
.
Le problème auquel vous allez être confronté est le suivant : vous allez devoir séparer votre programme (votre backend) en plusieurs fichiers et des variables définies dans certains fichiers devront être accessibles d’autres fichiers. Cela nous amène à définir des variables globales.
Pour cela, Node possède un objet global et on peut imaginer que toute variable définie hors d’une fonction ou d’une classe en fait partie. Pour voir si c’est bien le cas, considérons le programme suivant :
// création de 3 variables.
// Rappel : il est préférable d'utiliser let ou const plutôt que var
let xxLet = 3;
var xxVar = 4;
xxRien = 5;
function xxFunc() { return True; }
// affichage de l'objet global de node
console.log(global);
Exécutons le programme et filtrons sur ce qui contient la chaîne xx :
node myvars.js | grep xx
On obtient alors :
xxRien: 5
On constate donc que :
les fonctions ne font pas partie de l’objet global;
les variables créées avec let, var ou const n’en font pas partie non plus.
La solution, pallier cela, consiste à exploiter la notion de module.
Grosso modo, on peut considérer qu’en Node un module correspond à un fichier.
Règle
La portée des variables et fonctions que vous définissez dans un fichier fic.js
est le fichier lui-même.
Donc, par défaut, ces variables et fonctions ne seront pas connues des autres fichiers, comme indiqué dans l’exemple ci-dessus.
On peut observer le contenu d’un module, notamment son champ exports qui indique ce qu’il permet aux autres fichiers de connaître de lui :
console.log(module);
{
id: '.',
path: '/home/gonzales/enseignement/projdev2-24-25/prog',
exports: {},
filename: '/home/gonzales/enseignement/projdev2-24-25/prog/module_view.js',
loaded: false,
children: [],
paths: [
'/home/gonzales/enseignement/projdev2-24-25/prog/node_modules',
'/home/gonzales/enseignement/projdev2-24-25/node_modules',
'/home/gonzales/enseignement/node_modules',
'/home/gonzales/node_modules',
'/home/node_modules',
'/node_modules'
],
[Symbol(kIsMainSymbol)]: true,
[Symbol(kIsCachedByESMLoader)]: false,
[Symbol(kIsExecuting)]: true
}
Pour rendre visible une variable ou une fonction, il suffit de l’exporter, en la rajoutant à la propriété exports du module, comme indiqué ci-dessous. On peut modifier le nom de ce qui est exporté, mais cela n’est pas forcément souhaitable :
function maFonction(x) { return x+1; }
module.exports.maFonction = maFonction; // même nom
module.exports.maFonctionExportee = maFonction; // autre nom
let maVar1 = 1; module.exports.maVar1 = maVar1;
var maVar2 = 1; module.exports.maVar2 = maVar2;
const maVar3 = 1; module.exports.maVar3 = maVar3;
console.log(module);
{
id: '.',
path: '/home/gonzales/enseignement/projdev2-24-25/prog',
exports: {
maFonction: [Function: maFonction],
maFonctionExportee: [Function: maFonction],
maVar1: 1,
maVar2: 1,
maVar3: 1
},
filename: '/home/gonzales/enseignement/projdev2-24-25/prog/exportation.js',
loaded: false,
children: [],
paths: [
'/home/gonzales/enseignement/projdev2-24-25/prog/node_modules',
'/home/gonzales/enseignement/projdev2-24-25/node_modules',
'/home/gonzales/enseignement/node_modules',
'/home/gonzales/node_modules',
'/home/node_modules',
'/node_modules'
],
[Symbol(kIsMainSymbol)]: true,
[Symbol(kIsCachedByESMLoader)]: false,
[Symbol(kIsExecuting)]: true
}
Comme en PHP, le mot clef pour importer du code d’un autre fichier est require (ici, il n’y a pas de require_once, les require ne sont exécutés qu’une seule fois). Dans l’exemple ci-dessous, on récupère ainsi un objet qui contient précisément ce qui a été exporté. En principe, il est d’usage de donner à cet objet le même nom que le module importé. Ci-dessous, on crée l’objet en const afin d’éviter qu’on puisse le modifier par inadvertence.
const exportation = require('./exportation');
console.log("========================");
console.log(exportation);
console.log('appel fonction:', exportation.maFonction(exportation.maVar1));
{
id: '/home/gonzales/enseignement/projdev2-24-25/prog/exportation.js',
path: '/home/gonzales/enseignement/projdev2-24-25/prog',
exports: {
maFonction: [Function: maFonction],
maFonctionExportee: [Function: maFonction],
maVar1: 1,
maVar2: 1,
maVar3: 1
},
filename: '/home/gonzales/enseignement/projdev2-24-25/prog/exportation.js',
loaded: false,
children: [],
paths: [
'/home/gonzales/enseignement/projdev2-24-25/prog/node_modules',
'/home/gonzales/enseignement/projdev2-24-25/node_modules',
'/home/gonzales/enseignement/node_modules',
'/home/gonzales/node_modules',
'/home/node_modules',
'/node_modules'
],
[Symbol(kIsMainSymbol)]: false,
[Symbol(kIsCachedByESMLoader)]: false,
[Symbol(kIsExecuting)]: true
}
========================
{
maFonction: [Function: maFonction],
maFonctionExportee: [Function: maFonction],
maVar1: 1,
maVar2: 1,
maVar3: 1
}
appel fonction: 2
Notez que l’on n’indique pas l’extension .js
du fichier importé et que, pour
l’importation des fichiers « locaux », c’est-à-dire ceux qui ne sont pas des packages
node installés, on préfixe le nom du fichier par le répertoire courant « ./ ».
L’affichage de la console ci-dessus montre que lorsque l’on require un fichier,
on exécute toutes ses instructions (cf. les lignes avant
la démarcation « ======================== »).
Node comprend de nombreux modules (cf. la documentation de Node). La plupart de leurs fonctions sont fournies en deux modes : synchones et asynchrones.
Le programme suivant permet d’afficher la liste des fichiers du répertoire courant. Dans la variable files, on récupère cette liste, puis on l’affiche. C’est donc un programme synchrone.
// on importe le package fs pour pouvoir interagir avec le système de fichiers
const fs = require('fs');
// lecture du répertoire courant et stockage de la liste des fichiers
// dans la variable files
const files = fs.readdirSync('.');
console.log(files);
Notez qu’ici, fs est le nom d’un package de node. Quand on l’inclut avec
l’instruction require(), on ne préfixe pas avec « ./ » puisque c’est un
fichier du répertoire node_modules
.
La version asynchrone de ce programme est la suivante :
const fs = require('fs');
fs.readdir('.', function (err, files) {
// arrivé dans cette fonction, soit le readdir s'est bien passé et on a la liste
// des fichiers dans files et err n'est pas défini (null), soit il y a eu une
// erreur et files est null
// => on traite systématiquement les 2 cas :
if (err)
console.log('Erreur :', err);
else
console.log('fichiers :', files);
});
Dans les versions asynchrones, on a systématiquement un 2ème argument qui est une callback. Celle-ci a toujours un 1er paramètre qui permet de gérer les erreurs produites par la fonction asynchrone.
Installez le package npm mysql2
qui vous permettra de requêter votre serveur
MySQL à partir de Node.
L’utilisation de mysql2
est assez similaire à celle de PDO.
Pour se connecter, il faut utiliser la fonction createConnection() :
const mysql = require('mysql2');
const db = mysql.createConnection({ // connexion à la BD. Cela produit une
host : 'localhost', // instance qui sera utilisée pour les
user : 'polytech', // queries.
password : 'polytech', // A noter que cet objet continuera d'exister
database : 'polytech' // jusqu'à ce qu'on applique sa méthode end()
});
db.connect(); // réalisation de la connexion à la base de données
Une fois la connexion réalisée, comme dans PDO, dans les requêtes SQL, on ne fournit pas directement les valeurs de certains paramètres mais plutôt des ?. La différence principale avec PDO, c’est que les requêtes sont asynchrones. Ainsi, la fonction query() prend en arguments 3 paramètres :
la commande SQL à exécuter, mais dans laquelle les valeurs communiquées par l’utilisateur sont remplacées par des ? ;
un tableau contenant les valeurs correspondant aux ?, dans le même ordre que les ? ;
une callback (fonction) qui indique ce qu’il faudra faire quand le serveur SQL aura transmis sa réponse. Cette callback possède 2 arguments : le 1er correspond à l’erreur qui s’est produite (si le serveur SQL renvoie une erreur) et le 2ème contient le résultat de la requête si elle s’est bien passée.
db.query( // on exécute une requête SQL
'SELECT * FROM courses WHERE id <= ?', // même syntaxe qu'en PDO !
[2], // ici, ce sont les data de PDO
(error, results) => { // les requêtes sont réalisées de manière
if (error) console.log(error); // asynchrone => on récupère le résultat
else console.log(results); // via une callback, permettant de gérer
}); // également les erreurs
Notez que la connexion à la base de données est effective jusqu’à ce qu’on exécute db.end(). Pourquoi est-ce important ? Parce que, comme les db.query() sont asynchrones, tant que vous n’exécutez pas db.end(), votre programme ne peut pas se terminer.
Le programme complet d’interrogation de la base de données est alors :
const mysql = require('mysql2');
const db = mysql.createConnection({ // connexion à la BD. Cela produit une
host : 'localhost', // instance qui sera utilisée pour les
user : 'polytech', // queries.
password : 'polytech', // A noter que cet objet continuera d'exister
database : 'polytech' // jusqu'à ce qu'on applique sa méthode end()
});
db.connect(); // réalisation de la connexion à la base de données
db.query( // on exécute une requête SQL
'SELECT * FROM courses WHERE id <= ?', // même syntaxe qu'en PDO !
[2], // ici, ce sont les data de PDO
(error, results) => { // les requêtes sont réalisées de manière
if (error) console.log(error); // asynchrone => on récupère le résultat
else console.log(results); // via une callback, permettant de gérer
}); // également les erreurs
console.log('les queries sont asynchrones'); // ceci s'affichera avant les résultats
db.end(); // afin de terminer le programme, il faut arrêter l'attente de l'objet db
Lorsque l’on exécute le script ci-dessus, on obtient un affichage similaire à :
les queries sont asynchrones
[
{ id: 1, nom: 'applis web et mobiles', nbEtuds: 45 },
{ id: 2, nom: 'robotique', nbEtuds: 20 }
]
Notez que le message indiquant que les queries sont asynchrones est écrit avant le tableau contenant les données de la base de données, alors même que l’instruction db.query() a été exécutée avant le console.log(), ce qui prouve bien que les requêtes SQL sont asynchrones.
L’avantage des requêtes asynchrones réside dans le fait qu’elles ne font pas perdre de temps au backend : pendant que l’on attend le résultat d’une requête SQL, le backend s’occupe des autres clients qui lui sont connectés. L’inconvénient majeur c’est que, quand on a plusieurs requêtes à exécuter séquentiellement, on a des callbacks imbriquées les unes dans les autres, ce qui devient rapidement pénible à lire.
En Angular, vous avez vu les Observables, qui permettaient, dans une requête asynchrone, de retourner immédiatement un objet, une coquille vide qui, à terme, contiendrait le résultat de la requête. En Javascript, il existe un autre mécanisme relativement similaire : les promesses (Promise en anglais).
L’idée va alors être d’encapsuler la requête SQL dans une promesse et d’inclure le
tout dans une fonction. Exécuter cette fonction revient à exécuter la requête. On
récupère alors immédiatement une Promise, qui est une coquille vide. Comme pour
les Observables, on a un mécanisme qui permet, quand on a la réponse, de
l’exploiter : ce n’est plus un subscribe() mais un then. Voici donc une
première amélioration du code de notre fichier mysql.js
:
const mysql = require('mysql2');
const db = mysql.createConnection({ // connexion à la BD. Cela produit une
host : 'localhost', // instance qui sera utilisée pour les
user : 'polytech', // queries.
password : 'polytech', // A noter que cet objet continuera d'exister
database : 'polytech' // jusqu'à ce qu'on applique sa méthode end()
});
db.connect(); // réalisation de la connexion à la base de données
function myQuery() {
// on retourne tout de suite une Promesse, sans attendre la réponse du serveur
return new Promise((resolve, reject) => {
db.query(
'SELECT * FROM courses WHERE id <= ?',
[2],
(error, results) => { // quand on a la réponse du serveur Mysql,
if (error) reject(error); // on rejette la promesse en cas d'erreur ou
else resolve(results); // on la résout si tout est OK
});
});
}
// l'argument du then est une fonction exécutée dès que la promesse est résolue,
// autrement dit, dès que l'on a la réponse du serveur Mysql
myQuery().then((results) => { console.log(results); });
db.end();
Note : Avec PDO, vous faisiez des fetchColumn, fetch ou fetchAll selon que vous souhaitiez obtenir juste une valeur, un vecteur ou une matrice. Ici, dans tous les cas, vous n’utilisez que db.query. Donc, il faut être vigilant à ce que vous « retournez » via le resolve. Imaginons que, dans le code ci-dessus, on savait qu’il n’y avait qu’un seul cours dont l’id était inférieur à une valeur donnée et que myQuery devait juste retourner l’id de ce cours. Dans ce cas, results doit être égal à quelque chose comme :
[ { id: 1, nom: 'applis web et mobiles', nbEtuds: 45 } ]
Et si l’on souhaite que ce soit l’id de 1 qui est retourné, on doit donc écrire :
function myQuery() {
// on retourne tout de suite une Promesse, sans attendre la réponse du serveur
return new Promise((resolve, reject) => {
db.query(
'SELECT * FROM courses WHERE id <= ?',
[2],
(error, results) => {
if (error) reject(error);
else {
if (results.length != 1) reject(results);
else resolve(results[0].id);
}
});
});
}
Astuce
Afin de bien déterminer ce que vous devez mettre dans le resolve, je vous suggère fortement d’exécuter systématiquement juste avant le resolve() un console.log(results). Ainsi, en fonction de ce que vous verrez dans la console, vous pourrez adapter le code à l’intérieur du resolve(). Quand vous êtes satisfait de ce que retourne la promise, vous pouvez supprimer le console.log(results).
Pour l’instant, on n’a pas gagné grand chose car à utiliser des Promises, on a juste déplacé le problème d’attente du retour du serveur SQL de db.query à myQuery.then. Cela dit, on a gagné sur un aspect : la fonction myQuery ne mélange pas l’appel au serveur SQL et la logique de mon application (le console.log()).
Mais l’avantage d’exploiter les Promises va apparaître quand on va les combiner avec le mot-clef await. Grâce à celui-ci, quand une instruction va récupérer une Promise, Javascript va attendre que la coquille vide de la Promise soit remplie avant de passer à l’instruction suivante. On aura donc, apparemment, un comportement séquentiel. Cela dit, en interne, Node ne va pas bêtement attendre, il va s’occuper des autres clients tant que la Promise n’aura pas été résolue. On aura donc, ainsi, un code qui s’apparente à un code séquentiel mais qui, en réalité, est un code asynchrone.
const mysql = require('mysql2');
const db = mysql.createConnection({ // connexion à la BD. Cela produit une
host : 'localhost', // instance qui sera utilisée pour les
user : 'polytech', // queries.
password : 'polytech', // A noter que cet objet continuera d'exister
database : 'polytech' // jusqu'à ce qu'on applique sa méthode end()
});
db.connect(); // réalisation de la connexion à la base de données
function myQuery() {
return new Promise((resolve, reject) => {
db.query(
'SELECT * FROM courses WHERE id <= ?',
[2],
(error, results) => { // quand on a la réponse du serveur Mysql,
if (error) reject(error); // on rejette la promesse en cas d'erreur ou
else resolve(results); // on la résout si tout est OK
});
});
}
// conserver les avantages de l'asynchronie en ayant un code apparemment séquentiel :
async function getResult() { // async permet d'utiliser await: await attend la
const result = await myQuery(); // la résolution de la promesse. Elle prend alors
console.log(result); // la valeur passée en argument du "resolve" et la
} // place dans result. Ensuite, seulement, on passe
// à l'instruction console.log
getResult(); // on appelle notre fonction pour interroger la base de données
db.end();
Notez que la fonction getResult() doit être indiquée comme étant asynchrone
(mot-clef async) afin de pouvoir utiliser le mot-clef await. Ici,
la fonction getResult() représente le code du backend qui interagit avec le
frontend (le code des fichiers checkLogin.php
, getCourses.php
, etc.).
Règle
À l’instar de votre backend PHP, dans le backend Node, toute requête SQL doit être contenue à l’intérieur d’une fonction. De plus, en Node, cette fonction renvoie une Promise.
Lorsque le code du backend qui interagit avec le frontend exécute ces fonctions, il utilise le mot-clef await pour attendre la réponse du serveur SQL.
Le mot-clef async induit plus de choses qu’on ne pourrait le croire. Dans l’exemple ci-dessous, la fonction myFunc renvoie directement la valeur 3, donc on pourrait légitimement penser que son type de retour est number. En fait, c’est une Promise parce que la fonction est préfixée du mot-clef async :
async function myFunc() {
return 3;
}
// async => toute valeur de retour est transformée en promesse
const x = myFunc();
console.log('x =', x);
async function bof() {
if (myFunc() === 3) console.log('pas ok');
if (await myFunc() === 3) console.log('ok');
}
bof();
x = Promise { 3 }
ok
Normalement, si vous avez bien écrit votre backend PHP, toutes les requêtes SQL
doivent se trouver dans un répertoire spécifique (que vous avez dû appeler
mySQL
, database
ou quelque chose d’approchant). Créez dans votre répertoire
backend_node
un répertoire avec le même nom et traduisez les fichiers de
du répertoire PHP en Node. Je vous suggère de garder les mêmes noms de
fichiers (en remplaçant l’extension .php
par .js
) et les mêmes noms de
fonctions.
Notez que, comme en PHP, rien ne vous interdit de créer un fichier config.js
qui contient toute la configuration de votre backend.
Créez un fichier test.js
pour tester vos fonctions de requêtes SQL au fur et à
mesure que vous les écrivez. Pour cela, le plus simple c’est d’appeler vos fonctions
en leur passant en arguments les mêmes valeurs que ce qu’auraient fait les codes PHP
des fichiers checkLogin.php
, getCourses.php
, etc.
Voici quelques références qui pourraient vous être utiles :
La documentation de mysql2 : https://www.npmjs.com/package/mysql2
Comment récupérer l’ID du dernier élément inséré dans une table : https://github.com/mysqljs/mysql#getting-the-id-of-an-inserted-row
Si vous voulez comprendre l’idée sous-jacente à l’utilisation des await et des Promises, les deux URL suivantes pourront vous être utiles :