Cours de JavaScript - Principes de base de nodejs et Express

version 1.10, dernière mise à jour le 14 décembre 2017.

   

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. 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
    6. Redémarrer le serveur automatiquement en cas de changement des sources : nodemon
    7. 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

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. 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'exempl 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 oeut 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

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

>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 en sauvegarder une version dans le même répertoire que celle-ci, que l'on partagera. Pour cela, on écrira npm install react --save ; une copie de la bibliothèque est alors stockée dans un sous-répertoire nommé node_modules.

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

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 à deux notions que nous n'avons pas abordées 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. À titre d'exemple, nous utilisons de plus une « flèche grasse », qui permet de simplifier grandement les écritures de fonctions dans les cas les plus simples. 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 tapant 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}) ;
}) ;

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 auss, par exemple, réaliser des boucles. Par exemple…

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)

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