Express.js
Cet article est en construction: un auteur est en train de le modifier.
En principe, le ou les auteurs en question devraient bientôt présenter une meilleure version.
Introduction
Express.js est un framework basé sur Node.js pour le développement d'applications web.
Objectifs de cette page
Cette page fait principalement référence au cours STIC I du Master MALTT et s'inscrit dans la perspective d'aborder les fondamentaux de la programmation à travers JavaScript. Plus en détail, Express.js permet de montrer de manière concrète et pragmatique des éléments conceptuels et techniques liés à l'architecture requête/réponse des applications web.
Pour comprendre le contenu de cette page la lecture des pages suivantes et conseillée :
Rappel sur l'architecture requête/réponse
À faire, en attente voir par exemple :
- Ce tutoriel sur OpenClassrooms qui explique le mécanisme
- La page sur le protocole HTTP sur Wikipedia
- Cette liste de codes HTTP sur Wikipedia pour connaître les différents status associés aux résultats d'une requête
Installation de Express.js
Installer le module Express
Comme tout module node.js qui se trouve sur npm, il y a deux possibilité pour installer Express.js :
- En locale dans le dossier de votre application
- Globalement dans votre système
Installation locale dans le dossier de l'application
La commande pour installer Express.js en tant que module locale dans le dossier de votre application est le suivant :
npm install express
Ce type d'installation résulte dans l'arborescence suivante :
Votre_dossier |- node_modules |- express |- votre_fichier_node_principale.js
Installation globale dans le système
Pour installer Express.js de manière globale dans le système, la commande est la suivante :
npm install -g express
L'installation d'express de manière globale n'est cependant pas très intéressante, car ce type d'installation est plus appropriée pour des modules qui prévoient une utilisation en ligne de commande, ce qui n'est pas le cas pour Express. L'installation en locale est donc le choix plus adéquat.
Importer le module dans votre application
Encore une fois, Express.js est un "simple" module Node.js et par conséquent il est suffisant d'utiliser la fonction require()
pour l'importer dans votre application :
var express = require('express');
Contrairement à d'autres modules tels que fs
, cependant, qui retournent un objet avec des propriétés et des méthodes, Express.js retourne une fonction. Pour cette raison, on associe cette fonction à une autre variable (généralement appelée app
) qui représente l'application web elle-même :
//Importer le module express var express = require('express'); //Le module express exporte une fonction, donc associer la fonction à une variable var app = express();
L'association d'une application express()
à una variable est également utile car Express prévoit de pouvoir "assembler" plusieurs applications dans le même script node:
var express = require('express'); var app_public = express(); //par exemple une application publique var app_private = express(); //par exemple une application qui utilise un login qui ne s'applique pas à l'app publique.
Première application web avec Express.js
Express.js est un framework basé sur le module http
de Node.js (avec quelques éléments en plus). Donc l'architecture d'un service/serveur web créé avec Express.js est assez similaire à un serveur/service web créé avec le module http. Voici un exemple avec des commentaires pour faciliter la compréhension des différents éléments :
//Importer le module express
var express = require('express');
//Le module express exporte une fonction, donc associer la fonction à une variable
var app = express();
//Définir la route principale de l'application et envoyer une réponse
app.get('/', function(request, response) {
//Express envois automatiquement les headers en fonction du contenu, dans ce cas du text/plain
response.send("My first express app!");
});
//Le server http écoute sur la porte 9000
app.listen(9000);
//Message pour la console
console.log("Server listening at http://localhost:9000/");
Le changement plus important par rapport à un simple serveur web avec http concerne l'utilisation des "routes" avec la méthode app.get()
qui a comme objectif d'intercepter les requêtes en fonction de l'URL. Dans cet exemple, nous spécifions la route "/", c'est-à-dire l'URL même de notre serveur/service web, qui dans notre cas est le localhost avec la porte 9000. Voici le résultat que vous obtenez à l'adresse http://localhost:9000 :
Ce simple exemple ne permet pas d’apprécier les potentialités de Express.js, mais donne néanmoins déjà un aperçu de sa "philosophie" de framework basé sur l'architecture requête/réponse. Dans la suite de cette page, nous fournirons quelques concepts théoriques et quelques exemples pour approfondir cette architecture.
Comprendre Express.js
Express.js permet d'exploiter la structure événementielle de Node.js dans le cadre spécifique des applications web basées sur l'architecture client/serveur et requête/réponse. Pour bien comprendre le fonctionnement de Express.js, il peut être utile de faire une comparaison avec un serveur web "normal" tel que Apache.
Le rôle de l'interprète
Une des différences fondamentales entre un serveur web Node.js/Express.js et un serveur traditionnel consiste dans le positionnement et le rôle de l'interprète du langage de programmation utilisé pour générer des pages web dynamique.
Cycle requête/réponse avec un server web traditionnel
Un serveur "traditionnel" est principalement construit pour servir des pages statiques (i.e. en simple HTML) selon une simple association entre l’URL de la ressource souhaitée et le positionnement des fichiers HTML sur le serveur web. Pour générer des pages web dynamiques, donc, un server comme Apache nécessite de la présence d'un interprète, comme par exemple PHP, qui identifie des pages dynamiques en fonction de l'extension (.php). Dans le cas de ces pages, le serveur exécute les instructions contenues dans le fichier - qui servent normalement à générer du HTML - avant de renvoyer la ressource en tant que réponse. La carte conceptuelle suivante montre le cycle requête/réponse lorsque le serveur utilise un serveur HTTP "traditionnel" :
Cycle requête/réponse avec un server web Node.js/Express.js
Le même cycle requête/réponse est par contre très différent avec un serveur créé avec Node.js, comme c'est le cas de Express.js. Node.js permet en effet de créer directement un serveur HTTP qui est chargé de traiter les requêtes faites par un client et générer des réponses, comme par exemple des pages HTML. En d'autres termes, en accord avec le principe événementielle de Node.js, les requêtes faites à travers un protocole HTTP ne représentent qu'un type d'événement particulier, qui peut être géré avec un module http. Express.js est tout simplement un ensemble d'objets (avec des propriétés et méthodes) qui peuvent être utiles pour gérer le cycle requête/réponse. La carte conceptuelle suivante montre ce cycle avec Express.js :
Single thread vs. Multi thread
Multi-thread: une copie pour chaque ressource
Une autre différence majeure entre Express.js et un server web traditionnel concerne l'architecture Multi- vs. Single-thread. Dans une architecture traditionnelle de type Multi-thread, le server associe à chaque requête une "copie" de la ressource qu'il crée ex-novo chaque fois qu'un client demande cette ressource. Les copies (i.e. les "threads") sont donc indépendantes les unes des autres et si deux clients demandent la même ressource de manière simultanée, ils vont obtenir les deux ressources telles qu'elles sont à ce moment exact dans le temps. Cela signifie que, dans le cas plus théorique que réel où deux requêtes sont traitées exactement en même temps, si la première requête apporte un changement à la ressource demandé, cette modification ne sera pas disponible dans l'autre requête simultanée. Voici un schéma illustrant une architecture Multi-thread:
Single thread: la même ressource pour tous
Express.js adopte plutôt une architecture de type Single-thread, dans laquelle chaque requête est traitée de manière individuelle en ayant accès à la même ressource. Si deux requêtes surviennent de manière simultanée, le server les insère dans une queue et détermine l'ordre de traitement. Normalement, celle qui reçoit la première position dans la queue sera également la première à être traitée, mais en raison de la nature fortement asynchrone de Node.js, il est difficile de prévoir exactement l'ordre de traitement. Ce qui est important à noter c'est plutôt le fait que l'architecture Single-thread fait ainsi que toutes les requêtes ont accès à la dernière version de la ressource, car toutes les requêtes ont accès à la même ressource et pas à des copies. Cela se traduit par le fait que une modification faite par la requête qui passe en première dans la queue de traitement sera déjà disponible dans la requête qui est passe en deuxième. Voici un schéma illustrant l'architecture Single-thread:
Veuillez noter que l'architecture Single-thread ne s'applique pas seulement à la même ressource (e.g. la page /about/ dans le cas du schéma), mais à toutes les ressources disponibles dans le server. Cela signifie que deux requêtes simultanées seront placées dans la queue de traitement même si elles demandent deux ressources différentes (e.g. une requête la page /about/ et l'autres la page /contact/). Ce mécanisme fait ainsi que toutes les requêtes qui arrivent sur un server Express.js soient traitées de manière centralisée et ont donc accès à la version la plus récente de toutes les données qui concernent toutes les ressources de l'application.
Le concept de "middleware"
Définition générale
Un middleware est un logiciel qui se situe entre le hardware et le software. Il permet la communication entre des logiciels qui ne sont initialement pas prévu pour communiquer. Le middleware permet aussi la communication entre bases de données. Toutes ces communications peuvent être réalisées de manière synchrone et asynchrones. Cela veut dire que les applications n'ont pas besoin d'être disponibles simultanément pour pouvoir communiquer.
L'échange de message fonctionne, de manière imagée, comme une boite mail. Les messages sont envoyés et reçus dans des queues de traitement. Ce type de programme est appelé MoM ou Message-Oriented-Middleware.
Les trois principales interactions que peut réaliser un middleware sont l'échange de message, l'appel de procédure ainsi que la manipulation d'objets.
Les middleware dans Express.js
Dans le cadre d'Express.js, le middleware maintient son rôle de couche intermédiaire, mais appliquée entre la requête et la réponse plutôt qu'entre le hard- et le soft-ware. Si on résume à l'essentiel l'architecture requête/réponse, on peut établir que la requête d'un client n'est pas satisfaite avant qu'elle reçoit une réponse. Donc il y a des mécanismes intermédiares entre la requête et la réponse qui sous-tendent la satisfaction de la requête avec la réponse appropriée.
En considération de l'architecture Single-thread d'Express.js (voir point précédent), toutes les requêtes passent par le même endroit et il faut donc des mécanismes qui permettent, parmi d'autres choses, de :
- Intercepter et identifier quelle type de requête a été faite, par exemple :
- Quelle ressource (e.g. quelle page) a été demandée?
- Quelle type de requête a été faite? Une requête de type GET, de type POST, etc.
- La requête dispose-t-elle d'informations supplémentaires sur le client (cookie avec username, etc.) ?
- Est-ce que la requête demande une ressource qui existe dans le server (ou existe encore) ?
- Construire une réponse appropriée à la requête, par exemple :
- Est-ce que le client a le droit de recevoir tout le contenu de la réponse ou seulement une partie ?
- Est-ce qu'il faut personnaliser la réponse ? Par exemple en envoyant les données dans un database qui ont été insérées par un utilisateur spécifique.
Une chaîne séquentielle de middleware
Une application web avec Express.js se compose normalement de plusieurs middleware qui ont justement le rôle d'évaluer la requête et envoyer, selon la logique de l'application, la réponse associée à un requête de ce type. Pour déterminer la logique de l'application, on fait recours à plusieurs middlewares représentant des étapes sequentielles. Voici une représentation schématique du cycle qui se crée :
- Le server intercepte une requête
- Passage dans le Middleware1, si la logique du Middleware1 prévoit une réponse, alors la requête est satisfaite, si non continuer dans le cycle
- Passage dans le Middleware2, si la logique du Middleware2 prévoit une réponse, alors la requête est satisfaite, si non continuer dans le cycle
- Passage dans le Middleware3, si la logique du Middleware3 prévoit une réponse, alors la requête est satisfaite, si non continuer dans le cycle
- Etc.
De manière plus concrète, un middleware est une fonction qui accepte au moins trois arguments :
- L'objet requête
- L'objet réponse
- Le prochain middleware qui doit être exécuté avec une fonction de callback (e.g. next()).
Le next() middleware sera executé si le middleware courant ne prévoit pas une réponse, autrement le cycle s'arrête. Si un middleware ne prévoit pas un next(), le processus va rester figé dans ce middleware, même s'il n'y a pas une réponse, ce qui se traduit normalement par une page "loading" sans arrêt. Il est donc important d'ajouter toujours la possibilité de poursuivre au prochain middleware si le middleware courant ne prévoit pas une réponse.
Voici le code "prototypique" d'une suite de middleware:
//Définitions des middlesware
//Middleware 1
function myMiddleware1(request, response, next) {
if(request ... ) {
response.send("La requête a trouvé son bonheur");
} else {
next(); //On continue dans le cycle
}
}
//Middleware 2
function MyMiddleware2(request, response, next) {
if(request ...) {
response.send("La requête a trouvé son bonheur");
} else {
next(); //On continue dans le cycle
}
}
//Middleware final
function MyMiddlewareFinal(request, response) {
response.send("La requête ne trouve pas son bonheur, désolé!");
}
//Utilisation hypothétique des middleware
MyMiddleware1(request, response, Middleware2);
MyMiddleware2(request, response, MiddlewareFinal);
Positionnement et "scope" des middlewares
Les middlewares peuvent s'appliquer généralement de deux manières :
- De manière "globale" à travers la notation
app.use(myMiddleware)
- De manière spécifique à une route à travers la notation
app.get("/", myMiddleware, function (request, response) {...});
Dans les deux cas, il est possibile d'appliquer plusieurs middleware à la suite. De manière globale :
app.use(myMiddleware1); app.use(myMiddleware2); ...
Ou de manière spécifique à une route :
app.get("/", myMiddleware1, myMiddleware2, ..., function (request, response) { ... });
Veuillez noter que les middlewares "globaux" sont appliqués à partir de leur positionnement dans la logique de l'application, c'est-à-dire que si une route est déclarée avant l'utilisation d'un middleware "global", celui-ci ne sera pas appliqué à cette route, mais seulement à celles décalrées ensuite :
//Importer le module express
var express = require('express');
//Le module express exporte une fonction, donc associer la fonction à une variable
var app = express();
//Déclaration d'un middleware qui affiche un message "Working in progress"
function workingInProgress (request, response) {
response.send("Working in progress");
}
//Déclaration d'une route pour la homepage qui n'est pas affectée par le middleware
app.get("/", function (request, response) {
response.send("Welcome!");
});
//Application du middleware à toutes les routes suivantes
app.use(workingInProgress);
//Déclaration d'une route qui sera affectée par le middleware workingInProgress()
app.get("/about", function (request, response) {
//Ce message ne sera pas affiché, car "overruled" par le middleware global
response.send("About");
});
//Déclaration d'une autre route qui sera affectée par le middleware workingInProgress()
app.get("/contacts", function (request, response) {
//Ce message ne sera pas affiché, car "overruled" par le middleware global
response.send("Contacts");
});
//Lancer le server
app.listen(3000, console.log("Server listening to http://localhost:3000);
Voici le résultat qui sera affiché selon le type de requête faite:
- http://localhost:xxxx/ (homepage) --> Welcome
- http://localhost:xxxx/about --> Working in progress
- http://localhost:xxxx/contacts --> Working in progress
En d'autres termes, en fonction de l'architecture single-thread d'Express.js, la logique de l'application ne fait pas de distinction entre les requêtes /about et /contacts, car elles sont traités par le middleware global workingInProgress() avant qu'on puisse faire une distinction en fonction des différentes routes de type GET. Au contraire, la requête de type "/" (homepage) satisfait les conditions de la requête en evoyant une réponse avant le middleware global workingInProgress(), c'est pourquoi elle affichera le message prévu dans sa route.
Exemple: le express.static() middleware
Un bon exemple propédeutique pour comprendre le fonctionnement des middlewares concerne l’utilisation d’un middleware mis directement à disposition par le module express : le express.static() middleware. Nous avons vu dans le premier exemple d’application avec Express.js que pour accéder à une ressource particulière on utilise des routes, e.g. app.get('/url', …) avec un url différente pour chaque ressource. Il est claire que cette démarche n’est pas très pratique si on pense notamment au fait qu’un site web présente normalement plusieurs ressources et que certains d’entre elles ne sont pas des pages, mais tout simplement des fichier « statiques » tels que des images, des feuilles CSS, des fichiers JavaScript, etc. Il serait peu pratique si on devait spécifier une route pour chacune de ces ressources, parce que ceci serait en contraste avec l’utilité d’Express.js de faciliter la création d’application web.
Pour mettre en place facilement un mécanisme qui permet d’accéder à des ressources statiques de ce type, Express.js met à disposition un middleware à appliquer de manière globale qui accepte comme argument la référence absolue à un dossier qui contient les fichiers statiques. On peut mettre en place un serveur statique avec une simple ligne de code en utilisant le principe du middleware :
app.use(express.static(__dirname + '/dossier/qui/contient/les/fichiers/'));
La notation __dirname représente un simple raccourci pour identifier le dossier dans lequel se trouve le script qui contient cette ligne de code, ainsi qu’on puisse par la suite référencer le dossier qui contient les fichiers statiques en termes relatives. Imaginons que le serveur statique est censé afficher le contenu d’un dossier appelé public qui se trouve dans la même position du script qui crée l’application web, l’arborescence serait la suivante :
Dossier-application |- public |- page.html |- style.css |- image.jpg - server.js
Le code pour mettre en place le server statique serait par conséquent :
app.use(express.static(__dirname + '/public/'));
Notre dossier avec les fichiers statique contient trois fichiers, un fichier html, un fichier css et une image. La fonction du middleware consiste à créer une mécanisme qui intercepte la requête faite à l'application et contrôle s'il y a un fichier statique qui correspond à l'url de la requête. Dans ce cas:
- Une requête http://localhost:xxxx/page.html
- Une requête http://localhost:xxxx/style.css
- Une requête http://localhost:xxxx/image.jpg
De manière conceptuelle, donc, express.static() agit selon la logique suivante :
- Intercepter la requête
- Contrôler s'il existe une ressource dans le dossier des fichiers statiques dont la référence correspond à la requête
- Si une ressource de ce type existe, alors la requête est satisfaite et le contenu de ce fichier peut être envoyé en réponse
- Si une ressource de ce type n'existe pas, alors continuer avec le prochain middleware
Étant donné qu'il s'agit d'un middleware, les mêmes principes de positionnement et d'application illustrés plus haut s'applique également à express.static(). On peut illustrer un exemple. Imaginons de créer une route de ce type :
app.get("/page.html", function (request, response) { response.send("Je suis une route, pas un fichier statique"); });
La route possède un url qui est en conflit avec le fichier page.html servi par le server statique. Le type de ressource qui sera renvoyée en réponse dependra donc de la position du middleware par rapport à cette route :
- Si app.use(express.static(...)) apparait AVANT la route, alors le fichier statique sera affiché
- Si le server apparait APRES la route, alors le message "Je suis une route, pas un fichier statique" sera affiché
Veuillez noter à ce propos qu'un fichier appelé "index.html" intercepte une route de type "/", comme c'est souvent le cas dans les server traditionnels. De plus, le server statique prend automatiquement en compte également les sous-dossiers, c'est-à-dire qu'une requête avec url "/images/image1.jpg" ira cherche le fichier image1.jpg dans le dossier __dirname + "/public/images" sans avoir à déclarer un autre middleware pour le sous-dossier spécifique.
Les éléments principaux de Express.js
Express.js se base sur les éléments principaux suivants :
- L'application : l'ensemble de tous les éléments qui contribuent au fonctionnement global (i.e. la logique)
- La requête : les informations envoyées par le client à l'application;
- La réponse : les informations envoyées par l'application au client;
- Les routes : les différentes url (i.e. les "entry points") disponibles dans l'application;