version 1.5.0, dernière mise à jour le 13 novembre 2023.
Node.js
est un framework permettant de concevoir des sites programmés en JavaScript
côté serveur. Sa première version date de 2009. Cependant, à la différence de PHP
par exemple, il ne s'agit pas d'un simple langage de script tournant sur un serveur préexistant ; en effet, il faut coder soi-même avec node
les différentes couches du serveur. Il faudra ainsi mettre en place :
le serveur HTTP
lui-même, avec la gestion des différents messages HTPP
le routage des requêtes vers les bonnes pages du serveur
de quoi récupérer les paramètres saisis dans l'URL
un système permettant d'envoyer du contenu au client en fonction de l'URL
un dispositif permettant, en JavaScript, de gérer des accès au système de fichiers pour permettre non seulement la lecture, mais aussi l'écriture et même l'envoi de fichiers par l'utilisateur.
Heureusement, des frameworks spécialisés permettent de simplifier un certain nombre de ces étapes, comme Express.js par exemple.
Node est disponible quelle que soit la plateforme. Une fois téléchargé à partir du site officiel, il suffit pour le lancer de taper node
dans une ligne de commande. Est alors affiché une invite de commande <
. Pour quitter, il suffit de faire Ctrl-c
ou, ce qui est plus conforme à JavaScript
, .exit
.
Pour afficher la chaîne de caractères Bonjour
, il suffit alors d'écrire console.log("Bonjour")
. On peut aussi créer un fichier contenant cette ligne, appelé par exemple salut.js et le lancer en tapant node salut.js
Bien évidemment, en l'état on ne peut pas appeler cela un serveur, qui doit répondre à des requêtes en provenance d'un client. Nous allons complexifier notre code en mettant en place les différentes couches du serveur :
var http = require('http') ;
var monServeur=function(requete, reponse){
reponse.writeHead(200) ;
reponse.end('Bonjour') ;
}
var serveur = http.createServer(monServeur) ;
serveur.listen(8888) ;
Le code précédent appelle quelques commentaires.
require
est un mot-clef qui permet d'importer des modules dans un code JavaScript
. Nous importons ici l'ensemble du module http
, mais nous verrons plus tard comment n'importer qu'une partie d'un module, et comment en créer un.
createServer
est une méthode de http
, qui prend comme paramètre un callback
, c'est-à-dire soit une fonction anonyme (function(){...}
), soit le nom d'une fonction (un peu à la manière d'un gestionnaire d'événement). Cette méthode renvoie une référence à un objet serveur, qui nous pemettra de le paramétrer et le contrôler.
Un tel callback
prend deux arguments : la requête envoyée par l'utilisateur et la réponse à lui renvoyer. Ici, la méthode writeHead
permet d'écrire dans le header HTTP et de renvoyer un code 200 (« tout va bien »), puis la chaîne de caractères Bonjour
est envoyée au client
Il ne reste plus qu'à faire en sorte que le serveur écoute les requêtes, et c'est ce qui est fait à l'aide de la méthode listen
qui prend comme paramètre obligatoire un numéro de port. Nous n'avons pas indiqué ici de paramètre facultatif, et avons fixé ici le port 8888, mais en production, le port par défaut interrogé par les navigateurs est le port 80.
Il suffit alors d'ouvrir dans une fenêtre de navigateur l'URL http://localhost:8888 pour consulter ce contenu.
Le code précédent souffre cependant d'un gros défaut : si l'on remplace la chaîne 'Bonjour' par 'Bonjour à tous', c'est-à-dire une chaîne contenant un diacritique, le navigateur peut ne pas l'afficher correctement en raison d'un problème d'encodage de caractères. Il faut compléter notre entête HTTP. On peut aussi en profiter pour le compléter en spécifiant le type MIME du fichier envoyé en réponse, en écrivant resultat.writeHead(200,{"Content-Type": "text/plain; charset=UTF-8"})
. Dans le cas d'un fichier HTML, on écrira bien entendu resultat.writeHead(200,{"Content-Type": "text/html; charset=UTF-8"})
Le code précédent répond toujours un même contenu à n'importe quelle requête. Cela n'est pas le cas sur les serveurs en pratique, et il est nécessaire de mettre en place un routage, c'est-à-dire des associations entre des URL et des contenus que le serveur doit fournir. On va se servir d'une propriété de l'objet requete
, qui va nous permettre de récupérer des informations sur l'URL. Pour cela, il va falloir importer un autre module de node, le module url
en ajoutant var url = require('url');
. On peut alors accéder à l'URL saisie,
var http = require('http') ;
var url = require('url') ;
var monServeur=function(requete, reponse){
var page = url.parse(requete.url).pathname ;
reponse.writeHead(200,{"Content-Type": "text/plain; charset=UTF-8"}) ;
//D'autres reponse.write...
switch(page){
case '/page1' : (…)break ;
case '/page2' : (…)break ;
case '/page3' : (…)break ;
default : reponse.end('Ce cas ne devrait pas arriver') ;
}
}
var serveur = http.createServer(monServeur) ;
serveur.listen(8888) ;
reponse.end
est une méthode qui permet de publier des données, et qui signale également la fin de l'envoi de ces données. Si le volume de données est important, il est préférable de passer par des reponse.write(donnees)
, qui peuvent être successifs, avant de conclure par un reponse.end
pour terminer la publication. Par exemple…
reponse.write("<p>Ceci est un paragraphe</p><ul><li>premier item (...)") ;
reponse.write("<p>Ceci est un autre paragraphe</p><ul><li>autre liste (...)") ;
reponse.end("</body></html>") ;
Pour le moment, nos codes Node ne sont pas très organisés. Il n'est pas possible de construire un site complexe si on n'introduit pas un minimum de structure. Pour cela, il existe des conventions :
À la racine du projet doit figurer un fichier package.json
, au format standardisé, qui contient des informations relatives à l'application elle-même, à son auteur... mais aussi à ses dépendances.
Les fichiers index.js ne doivent contenir que les imports et exports de modules.
La logique de l'application (les fichiers permettant de définir les méthodes, les contenus...) doit être séparée des fichiers index.js, et rassemblée par thématiques (par exemple un répertoire product, un répertoire user, un répertoire config...)
Les tests doivent être aussi proches que possible des fichiers à tester, mais en fonction du framework de test utilisé, ils seront rassemblés dans un répertoire, ou placés dans le même répertoire que les fichiers à tester.
Ce fichier doit être placé à la racine du projet. En voici un exemple :
{ "name": "nomApplication", "version": "1.0.0", "private": true, "description": "bac à sable", "main": "index.js", "scripts": { "test": "./node_modules/.bin/mocha --reporter spec", "start": "node index.js" }, "author": "G. Chagnon", "license": "ISC", "dependencies": { "chai": "^3.5.0", "express": "^4.14.0", "mocha": "^3.2.0" } }
La plupart des propriétés possèdent des noms explicites. private
sert à indiquer s'il s'agit encore d'un développement en local, pas encore publié en ligne. scripts
indique les instructions possibles avec le gestionnaire de paquets npm
(voir ci-après) : on pourra ici lancer npm start
ou npm test
. Enfin, dependencies
dresse la liste des dépendances de l'application, avec les numéros de version.
Le numéro de version doit obéir à des conventions strictes afin d'éviter les problèmes de compatibilité. Il est écrit sous la forme Majeure.Mineure.Correction :
on incrémente Correction quand on corrige un bug d'une fonctionnalité existante
on incrémente Mineure quand on ajoute une fonctionnalité tout en maintenant la compatibilité avec les versions antérieures : dans ce cas, on remet Correction à 0.
on incrémente Majeure quand on introduit une évolution entraînant une incompatibilité avec les versions antérieures ; dans ce cas, on remet Correction et Mineure à 0.
Nodejs propose un package permettant d'accéder à l'ensemble du système de fichiers, appelé fs
. La documentation de fs est riche mais dans l'immédiat, nous allons nous contenter de l'utiliser pour charger un fichier en réponse à une requête, et non plus seulement une chaîne de caractères.
var http = require('http') ;
var url = require('url') ;
var fs = require('fs') ;
var monServeur=function(requete, reponse){
var page = url.parse(requete.url).pathname ;
reponse.writeHead(200,{"Content-Type": "text/plain; charset=UTF-8"}) ;
//D'autres reponse.write...
switch(page)
case '/page1' : reponse.write(fs.readFileSync(__dirname+'/page1'));break ;
case '/page2' : reponse.write(fs.readFileSync(__dirname+'/page2'));break ;
case '/page3' : reponse.write(fs.readFileSync(__dirname+'/page3'));break ;
default : reponse.end('Ce cas ne devrait pas arriver') ;
}
var serveur = http.createServer(monServeur) ;
serveur.listen(8888) ;
__dirname
est une constante qui désigne le répertoire dans lequel se trouve le fichier index.js (ici app).
Le routage, même s'il est possible, peut devenir assez complexe si le projet prend de l'ampleur. Il est alors plus efficace de recourir à un framework spécialisé. La Fondation Node.js développe ainsi le framework Express qui permet, par exemple de gérer des URL sur la base d'expressions rationnelles. Analysons une application de base utilisant Express :
// app.js
var express = require('express') ;
var app = express() ;
var port = 8888 ;
app.listen(port, function(){
console.log('Le serveur fonctionne sur le port '+port) ;
}
Cette fois-ci, la méthode listen
prend comme premier paramètre le numéro de port à écouter et comme second paramètre un callback qui permet d'afficher un message sur le serveur. Le routage en lui-même peut ensuite s'écrire…
// app.js
var express = require('express') ;
var app = express() ;
var port = 8888 ;
app.listen(port, function(){
console.log('Le serveur fonctionne sur le port '+port) ;
}) ;
app.get('/', function(req, res){
res.send('Bonjour tout le monde') ;
}) ;
L'avantage de cette méthode get
est qu'elle renvoie à son tour un objet similaire ; les appels peuvent donc être chaînés. Par exemple…
app.get('/page1', function(req, res){
res.send('Ceci est la page 1') ;
})
.get('/page2', function(req, res){
res.send('Ceci est la page 2') ;
}) ;
.get('/page3', function(req, res){
res.send('Ceci est la page 3') ;
}) ;
Enfin, Express permet de gérer des routes dynamiques, c'est-à-dire des routes dont certaines parties sont variables. Pour reprendre l'exemple précédent avec page1, page2 et page3, on aurait pu écrire…
app.get('/page:num', function(req, res){
res.send('Ceci est la page '+req.params.num) ;
}) ;
Attention cependant à vérifier a priori ce qui est saisi par l'utilisateur afin d'éviter l'injection de code !
Express permet aussi de gérer facilement les entêtes http, et notamment les réponses du serveur en cas d'erreur, la plus connue étant l'erreur 404 (lorsqu'une ressource ne peut pas être trouvée sur le serveur). Pour cela, à la toute fin du code, juste avant app.listen
, il faut inclure le code suivant :
app.use(function(req, res, next){
//la ligne suivante spécifie l'encodage de la réponse
res.setHeader('Content-Type', 'text/plain; charset=UTF-8') ;
//La ligne suivante spécifie le message à envoyer par le serveur quand la réponse est un code d'erreur 404
res.status(404).send('Page introuvable') ;
})
.listen(…
nodemon (qui s'installe à l'aide de npm, soit globalement avec npm install -g nodemon
ou localement) permet de redémarrer automatiquement le serveur si un des fichiers constitutifs de l'application est modifié, ce qui est très commode en phase de développement. Pour cela, il suffit de remplacer la ligne "start": "node index.js"
par "start": "nodemon index.js"
.
npm
est un « gestionnaire de paquets » pour Node
. Cela signifie qu'il permet de gérer les dépendances, le déploiement de l'application et de simplifier le lancement d'opérations comme celles des tests, des préprocesseurs CSS ou la minification de code. Nous n'aborderons pas dans cette partie toutes les possibilités offertes par ce puissant outil ; il existe de nombreuses ressources à ce sujet. Un autre gestionnaire de paquets, Yarn, existe. Il a été développé et lancée par Facabook en 2016 pour pallier des faiblesses de npm, notamment à l'époque en matière de sécurité, mais depuis lors npm s'est amélioré et sur ce point critique entre autres, les deux gestionnaires sont maintenant équivalents.
npm était installé automatiquement en même temps que node, et ne nécessitant pas d'autre opération, c'est le gestionnaire qui sera utilisé dans ce cours.
npm permet d'initialiser semi-automatiquement le fichier package.json. Pour cela, dans le répertoire consacré à l'application que l'on souhaite réaliser, il suffit de taper npm init
. Le script pose une série de questions pour créer ensuite le fichier package.json, avec des réglages par défaut.
npm permet aussi d'installer des bibliothèques avec la commande npm install nomDeLaBibliothèque
. Par exemple, pour installer React, on écrira npm install react
. Cependant, cette méthode présente l'inconvénient de ne pas embarquer la bibliothèque. Si l'on souhaite pouvoir transposer facilement l'application dans un autre environnement, il faut aussi l'indiquer comme dépendance du projet. Pour cela, on écrira npm install react --save
; cela met à jour le fichier package.json.
Par la suite, si on ne veut pas partager un projet en incluant tous les modules, et donc ne pas distribuer une archive incluant le répertoire node_modules, il suffira de taper sur la machine d'installation la commande npm install
: cela installera toutes les dépendances du projet.
Afin d'illustrer ce qu'il est possible de réaliser avec la combinaison node/npm, nous allons aborder un framework de test unitaire nommé Mocha. Mocha, couplé à ChaiJS, une bibliothèque JavaScript permettant de facilement spécifier des tests dans le cadre d'un Test Driven Development, est un framework très simple d'usage pour lancer automatiquement des jeux de tests unitaires.
Les deux bibliothèques s'installent facilement avec npm.
Avant toute chose, il faut modifier la ligne codant le script à lancer pour la phase de test. Dans le fichier package.json, nous écrirons donc "test": "./node_modules/.bin/mocha --reporter spec",
. L'option reporter
permet de spécifier le mode de reporting des erreurs, ici spec
qui est bien adapté quand on débute avec ce framework.
En développement guidé par les tests, avant d'écrire quelque ligne de code que ce soit, il faut commencer par écrire les tests qui permettront de vérifier le bon comportement des scripts.
Mocha permet facilement de décrire les tests à réaliser, et Chai permet de facilement les écrire. Supposons que l'on doive écrire une application consistant en deux fonctions : ajoute2
qui prend un paramètre et lui ajoute 2, et supprime2
qui prend un paramètre et lui retire 2. Cette application, que l'on va appeler ajoutesupprime
, est initialisée avec le package.json suivant :
{ "name": "ajoutesupprime", "version": "1.0.0", "private": true, "description": "Ajout et soustraction de 2", "main": "index.js", "scripts": { "test": "./node_modules/.bin/mocha --reporter spec", "start": "node index.js" }, "author": "G. Chagnon", "license": "ISC", "dependencies": { "chai": "^3.5.0", "express": "^4.14.0", "mocha": "^3.2.0" } }
À chaque fichier présent dans le répertoire app contenant du code à tester, est associé un fichier du même nom dans le répertoire test :
Dans notre cas, le fichier index.js contient uniquement require ('./app/index.js');
. C'est le fichier qui sera interprété quand sera lancée la commande npm start
. Nous allons créer dans le répertoire test
un fichier portant le même nom que le fichier à tester, ici ajoutesupprime.js. Dans ce fichier, nous allons décrire à l'aide de Mocha les tests à réaliser. Tout d'abord, chapeautons l'ensemble avec les éléments requis :
var ajoutesupprime = require("../app/ajoutesupprime.js") ;
var expect = require ("chai").expect ;
La seconde instruction permet de n'importer que la fonction expect
du module chai
.
Décrivons maintenant l'application d'un point de vue général :
//test/ajoutesupprime.js
var ajoutesupprime = require("../app/ajoutesupprime.js") ;
var expect = require ("chai").expect ;
describe("Ajoute et supprime 2", function(){
//...
}) ;
Complétons avec la description des deux fonctions :
//test/ajoutesupprime.js
var ajoutesupprime = require("../app/ajoutesupprime.js") ;
var expect = require ("chai").expect ;
describe("Ajoute et supprime 2", function(){
describe("Fonction ajoute2", function() {
//...
}) ;
describe("Fonction retire2", function() {
//...
} ;
}) ;
Enfin, en appelant la fonction it
expliquons ce que doit faire la fonction dans une situation donnée (celle du test), et avec la méthode expect
explicitons ce qu'on doit attendre :
//test/ajoutesupprime.js
var ajoutesupprime = require("../app/ajoutesupprime.js") ;
var expect = require ("chai").expect ;
describe("Ajoute et supprime 2", function(){
describe("Fonction ajoute2", function() {
it("ajoute 2 à 8", function() {
var res = ajoutesupprime.ajoute2(8) ;
expect(res).to.equal(10) ;
}) ;
}) ;
describe("Fonction retire2", function() {
it("soustrait 2 à 8", function() {
var res = ajoutesupprime.retire2(8) ;
expect(res).to.equal(6) ;
}) ;
}) ;
}) ;
chai
ne se limite pas à to
et equal
. Il y a beaucoup d'autres méthodes qui permettent de comparer un résultat obtenu à un résultat espéré.
Enfin, on écrit le code des fichiers app/index.js et app/ajoutesupprime.js :
//app/index.js
var http = require('http') ;
var url = require('url') ;
var ajoutesupprime = require('./ajoutesupprime') ;
var monServeur=function(requete, reponse){
var page = url.parse(requete.url).pathname ;
var sortie ;
switch(page) {
case '/ajoute': reponse.writeHead(200,{"Content-Type": "text/plain; charset=UTF-8"}); sortie = "Addition à 5 : "+ajoutesupprime.ajoute2(5);break ;
case '/supprime': reponse.writeHead(200,{"Content-Type": "text/plain; charset=UTF-8"}); sortie = "Soustraction de 5 : "+ajoutesupprime.retire2(5);break ;
defaultresultat.writeHead(404);sortie="Erreur 404"; ;
}
reponse.end(page) ;
}
var serveur = http.createServer(monServeur) ;
serveur.listen(8888) ;
Voici les deux déclarations du fichier ajoutesupprime.js
; nous faisons appel ici à une notion que nous n'avons pas abordée en cours :
//app/ajoutesupprime.js
exports.ajoute2 = function(x){
return x+2 ;
}) ;
exports.retire2 = x => x-2 ;
/*exports.retire2 = function(x) {
return x-2;
}*/
exports
permet d'exporter des objets si le fichier est appelé comme un module, comme c'est le cas ici avec require
. Nous utilisons de plus une fonction fléchée. L'équivalent en utilisant une fonction anonyme est indiqué en commentaire. Il aurait été tout à fait possible de faire de même pour la fonction ajoute2
: exports.ajoute2 = x => (x+2);
Au final donc, depuis la racine de l'application :
le fichier ./package.json permet de définir les commandes pour lancer (npm start
) et tester (npm test
) l'application ;
le fichier ./.index.js appelle le fichier ./app/index.js
le fichier ./app/index.js contient (dans ce cas encore simple) le paramétrage du serveur et du routeur ;
le fichier ./app/ajoutesupprime.js contient la logique de l'application ;
le fichier test/ajoutesupprime.js contient les tests à faire passer au fichier ./app/ajoutesupprime.js.
Ces fondations étant posées, on peut lancer les tests en tapant à la racine de l'application npm test
. Normalement, les deux tests devraient être déroulés et tout devrait bien se passer. En revanche, remplacer x+2
par x+1
produit une erreur : la commande npm start
fonctionne, le serveur ne « plante » pas, mais son comportement incorrect est détecté par npm test
.
Jusqu'à présent, nous nous sommes contentés d'afficher de simples chaînes de caractères en réponse aux requêtes de l'utilisateur. Il est évident qu'un tel fonctionnement est bien loin de celui d'un site Web, constitué de pages HTML
. Cependant, utiliser Express tel quel pour la génération des pages HTML
pose de gros problèmes, en mélangeant code JavaScript et HTML.
Il est tout à fait possible, comme en Java ou en PHP, de recourir à des systèmes de templates. Express est compatible avec la plupart des moteurs de template comme Smarty, Twig, etc. mais il semble logique, dans le cadre de ce cours, de recourir au système EJS qui repose sur la syntaxe JavaScript
.
L'installation se fait en saisissant npm install ejs
.
Tout d'abord, il faut indiquer à node
que le moteur de rendu ne sera pas celui par défaut, mais qu'il faudra au préalable passer par une phase d'interprétation avec EJS. Pour cela, on écrit…
var app = express() ;
app.set ('view engine', 'ejs') ;
Il faut créer, à la racine de l'application, un répertoire s'appelant obligatoirement views. Ensuite, pour chacune des pages vers lesquelles le routeur pourrait être amené à rediriger et pour lesquelles on souhaite recourir au système de template, il faut créer le gabarit de page correspondant dans le répertoire en question, et indiquer que la réponse doit être « rendue » avec le gabarit de page correspondant. Par exemple…
app.get('/page1', function(req, res){
res.render('page.ejs') ;
}) ;
De la même manière qu'on peut passer des paramètres directement à l'aide d'Express, on peut en passer à EJS. Par exemple…
app.get('/page:num', function(req, res){
res.render('page.ejs', {"numero": req.params.num}) ;
}) ;
Les gabarits doivent être placés dans le répertoire views. Dans le gabarit page.ejs, on écrira…
<!DOCTYPE html > <html lang="fr"> <head> <title>Page de test</title> <meta charset="utf-8" /> </head> <body> <p>Ceci est un paragraphe, sur la page <%= numero %>.</p> </body> </html>
<%= numero %>
est remplacé par la valeur de la variable numero qui a été transmise au gabarit via req.params.num
On peut aussi, par exemple, réaliser des boucles…
app.get('/page:num', function(req, res){
res.render('page.ejs', {"numero": req.params.num}) ;
}) ;
<!DOCTYPE html > <html lang="fr"> <head> <title>Page de test</title> <meta charset="utf-8" /> </head> <body> <p>Ceci est un paragraphe, sur la page <%= numero %>.</p> <ul> <% for (let i=0;i<numero;i++){%> <li>Ligne <%= i+1%></li> <%}%> </ul> </body> </html>
À chaque fois que <%=
apparaît, le résultat du calcul est inséré dans le HTML. Le code précédent, en réponse à une requête /page3, renverra donc le code HTML
suivant :
<!DOCTYPE html > <html lang="fr"> <head> <title>Page de test</title> <meta charset="utf-8" /> </head> <body> <p>Ceci est un paragraphe, sur la page 3.</p> <ul> <li>Ligne 1</li> <li>Ligne 2</li> <li>Ligne 3</li> </ul> </body> </html>
L'interface WebSocket permet au navigateur de maintenir une connexion bidirectionnelle avec le serveur. Dans le cas d'Ajax, le client envoie des requêtes au serveur, qui y répond. Websocket permet au serveur d'envoyer des données au client pour que celui-ci les incorpore dynamiquement, sans qu'il ait besoin de les demander, un peu à la manière du data-binding bidirectionnel dans les modèles MVC.
Pour cela, plusieurs bibliothèques existent :
socket.io, sans dépendance et capable en plus de Websocket de gérer les connexions \textit{via} cookie ou répartition de charge. Attention, couteau multi-fonction qui recourt à Websocket si possible mais a d'autres possibilités pour marcher sur les vieux navigateurs
ws pour une implémentation plus « pure » de Websocket
react-use-websocket pour l'intégration avec React
…
Le protocole utilisé est ws
, et l'adresse d'un serveur websocket sera donc de la forme ws://monserveur.com
ou, mieux, avec la version sécurisée du protocole, wss://monserveur.com
.
il faut charger le module socket.io via npm côté serveur, et incorporer le script côté client par <script src="/socket.io/socket.io.js"></script>
. Quelques méthodes sont disponibles :
connect(serveur)
permet de se connecter en http à serveur
(par exemple, en local, var socket=io('http://localhost:8888');
. Cette méthode est disponible côté client.
L'équivalent de connect
est, côté serveur, listen
qui intercepte les requêtes du client.
emit(commande)
qui permet d'émettre une donnée, que ce soit par le serveur ou le client.
on(commande, reponse)
qui, à la réception de la donnée commande
, exécute la reponse
, c'est-à-dire une fonction permettant de traiter ces données, que ce soit par le serveur ou le client.
Analysons le code suivant :
var socket = io('http://localhost:8888'); function notif(contenu){ // Code à exécuter pour traiter l'information de "contenu" } socket.on('notification', function (x) {notif(x)}); function appelServeur(){ console.log("requête envoyée"); socket.emit('appel', {clef: 'valeur'}); }
Ce code commence par ouvrir une connexion vers le serveur. On définit ensuite une fonction, notif
, qui contient le code à exécuter pour traiter l'information qui lui est passée en paramètre (ce peut être l'affichage d'une donnée dans un champ de formulaire par exemple). On indique ensuite avec socket.on
le nom de l'intervention à écouter (ici, les interventions notification
) et le callback à utiliser (ici, une fonction anonyme lançant la fonction notif)
. Enfin, on définit une fonction (que l'on peut par exemple appeler au clic sur un bouton ou à la validation d'un formulaire), qui permet d'envoyer une requête au serveur avec emit
.
En regard du code précédent, on trouve le code suivant côté serveur :
var server = http.createServer(function (req, res) {…}
var socket = require("socket.io"); (…) var listener = socket.listen(server, {log: false}); (…) function start(socket){ socket.emit('notification', "le serveur écoute les sockets"); socket.on('appel', function (){ console.log("Requête reçue"); listener.sockets.emit('notification', "Présent!"); } ); } listener.sockets.on('connection', function (socket) {start(socket);});
Le serveur écoute sans enregistrer de journal des requêtes. La fonction start
est la fonction qui doit être exécutée quand le client envoie une requête appelée connection
. Cette fonction émet en réponse une donnée (ici avec l'étiquette notification
et contenant une simple phrase "le serveur écoute les sockets". De plus, quand il reçoit la requête "appel", il émet une autre information, nommée "notification" et contenant la valeur "Présent!"
ws utilise côté client l'API Web socket native. Cela a pour inconvénient de ne pas être fonctionnel sur les navigateurs anciens, mais simplifie la mise en œuvre côté client. Il n'est pas nécessaire de charger une bibliothèque. Côté serveur, il faut importer le module ws : ws =import ('ws')
Côté client, le code est tout à fait similaire à l'exemple avec socket.io. Cependant, cette fois-ci, on passe par une connexion directe en utilisant le protocole ws
:
const socket = new WebSocket("wss://monserveur.org") ;
Sont alors disponibles les méthodes onpen
, onclose
, onerror
et onmessage
. Ces méthodes peuvent être remplacées par des event listeners associés aux événements correspondants. On écrira par exemple socket.addEventListener("open", gestionnaire)
.
Lors de la réception de données, celles-ci sont présentes dans la propriété data
de l'objet événement. Lors de la réception d'une erreur, l'information est disponible aussi dans l'objet événement transmis depuis le serveur.
On dispose aussi de la méthode send
pour l'envoi de données. Par exemple…
let socket = new WebSocket("ws://monserveurws.com"); socket.onopen = (e) => { console.log("[open] Connection established"); console.log("Sending to server"); socket.send("My name is John"); }; socket.onmessage = (e) => { console.log("[message] Data received from server: "+e.data); }; /* ou socket.addEventListener("message", (e) => { console.log("[message] Data received from server: "+e.data); });*/ socket.onclose = (e) => { if (e.wasClean) { console.log("[close] Connection closed cleanly, code="+e.code+" reason="+e.reason}); } else { // par exemple, processus serveur arrêté ou panne réseau // event.code est généralement 1006 dans ce cas console.warning('[close] Connection died'); } }; socket.onerror = (error) => { console.error(error);};
Le principe de ws est d'utiliser côté serveur, dans la mesure du possible, la même syntaxe que côté client. De base, on écrira ainsi par exemple…
// Importing the required modules const WebSocketServer = require('ws'); // Creating a new websocket server const wss = new WebSocketServer.Server({ port: 8888 }) // Creating connection using websocket wss.on("connection", ws => { console.log("New client connected"); // instructions à exécuter quand la connexion a été établie }); console.log("The WebSocket server is running on port 8888");
Les instructions à exécuter ressemblent à ce qui est codé côté client. Par exemple…
wss.on("connection", ws => { console.log("New client connected"); // sending message to client ws.send('Welcome, you are connected!'); //on message from client ws.on("message", data => { / / console.log(data.toString()) pour forcer la conversion de data en chaîne de caractères console.log("Client has sent us: "+data) }); // handling what to do when clients disconnects from server ws.on("close", () => { console.log("the client has disconnected"); }); // handling client connection error ws.onerror = function () { console.log("Some Error occurred") } });
MongoDB est un système de gestion de base de données NoSQL, auquel il est facile d'accéder depuis Node en raison de son format de stockage, le BSON (du JSON binaire). Une base MongoDB est structurée en collections, composées elles-mêmes de documents. Chaque document est un objet JavaScript, mais toutes les propriétés n'ont pas besoin d'être stockées. Cela permet de gagner de la place sur l'espace de stockage nécessaire pour la base de données. Par exemple :
{ "_id" : ObjectId("5c5e0511f59b8121641bbe21"), "nom" : "Dupont", "age": 34, "prenom": "Jules" } { "_id" : ObjectId("5c5e0ba614128f24f872a0a1"), "nom" : "Durand", "age": 23 } { "_id" : ObjectId("5c5ee355cd9c200710bdabf6"), "nom" : "Martin", age: 37 } { "_id" : ObjectId("5c5ee35ccd9c200710bdabf7"), "nom" : "Jones", "prenom": "William" } { "_id" : ObjectId("5c5ee4c5364ddb077b908523"), "nom" : "Bayard" }
_id
est une propriété générée automatiquement par MongoDB et correspond à une clef primaire.
Pour pouvoir utiliser Mongodb à partir de Nodejs, il faut recourir au client mongodb pour node, qui s'installe facilement avec npm. La connexion se fait de manière usuelle avec node, en important le module correspondant puis en l'instanciant :
var MongoClient = require('mongodb').MongoClient ;
const url = "mongodb://localhost" ;
const dbName = "nouvelle_base" ;
MongoClient.connect (url, function (error, client){
if (error) throw error ;
console.log("Connecté à la base de données") ;
const db = client.db(nbName) ;
//Traitements…
}) ;
À noter que le client mongodb utilise les promesses ; il est possible d'en tenir compte et d'écrire le code en conséquence (par exemple pour les opérations de lecture ou écriture dans la base), mais nous ne le ferons pas dans le cadre d'initiation de ce cours.
Une base de données MongoDB étant structurée en collections (un peu l'équivalent des tables dans une base de données relationnelle), il faut créer une collection avant de pouvoir ajouter des données. La méthode createCollection
permet de le faire. Elle prend deux paramètres :
le premier paramètre est une chaîne de caractères donnant le nom de la collection. Ce paramètre est obligatoire : db.createCollection("maCollection");
le second paramètre, facultatif, spécifie les options dans un objet JavaScript. Ces options permettent de préciser u certain nombre de carctéristiques de la collection, comme sa taille maximale en octets, le nombre maximum de documents qu'elle peut contenir, ou bien un gabarit de document-type permettant de préciser les règles de validité des enregistrements. Par exemple…
db.createCollection("clients", { capped: true, size : 6142800, max : 10000, validator: { $jsonschema : { bsonType: "object", required: [ "nom", "dateNaissance", "surnom" ], properties: { nom: { bsonType: "string", description: "chaîne de caractères obligatoire" }, dateNaissanceXXe: { bsonType: "int", minimum: 1901, maximum: 2000, exclusiveMaximum: false, description: "entier obligatoire entre 1901 et 2000" }, surnom: { bsontype: "string" description: "chaîne de caractères obligatoire" } ville : { enum: [ "Paris", "Lyon", "Marseille", "Lille", "Bordeaux", null ], description: "uniquement une des valeurs de la liste" } } } });
On supprime une collection avec la commande db.collection.drop();
.
On peut ajouter de nouveaux documents avec deux méthodes :
La méthode insertOne(contenu, [callback])
permet d'ajouter un document à une collection donnée. Supposons par exemple définie la collection coll1
; on ajoutera alors db.coll1.insertOne({reponseUltime: 42});
. Le callback
permet d'indiquer un traitement spécifique, par exemple en cas d'erreur : db.coll1.insertOne({reponseUltime: 42});
insertMany
permet d'ajouter plusieurs documents, sous la forme d'un tableau. Par exemple, db.coll1.insertMany([{enr: "hip"},{enr: "hop"}]);
La lecture de la base de données passe par l'utilisation d'une méthode, find()
, couplée ou non à des filtres. Pour récupérer tous les documents d'une collection, on utilise ainsi db.col1.find({})
, donc find()
sans aucun filtre. Supposons maintenant définie une collection nommée carnet
:
db.collection('carnet').insertMany([ { nom: 'Durand', prenom: 'Jules', dateNaissance: "2000-01-01", enfant: 3, statut: "ami" }, { nom: 'Dupont', prenom: 'Hector', dateNaissance: "1999-07-21", enfant: 1, statut: "ami" }, { nom: 'Martin', prenom: 'Cunégonde', dateNaissance: "1999-11-12", statut: "collègue" }, { nom: 'Bernard', prenom: 'Richard', dateNaissance: "2000-05-23", enfant: 2, statut: "collègue" }, ]);
On peut récupérer un document correspondant à un unique critère avec par exemple db.carnet.find({nom: "Dupont"})
. Mais cette méthode permet aussi de récupérer directement de multiples documents, par exemple tous les documents pour lesquels statut
vaut "collègue"
: db.carnet.find({statut: "collègue"})
. On peut également utiliser des filtres. Par exemple, pour récupérer tous les documents possédant la propriété enfant
, on écrira db.carnet.find({enfant: {$exists: true}})
. La liste complète et détaillée des filtres est disponible sur le site officiel.
La mise à jour d'un document se fait de manière similaire à leur création, avec les méthodes updateOne()
et updateMany()
. Par exemple :
db.collection('repertoire').updateOne( { nom: 'Durand' }, { $set: { prenom: 'Jules', numero: 21, rue: 'avenue du Général-de-Gaulle' }, } );
Mongodb permet là encore d'utiliser deux méthodes: deleteOne()
et deleteMany()
pour supprimer des documents. Reprenons la collection carnet
donnée en exemple précédemment. On peut alors supprimer le premier document correpondant à un critère donné avec deleteOne()
en fournissant un filtre : db.collection.deleteOne({statut: "collègue"});
va ainsi supprimer le document {nom: 'Martin', prenom: 'Cunégonde', dateNaissance: "1999-11-12", statut: "collègue"}
.
On peut aussi supprimer tous les documents correspondant à un filtre donné avec deleteMany()
. Par exemple, db.collection.deleteMany({statut: "ami"});
supprimera ici les deux premiers documents de la base. On peut également utiliser des filtres, comme pour la lecture. Par exemple, db.col1.deleteMany({dateNaissance: {$gt:"1999-12-22"}})
supprime tous les documents correspondant aux personnes nées après le 22 décembre 1999.
Cette création est mise à disposition par Gilles Chagnon sous un contrat Creative Commons.