Cours de JavaScript - Initiation à JavaScript côté serveur : nodejs, Express, Websocket, MongoDB

version 1.5.0, dernière mise à jour le 13 novembre 2023.

   

Table des matières (TdM)

  1. I. Généralités
    1. Principe
    2. Installation et lancement
    3. Un serveur simple
      1. Mise en place du serveur
      2. Routage
      3. Exercice : Mise en place d'un serveur de base avec Node.js
    4. Structure type d'un projet Node
      1. Arborescence
      2. Le fichier package.json
    5. Accéder au système de fichiers avec le package fs
    6. Mise en place d'un routage simple avec Express
      1. Syntaxe de base
      2. Pour aller un peu plus loin…
      3. Gestion des erreurs 404
    7. Redémarrer le serveur automatiquement en cas de changement des sources : nodemon
    8. Exercice : Mise en place d'un mini-site avec Express
  1. II. Structurer le projet avec npm
    1. Présentation de npm
    2. Gestion du fichier package.json
      1. Initialiser package.json
      2. Gestion des dépendances
    3. Exercice : Initialisation d'un projet avec npm
  1. III. Le framework de test Mocha
    1. Introduction
      1. Principe de fonctionnement
      2. Paramétrage du fichier package.json
    2. Écriture des tests
    3. Lancer et interpréter les tests
    4. Exercice : Développement d'un mini-site en utilisant des tests
  1. IV. Utiliser des templates : l'exemple d'EJS
    1. Introduction
    2. Mise en œuvre de base
      1. Changement du moteur de rendu
      2. Mise en place des vues
    3. Passage de paramètre, éléments de programmation
      1. Passage de paramètre depuis l'URL
      2. Boucles, tests…
    4. Exercice : Utilisation d'EJS
  1. V. Échanges asynchrones client/serveur avec Websocket
    1. Introduction
    2. Avec socket.io
      1. Principe
      2. Côté client
      3. Côté serveur
      4. Exercice : Utilisation de socket.io
    3. Avec ws
      1. Principe
      2. Côté client
      3. Côté serveur
      4. Exercice : Utilisation de ws
  1. VI. Nodejs et Mongodb
    1. Généralités
      1. Structure de la base
      2. Connexion à la base
    2. Manipulation des collections
      1. Création de nouvelle collection
      2. Suppression de collection
    3. Création, lecture, mise à jour et suppression de document
      1. Création de nouveau document
      2. Lecture de documents
      3. Mise à jour d'un document
      4. Suppression d'un document
    4. Exercice : Ajout simple de documents
    5. Exercice : Manipulation de la base de données via le routage

Retour au menu

Contenu du cours

I. Généralités

1. Principe

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 :

Heureusement, des frameworks spécialisés permettent de simplifier un certain nombre de ces étapes, comme Express.js par exemple.

>Retour à la TdM

2. Installation et lancement

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

>Retour à la TdM

3. Un serveur simple

a. Mise en place du serveur

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.

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"})

b. Routage

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>") ;

Exercice 1. Mise en place d'un serveur de base avec Node.js

Énoncé
Correction (Serveur de base)
Correction (Entête HTTP)
Correction (Routeur)

>Retour à la TdM

4. Structure type d'un projet Node

a. Arborescence

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 :

b. Le fichier package.json

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 :

>Retour à la TdM

5. Accéder au système de fichiers avec le package fs

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).

>Retour à la TdM

6. Mise en place d'un routage simple avec Express

a. Syntaxe de base

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') ;
}) ;

b. Pour aller un peu plus loin…

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 !

c. Gestion des erreurs 404

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(…

>Retour à la TdM

7. Redémarrer le serveur automatiquement en cas de changement des sources : nodemon

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".

>Retour à la TdM

Exercice 1. Mise en place d'un mini-site avec Express

Énoncé
Correction (fichier zip)

II. Structurer le projet avec npm

1. Présentation de npm

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.

>Retour à la TdM

2. Gestion du fichier package.json

a. Initialiser package.json

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.

b. Gestion des dépendances

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.

>Retour à la TdM

Exercice 1. Initialisation d'un projet avec npm

Énoncé
Correction (fichier zip)

III. Le framework de test Mocha

1. Introduction

a. Principe de fonctionnement

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.

b. Paramétrage du fichier package.json

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.

>Retour à la TdM

2. Écriture des tests

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 :

>Retour à la TdM

3. Lancer et interpréter les tests

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.

>Retour à la TdM

Exercice 1. Développement d'un mini-site en utilisant des tests

Énoncé
Correction (fichier zip)

IV. Utiliser des templates : l'exemple d'EJS

1. Introduction

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.

>Retour à la TdM

2. Mise en œuvre de base

a. Changement du moteur de rendu

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') ;

b. Mise en place des vues

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') ;
}) ;

>Retour à la TdM

3. Passage de paramètre, éléments de programmation

a. Passage de paramètre depuis l'URL

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

b. Boucles, tests…

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>

>Retour à la TdM

Exercice 1. Utilisation d'EJS

Énoncé
Correction (fichier zip)

V. Échanges asynchrones client/serveur avec Websocket

1. Introduction

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 :

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.

>Retour à la TdM

2. Avec socket.io

a. Principe

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 :

b. Côté 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.

c. Côté serveur

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!"

Exercice 1. Utilisation de socket.io

Énoncé
Correction

>Retour à la TdM

3. Avec ws

a. Principe

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')

b. Côté client

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);};
c. Côté serveur

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")
	}
});

Exercice 1. Utilisation de ws

Énoncé
Correction

>Retour à la TdM

VI. Nodejs et Mongodb

1. Généralités

a. Structure de la base

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.

b. Connexion à la base

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.

>Retour à la TdM

2. Manipulation des collections

a. Création de nouvelle collection

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 :

b. Suppression de collection

On supprime une collection avec la commande db.collection.drop();.

>Retour à la TdM

3. Création, lecture, mise à jour et suppression de document

a. Création de nouveau document

On peut ajouter de nouveaux documents avec deux méthodes :

b. Lecture de documents

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.

c. Mise à jour d'un document

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' },
	}
);
d. Suppression d'un document

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.

>Retour à la TdM

Exercice 1. Ajout simple de documents

Énoncé
Correction

Exercice 2. Manipulation de la base de données via le routage

Énoncé
Correction

Historique de ce document

Conditions d'utilisation et licence

Creative Commons License
Cette création est mise à disposition par Gilles Chagnon sous un contrat Creative Commons.

Retour au menu