Grégory Bourguin
SysReIC - LISIC - ULCO
Introduction à Express

Introduction à Express

Nous avons vu que le runtime Node.js fournit les bases permettant de développer un serveur web en Javascript. Cependant, développer un site complet sur ces bases uniques serait très complexe : il faudrait écrire nous-mêmes de nombreuses fonctionnalités nécessaires comme le routage, l'extraction de données contenues dans les requêtes, la génération de pages basée sur des templates, etc.

Le framework Express est un package Node.js largement utilisé par les développeurs et qui facilite grandement l'ensemble de ces tâches.

Installation

L'installation d'Express se fait simplement grâce à npm: npm install express

Mise en oeuvre

La mise en oeuvre d'Express commence par la définition d'une application basée sur ce framework : il s'agit de créer un script qui utilise Express pour gérer les requêtes envoyées au serveur.

Une approche consiste à créer dans backend/ un nouveau fichier nommée app.js :

app.js

const express = require('express'); // inclusion d'express

// Instanciation d'une application express
const app = express();

// Configuration de l'application : une première gestion "basique" des requêtes.
app.use((req, res) => {
    // Une fois encore, les requêtes sont pour l'instant toutes traitées de la même manière.
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html;charset=utf-8');
    res.end('Le serveur Express dit <b>bonjour</b>');
});

// Exportation de notre application express
module.exports = app;

Il faut ensuite modifier notre point d'entrée server.js pour qu'il délègue le traitement des requêtes à l'instance créée.

server.js

require('dotenv').config({ path: './.env'} ) ;

const port = process.env.PORT || 3000

const http = require('http');
const app = require('./app'); // inclusion d'Express

// mise en oeuvre : on délègue la gestion des requêtes à Express
const server = http.createServer(app);

server.listen(port,  ()=>{
    console.log(`Le server écoute sur http://127.0.0.1:${port}/`);
})

Alternative

Une autre solution est de mettre tout le code de démarrage du serveur dans le fichier d'application app.js (et donc de supprimer le fichier server.js qui devient inutile).

Exemple :

app.js

require('dotenv').config({path: './.env'})

const express = require('express'); // inclusion d'express
const app = express(); // Instanciation d'une application express

// Configuration de l'application : une première gestion "basique" des requêtes.
app.use((req, res) => {
    // Une fois encore, les requêtes sont pour l'instant toutes traitées de la même manière.
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html;charset=utf-8');
    res.end('Le serveur Express dit <b>bonjour</b>');
});

const port = process.env.PORT || 3000
app.listen(port,()=>{
    console.log(`Le server écoute sur http://127.0.01:${port}/`);
})

Bien entendu, il faut dans ce cas mettre à jour les scripts de démarrage dans le fichier package.json pour ne plus utiliser server.js mais directement app.js.

Middleware

L'application express que nous venons d'implémenter est très (trop) simple : toutes les requêtes sont à nouveau traitées de la même manière : dans le handler passé à app.use, nous n'avons fait aucun usage du paramètre req qui contient les informations spécifiques concernant la requête reçue.

Dans une application "réelle", le serveur va généralement traiter les requêtes différemment selon les informations qu'elles contiennent (chemin, méthode, etc.) : par exemple, une requête utilisant la méthode GET, ne sera certainement pas traitée comme une requête utilisant la méthode POST.

Dans Express, cette différenciation est réalisée sous la forme de middlewares.

Principe

Dans Node.js et Express, les informations relatives à la requête sont disponibles dans l'objet req qui est transmis aux handlers que vous implémentez.

Par exemple, dans le app.js précédent, vous pouvez demander au serveur d'afficher la méthode utilisée pour la requête reçue en ajoutant la ligne :

app.use((req, res) => {
    console.log(req.method);
    ...
});

A chaque requête reçue (rafraichissez la page dans le navigateur), vous verrez apparaitre dans la console du serveur la méthode utilisée.

De fait, dans une application complexe, il faut écrire le code qui va extraire les informations contenues dans les requêtes et décrire tous traitements spécifiques spécifiques à effectuer pour chaque type de requête : par ex. le traitement des paramètres éventuels d'une requête GET, le parsing du corps d'une requête POST et la récupération des données transmises (données de formulaires, fichiers uploadés, ...), etc.

Écrire la totalité de ces instructions dans le seul handler de notre app.use serait (entre autres) illisible. De plus, une bonne partie de ces traitements est commune au fonctionnement de tous les serveurs web, et de nombreuses fonctionnalités ont déjà été implémentées par la communauté des développeurs.

Pour permettre le découpage et l'intégration des traitements, Express introduit la notion de middleware : une application va faire passer les informations concernant la requête (req) et la construction de la réponse (res) dans une chaine de middlewares dont le dernier sera chargé d'envoyer la réponse finale au client.

Implémentation

Le app.use que nous avons écrit est en fait lui-même un middleware de notre application.

Une manière d'ajouter des middlewares consiste à prendre en compte un nouveau paramètre next dans le handler, et d'y faire appel pour déclencher l'exécution du middleware suivant.

L'exemple ci-dessous fait s'enchainer 3 middlewares basiques :

app.js

const express = require('express');
const app = express();

// 1er middleware : ex. d'affichage d'informations dans la console
app.use((req, res, next) => {
    const now = new Date().toDateString() ;
    console.log(`${now} : une requête ${req.method} est arrivée !`);
    next(); // l'appel à next() transmet les informations pour traitement dans le middleware suivant
});


// 2ème middleware : préparation de la réponse
app.use((req, res, next) => {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html;charset=utf-8');
    next(); // l'appel à next() transmet les informations pour traitement dans le middleware suivant
});

// 3ème middelware : envoi de la réponse
app.use((req, res) => {
    res.end('Le serveur Express dit <b>bonjour</b>');
});

...

Réponse Statique

La réponse de notre serveur au client n'a jusqu'à présent été constituée que de quelques méta-informations et d'une simple chaîne.

Bien entendu, Express fournit les moyens de renvoyer des réponses plus complexes sous la forme de fichiers.

Nous allons maintenant voir 2 manières de renvoyer des fichiers "statiques", c'est à dire qui ne sont pas transformés/complétés par le serveur avant d'être retournés au client.

NB : les fichiers statiques d'un serveur sont en général les feuilles de style(.css), les fichiers images, des fichiers html "pur", etc.

Envoi de Fichier

La méthode res.sendFile d'Express permet d'envoyer un fichier au client.

On peut donc utiliser cette méthode pour envoyer notre réponse sous la forme plus classique d'une page html.

Pour l'exemplifier, nous allons créer un simple fichier nommé index.html. La convention veut que les documents statiques (fichiers html, feuilles CSS, images, etc.) d'un serveur web soient mis dans un dossier nommé /public.

index.html

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Super Site</title>
</head>
<body>

<h1>Le serveur Express dit bonjour !!!</h1>

</body>
</html>

Il nous reste maintenant à modifier notre application Express pour qu'elle renvoie directement ce fichier au client.


NB : dans cet exemple, quelle que soit la requête du client, notre serveur renvoie la même page statique index.html.

Dossier Public

Il est possible de faire en sorte qu'Express renvoie différents fichiers statiques en se basant automatiquement sur le chemin public indiqué dans la requête.

La fonction middleware express.static(root, [options]) permet à Express de servir les fichiers statiques se trouvant sous le dossier root.

On donne généralement le nom de public au dossier racine contenant les fichiers statiques.

L'exemple ci-dessous transforme l'application précédent pour qu'elle serve automatiquement tous les fichiers statiques se trouvant dans public/ :

const express = require('express');
const app = express();

const path = require('path');

// Ajoute un middleware qui retourne les documents statiques situés sous le dossier /public.
// NB : il faut le mettre avant tout autres use qui modifie res 
// pour que le cas des fichiers static soit bien traité en 1er dans la chaine des middlewares.
app.use(express.static(path.join(__dirname,'public')));

app.use((req, res, next) => {
    const now = new Date().toDateString() ;
    console.log(`${now} : une requête ${req.method} est arrivée !`);
    next();
});

module.exports = app;

On peut maintenant accéder au fichier statique index.html en allant à l'adresse http://127.0.0.1:3000/pages/index.html.

L'intérêt de cette méthode est que le serveur peut renvoyer d'autres fichiers statiques. Pour l'exemplifier, nous allons modifier index.html en y ajoutant une référence à une feuille de style (un autre fichier statique) que nous mettrons dans public/css/mains.css.

main.css

body{
    background: black;
    color: limegreen;
    text-align: center;
}

Le fichier html modifié :

index.html

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Super Site</title>

    <!--Référence au fichier statique main.css-->
    <link rel="stylesheet" href="/css/main.css">

</head>
<body>

<h1>Le serveur Express dit bonjour !!!</h1>

</body>
</html>

L'accès à cette page affiche maintenant le message centré en limegreen sur fond noir.

La console réseau indique que le serveur a bien envoyé les 2 fichiers statiques.

Templates EJS

S'il est important de permettre au serveur de renvoyer des fichiers statiques (images, css, etc.), les sites web mettent généralement en oeuvre un mécanisme de réponses dynamiques basé sur des templates : le fichier html renvoyé contient des "trous" qui sont remplis dynamiquement par le serveur (au moment du traîtement de la requête) en utilisant un langage de scripts.

Il existe de nombreux frameworks qui facilitent la réalisation de sites à base de templates.

Un des plus populaires dans le cadre de Node.js et Express est EJS (Embedded JavaScript templating) qui, comme son nom l'indique, utilise Javascript comme langage de script.

Installation

L'installation d'EJS est faite simplement par : npm install ejs.

Définition de vues

En EJS, les fichiers .html sont remplacées par des fichiers .ejs appellés vues (views).

Les vues EJS contiennent du code "classique" : html + css + JavaScript "client" (exécuté par le navigateur), mais aussi du code JavaScript "serveur" qui sera exécuté par le serveur avant envoi au client.

Le traitement du code Javascript "serveur" est similaire au traitement d'un code PHP dans un serveur PHP : il est interprété sur le serveur, remplacé par d'éventuelles sorties, et n'apparait en aucun cas dans la page résultante envoyée au client.

Le code Javascript "serveur" EJS est mis entre les balises <% et %>.

Par convention, les pages EJS sont rangées dans le dossier /views/pages/.


L'exemple ci-dessous correspond à une vue nommée home.ejs qui peut être paramétrée par une variable nommée user :


La balise ouvrante <%= permet d'effectuer une sortie dans la page.

Dans l'exemple, la ligne 19 fera afficher "Bonjour valeur_de_user".

Engine & Render

Pour que le serveur utilise EJS, il faut préciser à Express le moteur (engine) de rendu qu'il doit utiliser. Il faut aussi indiquer à ce moteur le dossier dans lequel il pourra trouver les vues demandées.

La configuration du moteur de rendu est réalisée grâce à la méthode set d'express.

Dans notre exemple, il faut donc compléter le contenu de app.js avec :

app.set('view engine', 'ejs'); // Définition du moteur de rendu 
app.set('views', path.join(__dirname, 'views')); // Déclaration du dossier contenant les vues

La demande de rendu est réalisée grâce à la méthode render de l'objet res.

Dans notre exemple (simple), on veut que le traitement de toutes les requêtes reçues par le serveur réponde avec le rendu de la vue home.ejs qui se trouve dans le sous-dossier pages du dossie des vues views.

On écrira donc le middleware :

app.use((req, res) => {
    // demande de rendu EJS
    res.render('pages/home') ; // on donne le chemin dans views, et on omet le .ejs
});

On obtient le fichier app.js :

Avec pour résultat :

Render Paramétré

L'intérêt des templates est de pouvoir les paramétrer et c'est ce que nous avons prévu avec la variable user utilisée dans home.ejs.

Cependant, la version actuelle de app.js n'envoie pas de paramètre à la vue : la variable user n'est donc pas définie et la 1ère ligne de home.ejs a donc pour effet de la créer en lui affectant la valeur 'bel(le) inconnu(e)', ce qui explique l'affichage obtenu.

L'envoi de paramètre(s) à une vue est réalisé en ajoutant un dictionnaire de paramètre(s) comme second argument de l'appel à la méthode render.

Pour envoyer un user à notre vue, il faut donc modifier l'appel à render dans notre app.js avec :

res.render('pages/home', { user: 'Greg'}) ;

Une requête sur le serveur a maintenant pour résultat :

Structures de Contrôles

Les balises EJS peuvent aussi contenir des structures de contrôle (if, for, while, ...) permettant de gérer la génération de portions de code client.

La modification de home.ejs ci-dessous a le même effet que précédemment, mais la génération est ici contrôlée par une structure if ... else ... (lignes 14 à 18) :

Exercice 1

Version 1

Modifiez l'application pour que home.ejs puisse être paramétrée par : nickname et sex.

Selon la configuration des paramètres, l'application affichera un des messages suivants :

  • "Bonjour bel(le) inconnu(e)"
  • "Bonjour bel inconnu"
  • "Bonjour belle inconnue"
  • "Bonjour nickname
  • "Bonjour Mr nickname"
  • "Bonjour Mme nickname"

La correction se trouve ICI.

Version 2

Au lieu d'utiliser 2 paramètres séparés nickname et sex, faites en sorte que ces informations soient les attributs d'un seul et même paramètre nommé user.

La correction se trouve ICI.

Partials

Les pages d'un site sont souvent constituées de parties qui sont réutilisées d'une page à l'autre comme l'entête (header) et le pied de page (footer).

L'instruction <%- include('chemin') %> permet d'inclure des parties de pages définies dans d'autres fichiers .ejs.

Dans notre exemple, on peut créer dans views un nouveau sous-dossier nommé partials dans lequel on mettra les fichiers head.ejs et foot.ejs.

head.ejs

<!doctype html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Test EJS</title>
    <link rel="stylesheet" href="/css/main.css">
</head>
<body>

foot.ejs

</body>
</html>

Ils peuvent être importés dans home.ejs grâce à include, et pourraient tout à fait servir dans d'autres pages.

Exercice 2

Transformez l'exercice précédent pour qu'il utilise les partials head.ejs et foot.ejs

Express EJS Layout

L'utilisation d'include est pratique, mais cette méthode est fastidieuse lorsque l'on souhaite inclure systématiquement des parties comme le header et le footer sur toutes les pages d'un site.

Le package express-ejs-layouts permet de pallier ce problème en faisant en sorte que les vues utilisent un(des) layout(s) spécifique(s) sans avoir à ajouter include dans chaque vue.

L'installation est à nouveau faite simplement grâce à npm (cf. documentation).

Pour l'utiliser dans notre application, nous définissons le layout souhaité dans un fichier par exemple nommé layout.ejs que nous mettons dans un sous-dossier layouts du dossier views.

Vous remarquerez que la ligne 15 de layout.ejs utilise l'instruction EJS <%- body %> : c'est à cet endroit que sera injectée la vue que l'on souhaitera afficher.


Notez aussi que layout.ejs inclut les fichiers header.ejs et footer.ejs tels que :

header.ejs

<header>
    Voici un Header
</header>

footer.ejs

<footer>
    Voici un Footer
</footer>

Ce layout étant plus complexe que les pages que nous avons créées jusqu'ici, voici une nouvelle version de notre feuille de style main.css :

La dernière étape consiste à ajouter le middleware express-ejs-layouts dans app.js :


Et voici le résultat obtenu :

La correction se trouve ICI.

Exercice 3

Transformez l'exercice précédent pour qu'il n'utilise plus des include, mais qu'il mette en oeuvre un layout.