« Express.js » : différence entre les versions
mAucun résumé des modifications |
|||
(16 versions intermédiaires par 3 utilisateurs non affichées) | |||
Ligne 1 : | Ligne 1 : | ||
{{tutoriel | |||
|fait_partie_du_cours=Initiation à la pensée computationnelle avec JavaScript | |||
|fait_partie_du_module=JavaScript sur le serveur | |||
|module_précédant=JavaScript dans le navigateur | |||
|module_suivant=JavaScript dans d'autres environnements | |||
|pas_afficher_sous-page=Non | |||
|page_precedente=Node.js | |||
|page_suivante=Socket.io | |||
|statut=à finaliser | |||
|difficulté=avancé | |||
|pages_prérequises=Node.js | |||
|cat tutoriels=JavaScript | |||
}} | |||
==Introduction== | ==Introduction== | ||
[http://expressjs.com/ Express.js] est un framework basé sur [[Node.js]] pour le développement d'applications web. Express.js (appelé également Express par la suite) applique les mêmes caractéristiques générales de Node.js (non-blocking I/O, event-driven, …) mais dans le cadre plus spécifique des applications web basées sur l’architecture requête/réponse. Le résultat est un framework très flexible qui permet surtout de créer des applications qui prévoient l’échange de données en temps réel entre plusieurs utilisateurs. | [http://expressjs.com/ Express.js] est un framework basé sur [[Node.js]] pour le développement d'applications web. Express.js (appelé également Express par la suite) applique les mêmes caractéristiques générales de Node.js (non-blocking I/O, event-driven, …) mais dans le cadre plus spécifique des applications web basées sur l’architecture requête/réponse. Le résultat est un framework très flexible qui permet surtout de créer des applications qui prévoient l’échange de données en temps réel entre plusieurs utilisateurs. | ||
Ligne 5 : | Ligne 18 : | ||
===Objectifs de cette page=== | ===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 | Cette page fait principalement référence au cours [[STIC I]] du Master MALTT et s'inscrit dans la perspective d'aborder les rudiments de la programmation à travers [[JavaScript]]. Plus en détail, Express.js permet de montrer de manière concrète et pragmatique les é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 est conseillée : | Pour comprendre le contenu de cette page la lecture des pages suivantes est conseillée : | ||
Ligne 14 : | Ligne 27 : | ||
===Avertissement de sécurité=== | ===Avertissement de sécurité=== | ||
[[Fichier:Warning.png| | [[Fichier:Warning.png|75px|gauche|]] | ||
Le '''même avertissement de sécurité de [[Node.js]] s'applique dans le cas d'Express.js''', car les technologies utilisées sont très similaires. | Le '''même avertissement de sécurité de [[Node.js]] s'applique dans le cas d'Express.js''', car les technologies utilisées sont très similaires. | ||
Ligne 29 : | Ligne 42 : | ||
==Installation de Express.js== | ==Installation de Express.js== | ||
La commande pour installer Express.js en tant que module local dans le dossier de votre application est la suivante : | |||
La commande pour installer Express.js en tant que module | |||
npm install express | npm install express | ||
Ligne 48 : | Ligne 52 : | ||
|- express | |- express | ||
|- votre_fichier_node_principale.js | |- votre_fichier_node_principale.js | ||
===Importer le module dans votre application=== | ===Importer le module dans votre application=== | ||
Ligne 63 : | Ligne 59 : | ||
var express = require('express'); | var express = require('express'); | ||
Contrairement à d'autres modules tels que <code>fs</code> | Contrairement à d'autres modules tels que <code>fs</code>, 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 <code>app</code>) qui représente l'application web elle-même : | ||
//Importer le module express | //Importer le module express | ||
Ligne 93 : | Ligne 89 : | ||
</source> | </source> | ||
Le changement plus important par rapport à un simple serveur web avec http concerne l'utilisation des "routes" avec la méthode <code>app.get()</code> 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 : | Le changement le plus important, par rapport à un simple serveur web avec http, concerne l'utilisation des "routes" avec la méthode <code>app.get()</code> 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 : | ||
[[Fichier:Express.js-simple-web-app.png|400px|vignette|néant|Simple application avec Express.js.]] | [[Fichier:Express.js-simple-web-app.png|400px|vignette|néant|Simple application avec Express.js.]] | ||
Ligne 101 : | Ligne 97 : | ||
==Comprendre Express.js== | ==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 | 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 d'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=== | ===Le rôle de l'interprète=== | ||
Ligne 108 : | Ligne 104 : | ||
====Cycle requête/réponse avec un server web traditionnel==== | ====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 serveur comme Apache nécessite | 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 serveur comme Apache nécessite 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" : | ||
[[Fichier:Express.js-normal-web-server.png|none|thumb|500x500px|Cycle requête/réponse avec un server HTTP "traditionnel".]] | [[Fichier:Express.js-normal-web-server.png|none|thumb|500x500px|Cycle requête/réponse avec un server HTTP "traditionnel".]] | ||
Ligne 121 : | Ligne 117 : | ||
====Multi-thread: une copie pour chaque ressource==== | ====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 | 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ée, cette modification ne sera pas disponible dans l'autre requête simultanée. Voici un schéma illustrant une architecture Multi-thread: | ||
[[Fichier:Express.js-multi-thread.png|600px|vignette|néant|Architecture Multi-thread d'un server web : chaque requête reçoit une "copie" de la ressource demandée.]] | [[Fichier:Express.js-multi-thread.png|600px|vignette|néant|Architecture Multi-thread d'un server web : chaque requête reçoit une "copie" de la ressource demandée.]] | ||
Ligne 127 : | Ligne 123 : | ||
====Single thread: la même ressource pour tous==== | ====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 | 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 non à des copies. Cela se traduit par le fait qu'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 passée en deuxième. Voici un schéma illustrant l'architecture Single-thread: | ||
[[Fichier:Express.js-single-thread.png|600px|vignette|néant|Architecture Single-thread d'Express.js : le server traite de manière centralisée toutes les requêtes, qui ont par conséquent accès à la même ressources (dans son état le plus actuel).]] | [[Fichier:Express.js-single-thread.png|600px|vignette|néant|Architecture Single-thread d'Express.js : le server traite de manière centralisée toutes les requêtes, qui ont par conséquent accès à la même ressources (dans son état le plus actuel).]] | ||
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' | 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. l'une requête la page /about/ et l'autre la page /contact/). Ce mécanisme fait ainsi que toutes les requêtes qui arrivent sur un server Express.js sont traitées de manière centralisée et ont donc accès à la version la plus récente de toutes les données qui concerne toutes les ressources de l'application. | ||
===Le concept de "middleware"=== | ===Le concept de "middleware"=== | ||
Ligne 137 : | Ligne 133 : | ||
====Définition générale==== | ====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 | 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évus 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 asynchrone. 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. | 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 | Les trois principales interactions que peut réaliser un middleware sont l'échange de message, l'appel de procédure et la manipulation d'objets. | ||
====Les middleware dans Express.js==== | ====Les middleware dans Express.js==== | ||
Ligne 147 : | Ligne 143 : | ||
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 ne reçoive une réponse. Donc, il y a des mécanismes intermédiaires entre la requête et la réponse qui sous-tendent la satisfaction de la requête avec la réponse appropriée. | 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 ne reçoive une réponse. Donc, il y a des mécanismes intermédiaires 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, | 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, entre autres, de : | ||
* Intercepter et identifier quel type de requête a été faite, par exemple : | * Intercepter et identifier quel type de requête a été faite, par exemple : | ||
Ligne 160 : | Ligne 156 : | ||
====Une chaîne séquentielle de middleware==== | ====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 à | Une application web avec Express.js se compose normalement de plusieurs middleware qui ont justement le rôle d'évaluer la requête et d'envoyer, selon la logique de l'application, la réponse associée à une requête de ce type. Pour déterminer la logique de l'application, on fait recours à plusieurs middlewares représentant des étapes séquentielles. Voici une représentation schématique du cycle qui se crée : | ||
# Le server intercepte une requête | # Le server intercepte une requête | ||
Ligne 174 : | Ligne 170 : | ||
# Le prochain middleware qui doit être exécuté avec une fonction de callback (e.g. next()). | # 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 | 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 de réponse, ce qui se traduit normalement par une page "loading" en continu. 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: | Voici le code "prototypique" d'une suite de middleware: | ||
Ligne 225 : | Ligne 221 : | ||
app.get("/", myMiddleware1, myMiddleware2, ..., function (request, response) { ... }); | 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 | 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éclarées ensuite : | ||
<source lang="JavaScript"> | <source lang="JavaScript"> | ||
Ligne 268 : | Ligne 264 : | ||
* http://localhost:xxxx/contacts --> 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 | 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ées par le middleware global workingInProgress() avant qu'on ne 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 envoyant 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==== | ====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. | 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 | 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érent pour chaque ressource. Il est clair 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 certaines d’entre elles ne sont pas des pages, mais tout simplement des fichiers « 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 : | 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 : | ||
Ligne 279 : | Ligne 275 : | ||
app.use(express.static(__dirname + '/dossier/qui/contient/les/fichiers/')); | 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 | 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 relatifs. Imaginons que le serveur statique soit 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 | Dossier-application | ||
Ligne 292 : | Ligne 288 : | ||
app.use(express.static(__dirname + '/public/')); | app.use(express.static(__dirname + '/public/')); | ||
Notre dossier avec les fichiers | Notre dossier avec les fichiers statiques 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. Veuillez noter que le nom du dossier ne doit pas faire partie de l'url de la requête, donc dans ce cas les ressources statiques seraient accessibles aux adresses suivantes : | ||
* Une requête http://localhost:xxxx/page.html | * Une requête http://localhost:xxxx/page.html | ||
Ligne 305 : | Ligne 301 : | ||
** Si une ressource de ce type n'existe pas, alors continuer avec le prochain middleware | ** 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' | Étant donné qu'il s'agit d'un middleware, les mêmes principes de positionnement et d'application illustrés plus haut s'appliquent également à express.static(). On peut illustrer un exemple. Imaginons la création d'une route de ce type : | ||
app.get("/page.html", function (request, response) { | app.get("/page.html", function (request, response) { | ||
Ligne 311 : | Ligne 307 : | ||
}); | }); | ||
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 | 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 dépendra 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 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é | * 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 | Veuillez noter à ce propos qu'un fichier appelé "index.html" intercepte une route de type "/", comme c'est souvent le cas dans les serveurs traditionnels. De plus, le serveur statique prend aussi automatiquement en compte les sous-dossiers, c'est-à-dire qu'une requête avec url "/images/image1.jpg" ira chercher le fichier image1.jpg dans le dossier __dirname + "/public/images" sans avoir à déclarer un autre middleware pour le sous-dossier spécifique. | ||
* Voir la [http://expressjs.com/en/starter/static-files.html documentation sur le site officiel] pour plus de détails sur express.static() | * Voir la [http://expressjs.com/en/starter/static-files.html documentation sur le site officiel] pour plus de détails sur express.static() | ||
Ligne 328 : | Ligne 324 : | ||
* Les '''middlewar'''e : | * Les '''middlewar'''e : | ||
** sont tout simplement des fonctions qui sont exécutées de manière séquentielle selon leur position dans le code de l’application. La chaine de middleware s’arrête exclusivement lorsqu’un middleware envoie une réponse au client qui a fait la requête. | ** sont tout simplement des fonctions qui sont exécutées de manière séquentielle selon leur position dans le code de l’application. La chaine de middleware s’arrête exclusivement lorsqu’un middleware envoie une réponse au client qui a fait la requête. | ||
** peuvent exécuter tout type d’opération. Ces opérations sont souvent en relation avec la requête (e.g. contrôler si la requête répond à | ** peuvent exécuter tout type d’opération. Ces opérations sont souvent en relation avec la requête (e.g. contrôler si la requête répond à certaines conditions, comme par exemple si l’utilisateur dispose d’un cookie d’authentification), mais peuvent également être en lien avec d’autres éléments d’une application (e.g. interroger une base de données, écrire le contenu d’un fichier, etc.). | ||
* Les '''routes''' : | * Les '''routes''' : | ||
** représentent une forme de « middleware | ** représentent une forme de « middleware final », car elles sont censées renvoyer une réponse en fonction de l’url spécifié par la requête. Les routes peuvent donc être considérées comme des « sorties » du cycle de middleware une fois que la requête du client a été satisfaite. | ||
** définissent normalement des pages considérées "dynamiques", c'est-à-dire dont le contenu est créé en fonction de | ** définissent normalement des pages considérées "dynamiques", c'est-à-dire dont le contenu est créé en fonction de certaines conditions ou en assemblant différentes sources de données, pas tout simplement des fichiers html statiques. | ||
[[Fichier:Express.js-synthese-schema.png|600px|vignette|néant|Workflow d'une application Express.js avec l'articulation entre middleware et routes]] | [[Fichier:Express.js-synthese-schema.png|600px|vignette|néant|Workflow d'une application Express.js avec l'articulation entre middleware et routes]] | ||
Ligne 338 : | Ligne 334 : | ||
* Voir la documentation sur comment [http://expressjs.com/en/guide/writing-middleware.html écrire un middleware] sur le site officiel | * Voir la documentation sur comment [http://expressjs.com/en/guide/writing-middleware.html écrire un middleware] sur le site officiel | ||
* Voir la documentation sur comment [http://expressjs.com/en/guide/using-middleware.html utiliser un middleware] sur le site officiel (avec exemples | * Voir la documentation sur comment [http://expressjs.com/en/guide/using-middleware.html utiliser un middleware] sur le site officiel (avec exemples avancés) | ||
* Voir la documentation sur [http://expressjs.com/en/guide/routing.html les routes] sur le site officiel | * Voir la documentation sur [http://expressjs.com/en/guide/routing.html les routes] sur le site officiel | ||
Ligne 345 : | Ligne 341 : | ||
Cette section présente un aperçu des éléments principaux du framework. Il s’agit d’une adaptation par rapport à la [http://expressjs.com/en/4x/api.html documentation de l’API] (version 4.x) disponible sur le site officiel dans la mesure où : | Cette section présente un aperçu des éléments principaux du framework. Il s’agit d’une adaptation par rapport à la [http://expressjs.com/en/4x/api.html documentation de l’API] (version 4.x) disponible sur le site officiel dans la mesure où : | ||
* Seulement | * Seulement certaines propriétés et méthodes seront illustrées, celles qui ont le plus de probabilités d’être utilisées pour des débutants/novices | ||
* Les aspects purement techniques d’utilisation des propriétés et méthodes seront | * Les aspects purement techniques d’utilisation des propriétés et des méthodes seront implémentées par des explications théoriques se référant à la section « Comprendre Express.js ». | ||
Express.js se base sur les éléments principaux suivants : | Express.js se base sur les éléments principaux suivants : | ||
Ligne 353 : | Ligne 349 : | ||
# La '''requête''' : les informations envoyées par le client à l'application; | # La '''requête''' : les informations envoyées par le client à l'application; | ||
# La '''réponse''' : les informations envoyées par l'application au client; | # La '''réponse''' : les informations envoyées par l'application au client; | ||
# Le '''router''' : des "sous- | # Le '''router''' : des "sous-systèmes" de routes qui peuvent être ajoutées à l'application | ||
===L'application=== | ===L'application=== | ||
L’application se traduit généralement par la variable var app = express(), c’est-à-dire un objet/fonction qui présente les propriétés et les méthodes principales pour créer une application web. Voici une liste non exhaustive des éléments principaux | L’application se traduit généralement par la variable var app = express(), c’est-à-dire un objet/fonction qui présente les propriétés et les méthodes principales pour créer une application web. Voici une liste non exhaustive des éléments principaux associés à l’objet app. | ||
;app.get() | ;app.get() | ||
Ligne 367 : | Ligne 363 : | ||
;app.post() | ;app.post() | ||
Cette méthode permet d'intercepter une requête de type POST avec un path spécifique et d'y associer une fonction de callback qui, normalement, gère les données qui ont été | Cette méthode permet d'intercepter une requête de type POST avec un path spécifique et d'y associer une fonction de callback qui, normalement, gère les données qui ont été envoyées en POST (e.g. à travers un formulaire web). Pour le traitement de ces données, on utilise souvent un module spécifique qui permet d’accéder facilement aux éléments de la requête POST (voir le module '''body-parser''' plus bas dans la section suivante). Exemple : | ||
app.post('/submit', function (request, response) { | app.post('/submit', function (request, response) { | ||
Ligne 374 : | Ligne 370 : | ||
;app.all() | ;app.all() | ||
Cette méthode permet d'intercepter tout type de requête HTTP (GET, POST, PUT, DELETE, ...) avec un path qui peut | Cette méthode permet d'intercepter tout type de requête HTTP (GET, POST, PUT, DELETE, ...) avec un path qui peut être également de plus haut niveau par rapport à une route spécifique. Par exemple, cette méthode applique un middleware myMiddleware() à toute route qui commence avec /admin/ (donc par exemple /admin/index.html, /admin/dashboard/, /admin/blog/new/) : | ||
app.all('/admin/*', myMiddleware); | app.all('/admin/*', myMiddleware); | ||
;app.param() | ;app.param() | ||
Cette méthode permet d'exécuter un middleware pas sur la base d'une route spécifique ou | Cette méthode permet d'exécuter un middleware non pas sur la base d'une route spécifique ou d'un plus haut niveau, mais sur la base d'un paramètre dynamique contenu dans la requête. Un paramètre dynamique est associé à une route avec la notation :param comme par exemple dans la route GET suivante : | ||
app.get('/students/profile/:student_id', function (request, response) { | app.get('/students/profile/:student_id', function (request, response) { | ||
Ligne 392 : | Ligne 388 : | ||
}); | }); | ||
Au lieu de récupérer le nom et le prénom de l’étudiant dans les deux routes spécifiques qui utilisent le paramètre student_id, on peut plutôt créer une sorte de « règle » selon laquelle à chaque fois qu’une route propose ce paramètre, il est très probable qu’on | Au lieu de récupérer le nom et le prénom de l’étudiant dans les deux routes spécifiques qui utilisent le paramètre student_id, on peut plutôt créer une sorte de « règle » selon laquelle à chaque fois qu’une route propose ce paramètre, il est très probable qu’on ait besoin de ces informations. On peut donc créer un middleware basé directement sur le paramètre : | ||
app.param('student_id', function (request, response, next) { | app.param('student_id', function (request, response, next) { | ||
Ligne 431 : | Ligne 427 : | ||
;app.listen() | ;app.listen() | ||
Cette méthode s'applique normalement à la fin du fichier qui représente l'application car il s'agit de la méthode qui crée le server web. Il s'agit en effet d'une méthode qui fait appel au module http.createServer de Node.js, en lui passant la variable app (qu'on le rappelle, c'est une fonction de type express()) pour gérer le mécanisme requête/réponse du module http. La méthode accepte plusieurs arguments, en phase de test et développement en local il est bien de passer le nombre d'une porte supérieur à 3000 pour éviter des conflits avec d'autres portes utilisés par d'autres services sur votre | Cette méthode s'applique normalement à la fin du fichier qui représente l'application car il s'agit de la méthode qui crée le server web. Il s'agit en effet d'une méthode qui fait appel au module http.createServer de Node.js, en lui passant la variable app (qu'on le rappelle, c'est une fonction de type express()) pour gérer le mécanisme requête/réponse du module http. La méthode accepte plusieurs arguments, en phase de test et développement en local il est bien de passer le nombre d'une porte supérieur à 3000 pour éviter des conflits avec d'autres portes utilisés par d'autres services sur votre ordinateur. Exemple : | ||
app.listen(3000); | app.listen(3000); | ||
Il est | Il est également recommandé de passer une deuxième fonction avec tout simplement un console.log pour être sûr que le server web marche : | ||
app.listen(3000, console.log("Server listening to http://localhost:3000")); | app.listen(3000, console.log("Server listening to http://localhost:3000")); | ||
===La requête=== | ===La requête=== | ||
L’objet requête correspond à une « HTTP request » qui est faite par le client sur le server créé avec l’application express(). L’objet requête propose plusieurs propriétés et méthodes qui permettent d’évaluer de manière plus approfondie la requête par rapport à un | L’objet requête correspond à une « HTTP request » qui est faite par le client sur le server créé avec l’application express(). L’objet requête propose plusieurs propriétés et méthodes qui permettent d’évaluer de manière plus approfondie la requête par rapport à un serveur statique, où il y a tout simplement une équivalence entre l’URL d’accès et le path de la ressource statique. Avec Node.js/Express.js, la requête peut être analysée par rapport à d’autres informations qui permettent de la définir de manière à la fois plus spécifique et plus étendue. On peut par exemple discriminer la requête par rapport à : | ||
* Des données qui sont | * Des données qui sont envoyées dans le « body » de la requête telles que des données envoyées par un formulaire web | ||
* Des cookies qui permettent d’identifier si l’utilisateur a fait le login sur une page | * Des cookies qui permettent d’identifier si l’utilisateur a fait le login sur une page | ||
* L’adresse IP du client | * L’adresse IP du client | ||
* Le query string (e.g. des paramètres qui sont | * Le query string (e.g. des paramètres qui sont envoyés sous forme d’associations clés/valeurs comme dans l’url http://localhost:3000/students.html?order=lastname&sort=asc&limit=10) | ||
* Etc. | * Etc. | ||
Veuillez noter deux choses importantes à propos de l’objet requête en Express.js : | Veuillez noter deux choses importantes à propos de l’objet requête en Express.js : | ||
# Dans cette page on fait référence à cet objet avec le nom <code>request</code>, tandis que souvent dans d’autres tutoriels et même dans la documentation officielle on utilise l’abréviation <code>req</code>. Vu que cet objet est passé en tant qu’argument dans les middleware de callback, en réalité son nom n’est pas important à condition d’être | # Dans cette page on fait référence à cet objet avec le nom <code>request</code>, tandis que souvent dans d’autres tutoriels et même dans la documentation officielle on utilise l’abréviation <code>req</code>. Vu que cet objet est passé en tant qu’argument dans les middleware de callback, en réalité son nom n’est pas important à condition d’être consistant dans le choix. | ||
# Avec la version 4.x certains éléments de la requête sont gérés par des modules externes qu’il faut importer (i.e. utiliser la fonction require()) pour qu’ils soient plus facilement accessibles à travers l’objet requête. C’est le cas notamment des données envoyées dans le body, les cookies, les fichiers à uploader, etc. Certains des modules les plus utiles dans ce cas seront brièvement abordés directement ici, mais une liste plus exhaustive est disponible plus bas dans la page. | # Avec la version 4.x certains éléments de la requête sont gérés par des modules externes qu’il faut importer (i.e. utiliser la fonction require()) pour qu’ils soient plus facilement accessibles à travers l’objet requête. C’est le cas notamment des données envoyées dans le body, les cookies, les fichiers à uploader, etc. Certains des modules les plus utiles dans ce cas seront brièvement abordés directement ici, mais une liste plus exhaustive est disponible plus bas dans la page. | ||
Voici | Voici, ci-dessous, une liste des propriétés et des méthodes principales liées à l’objet requête. | ||
;request.path | ;request.path | ||
Cette propriété permet d’accéder au path associé à une requête. Dans un server web statique, le path correspond au chemin d’une ressource par rapport au dossier « root » du server, ce qui est d’ailleurs également le cas pour le express.static() middleware présenté plus haut dans la page. Cependant, dans Express.js le path assume plutôt une valeur sémantique qui permet d’organiser les routes en fonction de ce qu’elles sont censées représenter, plutôt que leur position physique. | Cette propriété permet d’accéder au path associé à une requête. Dans un server web statique, le path correspond au chemin d’une ressource par rapport au dossier « root » du server, ce qui est d’ailleurs également le cas pour le express.static() middleware présenté plus haut dans la page. Cependant, dans Express.js le path assume plutôt une valeur sémantique qui permet d’organiser les routes en fonction de ce qu’elles sont censées représenter, plutôt que leur position physique. | ||
Dans | Dans l’adresse http://localhost:3000/users l’objet request.path correspond à /users. On peut facilement déduire que cet objet est utilisé dans le cas de la méthode app.get() vue plus haut dans cette section. Cette méthode, en effet, ne fait que comparer la valeur de l’objet request.path avec la route proposée dans la méthode app.get(‘/users’, …). Si les deux correspondent, alors la fonction de callback sera exécutée et la réponse envoyée au client. | ||
;request.params | ;request.params | ||
Cette propriété est | Cette propriété est associée à un objet qui contient les associations clés/valeurs des paramètres dynamiques utilisés dans les routes. Un paramètre dynamique est une partie du path qui peut varier sans néanmoins changer la route qui va gérer la fonction de callback. Imaginons une route de ce type : | ||
app.get('/places/:country/:region/:city', function (request, response) { | app.get('/places/:country/:region/:city', function (request, response) { | ||
Ligne 475 : | Ligne 471 : | ||
} | } | ||
La réponse sera donc "Welcome to lugano, ticino, switzerland!!". Avec un URL de type http://localhost:3000/places/italy/lombardia/milano/ la réponse sera plutôt "Welcome to milano, lombardia, italy!!". Dans les deux cas, la même route sera exécutée, même si les path diffèrent dans leur structure, parce qu'ils présentent la même structure | La réponse sera donc "Welcome to lugano, ticino, switzerland!!". Avec un URL de type http://localhost:3000/places/italy/lombardia/milano/ la réponse sera plutôt "Welcome to milano, lombardia, italy!!". Dans les deux cas, la même route sera exécutée, même si les path diffèrent dans leur structure, parce qu'ils présentent la même structure paramétrique. | ||
;request.query | ;request.query | ||
Ligne 482 : | Ligne 478 : | ||
http://localhost:3000/students/list.html?order=lastname&sort=asc&start=0&limit=20 | http://localhost:3000/students/list.html?order=lastname&sort=asc&start=0&limit=20 | ||
Le query string commence après le caractère ? et chaque association clé/valeur | Le query string commence après le caractère ? et chaque association clé/valeur est séparée par le caractère &. L'association se fait avec le symbole d'affectation = utilisé également dans JavaScript selon la notation key = value. Dans cet exemple de url, on aurait un objet query.string qui contient ces valeurs : | ||
request.query = { | request.query = { | ||
Ligne 491 : | Ligne 487 : | ||
} | } | ||
La différence entre paramètres et query string peut parfois être arbitraire. De manière générale, on utilise les paramètres lorsqu'on connait à l'avance le nombre spécifique d'informations variables qui seront transmises à travers l'url et lorsque les paramètres sont obligatoires. Le query string est par contre utile lorsque ce nombre n'est pas connu à l'avance ou peut varier. On pourrait très bien imaginer d' | La différence entre paramètres et query string peut parfois être arbitraire. De manière générale, on utilise les paramètres lorsqu'on connait à l'avance le nombre spécifique d'informations variables qui seront transmises à travers l'url et lorsque les paramètres sont obligatoires. Le query string est par contre utile lorsque ce nombre n'est pas connu à l'avance ou peut varier. On pourrait très bien imaginer d'accéder à la page http://localhost:3000/students/list.html sans spécifier de paramètres et la page affichera automatiquement les premiers 20 étudiants triés par leur nom de famille. | ||
;request.body | ;request.body | ||
Cette propriété est associée aux éléments qui sont passés dans le body d'une requête, par exemple à travers la méthode POST d'un formulaire HTML. À partir d'Express.js 4.x. cette propriété résulte <code>undefined</code> et il est nécessaire d'utiliser un middleware pour que les données passées à travers le body de la requête | Cette propriété est associée aux éléments qui sont passés dans le body d'une requête, par exemple à travers la méthode POST d'un formulaire HTML. À partir d'Express.js 4.x. cette propriété résulte <code>undefined</code> et il est nécessaire d'utiliser un middleware pour que les données passées à travers le body de la requête soient élaborées pour donner un objet comme dans le cas de request.path ou request.query. Il faut également noter à ce sujet que les données qu'on peut passer à travers le body peuvent être plus complexes et nombreuses, d'où l'intérêt d'utiliser un middleware qui prend en charge cette opération comme par exemple: | ||
* Le module body-parser qui permet de gérer différents types de données dans le body (e.g. form, JSON, etc.) | * Le module body-parser qui permet de gérer différents types de données dans le body (e.g. form, JSON, etc.) | ||
Ligne 508 : | Ligne 504 : | ||
===La réponse=== | ===La réponse=== | ||
L’objet réponse correspond à une « HTTP response » que le | L’objet réponse correspond à une « HTTP response » que le serveur envoie au client qui a fait une requête à l’application express(). L’objet réponse possède plusieurs propriétés et méthodes qui permettent d’adapter les deux éléments principaux de la réponse : les headers (i.e. les entêtes) et le body (i.e. le contenu, très souvent du HTML). | ||
Comme c’était le cas pour la requête qui est souvent abrégée en <code>req</code> dans les tutoriels et documentation officielle de Express.js, la réponse (i.e. response) figure souvent avec la notation <code>res</code>. Pour une question de clarté, cette page utilise le nom complet <code>response</code>, mais cela est seulement conventionnel. | Comme c’était le cas pour la requête qui est souvent abrégée en <code>req</code> dans les tutoriels et la documentation officielle de Express.js, la réponse (i.e. response) figure souvent avec la notation <code>res</code>. Pour une question de clarté, cette page utilise le nom complet <code>response</code>, mais cela est seulement conventionnel. | ||
Voici | Voici, ci-dessous, une liste non exhaustive des propriétés et méthodes principales associées à la réponse. | ||
;response.send() | ;response.send() | ||
Ligne 540 : | Ligne 536 : | ||
}); | }); | ||
Contrairement à l’exemple précédent, dans ce cas le Content-Type de la | Contrairement à l’exemple précédent, dans ce cas le Content-Type de la réponse sera application/json plutôt que text/html, et le contenu sera automatiquement affiché en tant que JSON : | ||
{"ex1":"4.5","ex2":"4.75","ex3":"5.5"} | {"ex1":"4.5","ex2":"4.75","ex3":"5.5"} | ||
Pour envoyer du JSON il existe également une méthode spécifique '''response.json()''' qui marche pratiquement de la même manière, | Pour envoyer du JSON il existe également une méthode spécifique '''response.json()''' qui marche pratiquement de la même manière, à quelques exceptions près. | ||
;response.sendFile() | ;response.sendFile() | ||
Cette méthode permet d'envoyer en | Cette méthode permet d'envoyer en réponse le contenu d'un fichier. Le fichier peut être une page HTML, mais également un fichier PDF ou d'autres types de format qui est généralement supporté par des navigateurs web. Exemple : | ||
app.get("/", function (request, response) { | app.get("/", function (request, response) { | ||
Ligne 555 : | Ligne 551 : | ||
}); | }); | ||
L'intérêt de pouvoir renvoyer un fichier tel qu'il est, mais sans utiliser le middleware express.static(), peut concerner par exemple le contrôle sur un fichier PDF: si la requête présente | L'intérêt de pouvoir renvoyer un fichier tel qu'il est, mais sans utiliser le middleware express.static(), peut concerner par exemple le contrôle sur un fichier PDF: si la requête présente certaines caractéristiques (e.g. utilisateur a fait le login), alors le fichier sera disponible, autrement on peut afficher un message d'erreur. La méthode <code>sendFile()</code> accepte également des options, vous pouvez vous référez à la documentation officielle pour plus de détails. | ||
;response.redirect() | ;response.redirect() | ||
Cette méthode permet de | Cette méthode permet de rediriger la requête vers une autre destination qui peut être interne (e.g. une autre route) ou externe à l'application (e.g. un autre site). Exemples: | ||
app.get("/internal", function (request, response) { | app.get("/internal", function (request, response) { | ||
response.redirect('/otherpage'); | response.redirect('/otherpage'); | ||
Ligne 567 : | Ligne 563 : | ||
;response.status() | ;response.status() | ||
Cette méthode permet de définir un code de | Cette méthode permet de définir un code de statut (e.g. 200, 404, etc.) pour la réponse. Il est souvent utilisé avec la méthode <code>response.send()</code>. Par exemple : | ||
app.get("/profile/:user", function (request, response) { | app.get("/profile/:user", function (request, response) { | ||
//Si on ne trouve pas un utilisateur avec ce nom | //Si on ne trouve pas un utilisateur avec ce nom | ||
Ligne 577 : | Ligne 573 : | ||
# La référence à la view qui va servir de template | # La référence à la view qui va servir de template | ||
# Un objet avec associations | # Un objet avec associations clés/valeurs des données à passer | ||
Voici un exemple théorique, pour des exemples pratiques voir la section sur les middleswares utiles plus bas : | Voici un exemple théorique, pour des exemples pratiques voir la section sur les middleswares utiles plus bas : | ||
Ligne 659 : | Ligne 655 : | ||
==Modules/Middleware utiles== | ==Modules/Middleware utiles== | ||
La philosophie d’Express.js est volontairement très minimaliste, ce qui se reflète principalement sur deux aspects reliés : | La philosophie d’Express.js est volontairement très minimaliste, ce qui se reflète principalement sur deux aspects reliés : | ||
#Le framework propose « out of the box » juste les éléments essentiels, mais dès que des fonctionnalités plus avancées sont nécessaires, il faut implémenter des éléments « externes » (parfois ces éléments faisaient partie de versions précédentes à la version 4, mais ont été | #Le framework propose « out of the box » juste les éléments essentiels, mais dès que des fonctionnalités plus avancées sont nécessaires, il faut implémenter des éléments « externes » (parfois ces éléments faisaient partie de versions précédentes à la version 4, mais ont été enlevés justement en raison de cette philosophie) ; | ||
#Le développeur peut donc choisir quels éléments ajouter et parmi les éléments mêmes il a normalement le choix entre plusieurs modules qui proposent des fonctionnalités similaires mais avec des différences au niveau de la syntaxe, du fonctionnement interne, etc. Par exemple, Express.js ne propose aucun « template engine », mais le développeur peut choisir parmi une longue liste de possibilité selon ses préférences. | #Le développeur peut donc choisir quels éléments ajouter et parmi les éléments mêmes il a normalement le choix entre plusieurs modules qui proposent des fonctionnalités similaires mais avec des différences au niveau de la syntaxe, du fonctionnement interne, etc. Par exemple, Express.js ne propose aucun « template engine », mais le développeur peut choisir parmi une longue liste de possibilité selon ses préférences. | ||
Ligne 677 : | Ligne 673 : | ||
var bodyParser = require('body-parser'); | var bodyParser = require('body-parser'); | ||
Ce module peut être adapté avec plusieurs options, dans l'exemple qui suit nous allons mettre en place une configuration de base pour ajouter de manière globale les éléments d'un formulaire web à l'object <code>request.body</code>. Imaginons d'avoir une page web qui contient le formulaire suivant, qui sert | Ce module peut être adapté avec plusieurs options, dans l'exemple qui suit nous allons mettre en place une configuration de base pour ajouter de manière globale les éléments d'un formulaire web à l'object <code>request.body</code>. Imaginons d'avoir une page web qui contient le formulaire suivant, qui sert pour ajouter une note obtenue pour l'un des exercices du cours STIC I: | ||
<source lang="HTML5"> | <source lang="HTML5"> | ||
Ligne 692 : | Ligne 688 : | ||
#Le champ '''grade''' de type "number" | #Le champ '''grade''' de type "number" | ||
Voici comment on peut facilement gérer les informations envoyées à la route "/add" en utilisant la méthode <code>urlencoded()</code> du module body-parser qui permet de | Voici comment on peut facilement gérer les informations envoyées à la route "/add" en utilisant la méthode <code>urlencoded()</code> du module body-parser qui permet de récupérer le contenu des deux champs à travers l'objet <code>request.body</code> : | ||
<source lang="JavaScript"> | <source lang="JavaScript"> | ||
Ligne 725 : | Ligne 721 : | ||
app.use(cookieParser()) | app.use(cookieParser()) | ||
À ce | À ce stade, vous avez accès aux cookies envoyés par la requête de votre application dans l'objet <code>request.cookies</code>. Par exemple imaginons que votre application nécessite du cookie 'secretKey' pour identifier un utilisateur, la valeur sera accessible à travers la notation : | ||
request.cookies.secretKey | request.cookies.secretKey | ||
Pour ajouter la valeur d' | Pour ajouter la valeur d'un cookie il faut par contre utiliser la notation suivante : | ||
response.cookie('NomDuCookie', 'valeur du cookie'); | response.cookie('NomDuCookie', 'valeur du cookie'); | ||
===Template engines=== | ===Template engines=== | ||
L'un des avantages d'une page dynamique par rapport à une page HTML statique consiste | L'un des avantages d'une page dynamique par rapport à une page HTML statique consiste en la possibilité d'intégrer des données qui sont tirées d'autres sources par rapport au code "écrit à la main" par le développeur. L'application plus répandue dans ce sens concerne l'intégration de données tirées d'une base de données (e.g. un blog, un forum, etc.). Pour faciliter ce type d'intégration, Express.js met à disposition des '''template engines''', c'est-à-dire des fichiers qui contiennent : | ||
#Du HTML "traditionnel" pour gérer la structure de la page | #Du HTML "traditionnel" pour gérer la structure de la page | ||
#Une syntaxe particulière qui permet d' | #Une syntaxe particulière qui permet d'intégrer des données, par exemple sous forme de "placeholder" pour une variable ou de cycle pour l'itération d'un array ou d'un objet | ||
Lorsqu'on utilise un template engine, le mécanisme de création d'une page suit les étapes suivantes : | Lorsqu'on utilise un template engine, le mécanisme de création d'une page suit les étapes suivantes : | ||
Ligne 755 : | Ligne 751 : | ||
//Définir où se trouvent les "views" | //Définir où se trouvent les "views" | ||
app.set('views', './directory/avec/les/fichiers/template') | app.set('views', './directory/avec/les/fichiers/template') | ||
// | //Définir quel template engine utiliser | ||
app.set('view engine', 'NomDuTemplateEngine') | app.set('view engine', 'NomDuTemplateEngine') | ||
Dans l'esprit "non-opinionated" d'Express.js, il n'existe pas | Dans l'esprit "non-opinionated" d'Express.js, il n'existe pas de template engine associé automatiquement à l'application. Au contraire, il en existe plusieurs parmi lesquels le développeur peut choisir. De ce fait, il faudra d'abord installer le module du template engine avec la commande | ||
npm install NomDuModuleTemplateEngine | npm install NomDuModuleTemplateEngine | ||
Chaque template engine propose sa propre syntaxe et les views sont normalement associées à une extension déterminée par le template engine. Par exemple un template engine très utilisé avec Express.js s'appelle '''jade''' et les views sont représentées par des fichiers du style '''myView.jade'''. L'illustration des | Chaque template engine propose sa propre syntaxe et les views sont normalement associées à une extension déterminée par le template engine. Par exemple, un template engine très utilisé avec Express.js s'appelle '''jade''' et les views sont représentées par des fichiers du style '''myView.jade'''. L'illustration des différents templates engines et leur syntaxe va au-delà des objectifs de cette page. Nous nous limiterons à proposer deux exemples avec deux engines différents. Pour plus d'informations et pour une liste des engines disponibles, voir la [http://expressjs.com/en/guide/using-template-engines.html documentation officielle sur les template engines]. | ||
====Jade==== | ====Jade==== | ||
Ligne 774 : | Ligne 770 : | ||
app.set('view engine', 'jade'); | app.set('view engine', 'jade'); | ||
Imaginons à ce point | Imaginons à ce point l'utilisation d'une view "homepage.jade" pour rendre la réponse de la route "/" d'une application. D'abord l'arborescence de notre application doit être la suivante : | ||
|- node_modules | |- node_modules | ||
Ligne 839 : | Ligne 835 : | ||
</html> | </html> | ||
</source> | </source> | ||
* Pour plus de détails voir ce [http://www.sitepoint.com/jade-tutorial-for-beginners/ Tutoriel sur SitePoint] | |||
* Un [http://codepen.io/mafritz/pen/pgmbJG template de site personnel] qui utilise [[Bootstrap]] | |||
====EJS==== | ====EJS==== | ||
Ligne 845 : | Ligne 844 : | ||
npm install ejs | npm install ejs | ||
La démarche est ensuite similaire à celle | La démarche est ensuite similaire à celle de jade. Voici un exemple complet de page avec EJS. Code pour l'application : | ||
<source lang="JavaScript"> | <source lang="JavaScript"> | ||
Ligne 915 : | Ligne 914 : | ||
</source> | </source> | ||
Vous pouvez noter comme cette syntaxe est beaucoup plus proche du HTML traditionnel, avec les variables qui sont contenues dans des " | Vous pouvez noter comme cette syntaxe est beaucoup plus proche du HTML traditionnel, avec les variables qui sont contenues dans des "blocs" de type : | ||
* <%= ... %> pour le output | * <%= ... %> pour le output | ||
Ligne 957 : | Ligne 956 : | ||
===Intégration avec des bases de données=== | ===Intégration avec des bases de données=== | ||
Les applications Express.js peuvent facilement être liées à des bases | Les applications Express.js peuvent facilement être liées à des bases de données à travers des modules propres à différents types de databases tels que : | ||
* MySQL | * MySQL | ||
Ligne 964 : | Ligne 963 : | ||
* ... | * ... | ||
Veuillez noter que les modules installent exclusivement des "couches" | Veuillez noter que les modules installent exclusivement des "couches" intermédiaires qui permettent de lier Node.js avec les bases de données, mais les logiciels qui supportent les bases de données doivent déjà être disponibles sur le système. | ||
Pour plus d'informations consulter le [http://expressjs.com/en/guide/database-integration.html guide sur l'intégration avec les bases des données]. | Pour plus d'informations consulter le [http://expressjs.com/en/guide/database-integration.html guide sur l'intégration avec les bases des données]. | ||
====LokiJS==== | |||
Une alternative qui peut être utile dans le cadre d'une petite application (ou d'un prototype) qui ne nécessite pas de base de données "complexe" est représentée par le module [http://lokijs.org/ LokiJS]. LokiJS est en effet un database "in-memory" qui existe sous-forme de simple fichier JSON, mais qui peut-être manipulé avec les opérations habituelles des bases de données (c'est-à-dire CRUD - creating, retrieving, updating, deleting). | |||
LokiJS peut être utilisé côté-serveur (donc en tant que module [[Node.js]]), mais également coté-client comme base de données intégrée dans le navigateur. Pour des applications de type client-serveur, dans lesquelles plusieurs utilisateurs partagent les mêmes informations contenues dans une base de données partagée, l'utilisation en tant que module Node est conseillée. | |||
* Voir le [http://lokijs.org/ site officiel] pour plus de détails | |||
====TingoDB==== | |||
Une autre database "in-memory", similaire à LokiJS, est [http://www.tingodb.com/ TingoDB]. L'intérêt de cette solution est représentée par le fait que TingoDB utilise les mêmes commandes que [https://www.mongodb.org/ MongoDB], une base de données NoSQL souvent utilisée dans des gros projets. L'avantage est donc de pouvoir passer très facilement d'une petite application, avec une base de données "in-memory" (TingoDB), à une application plus grande, avec une base de données dédiée (MongoDB). | |||
* Voir le [http://www.tingodb.com/ site officiel] pour plus de détails (le site propose peu d'explications sur la syntaxe car elle est la même que MongoDB) | |||
====Object Data Manager==== | |||
Pour des applications plus complexes il existe des Object Data Manager (ODM) qui permettent d'interagir avec des bases de données à travers des objects, et donc de manipuler les données comme si elles étaient de "simples" objets JavaScript avec des propriétés et des méthodes. | |||
Les deux ODM les plus utilisés avec Node.js sont : | |||
* [http://mongoosejs.com/ Mongoose] : à utiliser avec la base de données MongoDB qui est une base de données de type No-SQL | |||
* [http://sequelizejs.com/ SequelizeJS] : à utiliser avec les bases de données de type SQL comme MySQL, Sqlite, etc. | |||
Les ODM nécessitent bien entendu que les bases de données soient installées sur la machine (en local ou sur le serveur). | |||
===Autres middlewares=== | ===Autres middlewares=== | ||
Le site officiel d'Express.js propose | Le site officiel d'Express.js propose [http://expressjs.com/en/resources/middleware.html une liste plus exhaustive de middlewares utiles]. | ||
==Le générateur d'application Express== | ==Le générateur d'application Express== | ||
Ligne 982 : | Ligne 1 006 : | ||
express myapp | express myapp | ||
où "myapp" représente le nom de votre application, ainsi que le nom du dossier qui contient les fichiers qui composent l'application. Veuillez noter que le générateur, tout en gardant la philosophie "unopinionated", propose néanmoins des choix | où "myapp" représente le nom de votre application, ainsi que le nom du dossier qui contient les fichiers qui composent l'application. Veuillez noter que le générateur, tout en gardant la philosophie "unopinionated", propose néanmoins des choix initiaux tels que | ||
* La présence d'un express.static() middleware | * La présence d'un express.static() middleware | ||
* La présence des middlewares body-parser et cookie-parser | * La présence des middlewares body-parser et cookie-parser | ||
* L'utilisation | * L'utilisation d'un template engine (jade par default) | ||
* Une gestion des erreurs ( | * Une gestion des erreurs (exemple page 404) | ||
Vous pouvez voir la liste des modules qui seront | Vous pouvez voir la liste des modules qui seront installés en utilisant la commande <code>npm install</code> dans le fichier packages.json : | ||
<source lang="JavaScript"> | <source lang="JavaScript"> | ||
Ligne 1 011 : | Ligne 1 035 : | ||
</source> | </source> | ||
Pour plus d'informations consultez | Vous pouvez passer à la commande <code>express</code> plusieurs arguments qui vous permettent de configurer à la base certains aspects de l'application tels que : | ||
* le template engine, par exemple <code>express --view=ejs myApp</code> va remplacer jade par ejs | |||
* un pré-processeur pour les feuilles de style, par exemple <code>express --css=less myApp</code> va automatiquement utiliser (et transformer) des fichiers styles.less en styles.css (voir [[LESS CSS]]) | |||
* un fichier .gitignore à travers l'option <code>--git</code> | |||
Ces options peuvent se combiner, par exemple : | |||
express --view=ejs --css=less --git myExpressApp | |||
Pour plus d'informations consultez [http://expressjs.com/en/starter/generator.html le guide sur le site officiel] à propos du générateur. | |||
==Liens== | ==Liens== | ||
Ligne 1 023 : | Ligne 1 057 : | ||
* [https://openclassrooms.com/courses/des-applications-ultra-rapides-avec-node-js/le-framework-express-js Tutoriel sur OpenClassrooms] (français) | * [https://openclassrooms.com/courses/des-applications-ultra-rapides-avec-node-js/le-framework-express-js Tutoriel sur OpenClassrooms] (français) | ||
* [https://github.com/azat-co/expressworks Tutoriel de type NodeSchool sur Express.js] (disponible | * [https://github.com/azat-co/expressworks Tutoriel de type NodeSchool sur Express.js] (également disponible en français, à utiliser avec la ligne de commande) | ||
* [https://www.youtube.com/watch?v=pKd0Rpw7O48 Express.js Tutorial: Building RESTful APIs with Node and Express] | |||
[[Catégorie: JavaScript]] | [[Catégorie: JavaScript]] |
Dernière version du 16 juin 2018 à 10:07
Initiation à la pensée computationnelle avec JavaScript | |
---|---|
Module: JavaScript sur le serveur ◀▬ ▬▶ | |
◀▬▬▶ | |
⚐ à finaliser | ☸ avancé |
⚒ 2018/06/16 | |
Prérequis | |
Catégorie: JavaScript |
Introduction
Express.js est un framework basé sur Node.js pour le développement d'applications web. Express.js (appelé également Express par la suite) applique les mêmes caractéristiques générales de Node.js (non-blocking I/O, event-driven, …) mais dans le cadre plus spécifique des applications web basées sur l’architecture requête/réponse. Le résultat est un framework très flexible qui permet surtout de créer des applications qui prévoient l’échange de données en temps réel entre plusieurs utilisateurs.
La plupart des informations contenues dans cette page se réfèrent à la version 4.x d'Express.js.
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 rudiments de la programmation à travers JavaScript. Plus en détail, Express.js permet de montrer de manière concrète et pragmatique les é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 est conseillée :
Avertissement de sécurité
Le même avertissement de sécurité de Node.js s'applique dans le cas d'Express.js, car les technologies utilisées sont très similaires.
Veuillez également être attentif au fait que les exemples illustrés dans cette page ont exclusivement une finalité propédeutique à l’apprentissage des concepts fondamentaux du développement web et ne sont pas conçus pour une utilisation en dehors d’un environnement contrôlé par le développeur (e.g. localhost).
Rappel sur l'architecture requête/réponse
À faire, en attente voir par exemple :
- Le fonctionnement du protocole HTTP
- 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
La commande pour installer Express.js en tant que module local dans le dossier de votre application est la suivante :
npm install express
Ce type d'installation résulte dans l'arborescence suivante :
Votre_dossier |- node_modules |- express |- votre_fichier_node_principale.js
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
, 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();
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 le 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 d'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 dynamiques.
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 serveur comme Apache nécessite 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énementiel 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ée, 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 non à des copies. Cela se traduit par le fait qu'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 passée 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. l'une requête la page /about/ et l'autre la page /contact/). Ce mécanisme fait ainsi que toutes les requêtes qui arrivent sur un server Express.js sont traitées de manière centralisée et ont donc accès à la version la plus récente de toutes les données qui concerne 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évus 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 asynchrone. 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 et 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 ne reçoive une réponse. Donc, il y a des mécanismes intermédiaires 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, entre autres, de :
- Intercepter et identifier quel type de requête a été faite, par exemple :
- Quelle ressource (e.g. quelle page) a été demandée?
- Quel 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 (données d'un formulaire web, 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 d'envoyer, selon la logique de l'application, la réponse associée à une requête de ce type. Pour déterminer la logique de l'application, on fait recours à plusieurs middlewares représentant des étapes séquentielles. 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 de réponse, ce qui se traduit normalement par une page "loading" en continu. 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);
En réalité, dans l’utilisation concrète d’Express.js, il n’est pas nécessaire de spécifier quel middleware sera exécuté successivement : Express.js crée automatiquement une liste (i.e. un « stack ») de middleware en fonction de leur position dans le code. Pour qu’un middleware soit pris en compte, il faut tout simplement l’appliquer à l’intérieur de l’application (i.e. l’associer à la variable app qui représente l’application express). Cela peut se faire de deux manières différentes qui sont illustrées dans le point suivant.
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 possible d'appliquer plusieurs middlewares à 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éclaré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ées par le middleware global workingInProgress() avant qu'on ne 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 envoyant 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érent pour chaque ressource. Il est clair 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 certaines d’entre elles ne sont pas des pages, mais tout simplement des fichiers « 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 relatifs. Imaginons que le serveur statique soit 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 statiques 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. Veuillez noter que le nom du dossier ne doit pas faire partie de l'url de la requête, donc dans ce cas les ressources statiques seraient accessibles aux adresses suivantes :
- 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'appliquent également à express.static(). On peut illustrer un exemple. Imaginons la création d'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 dépendra 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 serveurs traditionnels. De plus, le serveur statique prend aussi automatiquement en compte les sous-dossiers, c'est-à-dire qu'une requête avec url "/images/image1.jpg" ira chercher le fichier image1.jpg dans le dossier __dirname + "/public/images" sans avoir à déclarer un autre middleware pour le sous-dossier spécifique.
- Voir la documentation sur le site officiel pour plus de détails sur express.static()
Synthèse
Grâce aux points précédents, nous pouvons illustrer maintenant les principales caractéristiques d’Express.js en s’appuyant sur certains éléments théoriques et techniques :
- Express.js est un module de Node.js construit « on top of » le module natif
http
. L’utilité principale d’Express.js consiste à fournir aux développeurs une structure de l’application qui facilite l’échange requête/réponse. - La structure d’une application Express.js se compose principalement de deux éléments fondamentaux : les middlewares et les routes
- Les middleware :
- sont tout simplement des fonctions qui sont exécutées de manière séquentielle selon leur position dans le code de l’application. La chaine de middleware s’arrête exclusivement lorsqu’un middleware envoie une réponse au client qui a fait la requête.
- peuvent exécuter tout type d’opération. Ces opérations sont souvent en relation avec la requête (e.g. contrôler si la requête répond à certaines conditions, comme par exemple si l’utilisateur dispose d’un cookie d’authentification), mais peuvent également être en lien avec d’autres éléments d’une application (e.g. interroger une base de données, écrire le contenu d’un fichier, etc.).
- Les routes :
- représentent une forme de « middleware final », car elles sont censées renvoyer une réponse en fonction de l’url spécifié par la requête. Les routes peuvent donc être considérées comme des « sorties » du cycle de middleware une fois que la requête du client a été satisfaite.
- définissent normalement des pages considérées "dynamiques", c'est-à-dire dont le contenu est créé en fonction de certaines conditions ou en assemblant différentes sources de données, pas tout simplement des fichiers html statiques.
En conclusion, une application Express.js n'est qu'un ensemble de middleware et de routes. Par la suite, nous allons illustrer certains des éléments principaux d'Express.js qui facilitent l'articulation entre middleware et routes.
- Voir la documentation sur comment écrire un middleware sur le site officiel
- Voir la documentation sur comment utiliser un middleware sur le site officiel (avec exemples avancés)
- Voir la documentation sur les routes sur le site officiel
Les éléments principaux de Express.js
Cette section présente un aperçu des éléments principaux du framework. Il s’agit d’une adaptation par rapport à la documentation de l’API (version 4.x) disponible sur le site officiel dans la mesure où :
- Seulement certaines propriétés et méthodes seront illustrées, celles qui ont le plus de probabilités d’être utilisées pour des débutants/novices
- Les aspects purement techniques d’utilisation des propriétés et des méthodes seront implémentées par des explications théoriques se référant à la section « Comprendre 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;
- Le router : des "sous-systèmes" de routes qui peuvent être ajoutées à l'application
L'application
L’application se traduit généralement par la variable var app = express(), c’est-à-dire un objet/fonction qui présente les propriétés et les méthodes principales pour créer une application web. Voici une liste non exhaustive des éléments principaux associés à l’objet app.
- app.get()
Cette méthode permet d’intercepter une requête de type GET avec un path spécifique et d’y associer une fonction de callback qui, normalement, génère une réponse adaptée à la requête. Exemple :
app.get('/mypath', function (request, response) { res.send('GET request to http://localhost:3000/mypath'); });
- app.post()
Cette méthode permet d'intercepter une requête de type POST avec un path spécifique et d'y associer une fonction de callback qui, normalement, gère les données qui ont été envoyées en POST (e.g. à travers un formulaire web). Pour le traitement de ces données, on utilise souvent un module spécifique qui permet d’accéder facilement aux éléments de la requête POST (voir le module body-parser plus bas dans la section suivante). Exemple :
app.post('/submit', function (request, response) { res.send('POST request to http://localhost:3000/submit'); });
- app.all()
Cette méthode permet d'intercepter tout type de requête HTTP (GET, POST, PUT, DELETE, ...) avec un path qui peut être également de plus haut niveau par rapport à une route spécifique. Par exemple, cette méthode applique un middleware myMiddleware() à toute route qui commence avec /admin/ (donc par exemple /admin/index.html, /admin/dashboard/, /admin/blog/new/) :
app.all('/admin/*', myMiddleware);
- app.param()
Cette méthode permet d'exécuter un middleware non pas sur la base d'une route spécifique ou d'un plus haut niveau, mais sur la base d'un paramètre dynamique contenu dans la requête. Un paramètre dynamique est associé à une route avec la notation :param comme par exemple dans la route GET suivante :
app.get('/students/profile/:student_id', function (request, response) { //On peut récupérer un paramètre à l'intérieur de l'array params associé à l'objet request var id = Number(request.params.student_id); //E.g. Chercher l'étudiant dans une base des données et afficher son nom, prénom, etc. //... }); app.get('/students/grade/:student_id', function (request, response) { var id = Number(request.params.student_id); //E.g. Chercher l'étudiant dans une base des données et afficher son nom, prénom, etc. ainsi que ses notes, ... });
Au lieu de récupérer le nom et le prénom de l’étudiant dans les deux routes spécifiques qui utilisent le paramètre student_id, on peut plutôt créer une sorte de « règle » selon laquelle à chaque fois qu’une route propose ce paramètre, il est très probable qu’on ait besoin de ces informations. On peut donc créer un middleware basé directement sur le paramètre :
app.param('student_id', function (request, response, next) { console.log("J'ai besoin des informations sur l'étudiant"); next(); });
À ce moment, si vous essayez d'accéder aux deux routes définies plus haut, le message à la console s'affichera dans les deux cas. Vous pouvez donc déplacer la recherche des informations sur l'étudiant à l'intérieur de ce middleware et les réutiliser ensuite dans toutes les routes qui prévoient le paramètre student_id.
- app.use()
Cette méthode permet d'appliquer un middleware à un path spécifique de la requête. Si aucun path n’est spécifié, alors c’est comme si on l’appliquait au path « root » (i.e. /). Exemples :
myFirstMiddleware = function (request, response, next) { console.log("Middelware 1 has been called."; next(); } mySecondMiddleware = function (request, response, next) { console.log("Middleware 2 has been called; next(); } app.use(myFirstMiddleare); app.use('/admin', mySecondMiddleware);
Le tableau suivant montre quel middleware va s'appliquer en fonction de 4 hypothétiques path :
Path | myFirstMiddleware | mySecondMiddleware |
---|---|---|
/ | Oui | Non |
/about/us.html | Oui | Non |
/admin/ | Oui | Oui |
/admin/dashboard/create/user | Oui | Oui |
- app.listen()
Cette méthode s'applique normalement à la fin du fichier qui représente l'application car il s'agit de la méthode qui crée le server web. Il s'agit en effet d'une méthode qui fait appel au module http.createServer de Node.js, en lui passant la variable app (qu'on le rappelle, c'est une fonction de type express()) pour gérer le mécanisme requête/réponse du module http. La méthode accepte plusieurs arguments, en phase de test et développement en local il est bien de passer le nombre d'une porte supérieur à 3000 pour éviter des conflits avec d'autres portes utilisés par d'autres services sur votre ordinateur. Exemple :
app.listen(3000);
Il est également recommandé de passer une deuxième fonction avec tout simplement un console.log pour être sûr que le server web marche :
app.listen(3000, console.log("Server listening to http://localhost:3000"));
La requête
L’objet requête correspond à une « HTTP request » qui est faite par le client sur le server créé avec l’application express(). L’objet requête propose plusieurs propriétés et méthodes qui permettent d’évaluer de manière plus approfondie la requête par rapport à un serveur statique, où il y a tout simplement une équivalence entre l’URL d’accès et le path de la ressource statique. Avec Node.js/Express.js, la requête peut être analysée par rapport à d’autres informations qui permettent de la définir de manière à la fois plus spécifique et plus étendue. On peut par exemple discriminer la requête par rapport à :
- Des données qui sont envoyées dans le « body » de la requête telles que des données envoyées par un formulaire web
- Des cookies qui permettent d’identifier si l’utilisateur a fait le login sur une page
- L’adresse IP du client
- Le query string (e.g. des paramètres qui sont envoyés sous forme d’associations clés/valeurs comme dans l’url http://localhost:3000/students.html?order=lastname&sort=asc&limit=10)
- Etc.
Veuillez noter deux choses importantes à propos de l’objet requête en Express.js :
- Dans cette page on fait référence à cet objet avec le nom
request
, tandis que souvent dans d’autres tutoriels et même dans la documentation officielle on utilise l’abréviationreq
. Vu que cet objet est passé en tant qu’argument dans les middleware de callback, en réalité son nom n’est pas important à condition d’être consistant dans le choix. - Avec la version 4.x certains éléments de la requête sont gérés par des modules externes qu’il faut importer (i.e. utiliser la fonction require()) pour qu’ils soient plus facilement accessibles à travers l’objet requête. C’est le cas notamment des données envoyées dans le body, les cookies, les fichiers à uploader, etc. Certains des modules les plus utiles dans ce cas seront brièvement abordés directement ici, mais une liste plus exhaustive est disponible plus bas dans la page.
Voici, ci-dessous, une liste des propriétés et des méthodes principales liées à l’objet requête.
- request.path
Cette propriété permet d’accéder au path associé à une requête. Dans un server web statique, le path correspond au chemin d’une ressource par rapport au dossier « root » du server, ce qui est d’ailleurs également le cas pour le express.static() middleware présenté plus haut dans la page. Cependant, dans Express.js le path assume plutôt une valeur sémantique qui permet d’organiser les routes en fonction de ce qu’elles sont censées représenter, plutôt que leur position physique.
Dans l’adresse http://localhost:3000/users l’objet request.path correspond à /users. On peut facilement déduire que cet objet est utilisé dans le cas de la méthode app.get() vue plus haut dans cette section. Cette méthode, en effet, ne fait que comparer la valeur de l’objet request.path avec la route proposée dans la méthode app.get(‘/users’, …). Si les deux correspondent, alors la fonction de callback sera exécutée et la réponse envoyée au client.
- request.params
Cette propriété est associée à un objet qui contient les associations clés/valeurs des paramètres dynamiques utilisés dans les routes. Un paramètre dynamique est une partie du path qui peut varier sans néanmoins changer la route qui va gérer la fonction de callback. Imaginons une route de ce type :
app.get('/places/:country/:region/:city', function (request, response) { var message = "Welcome to " + request.params.city + ", " + request.params.region + ", " + request.params.country "!!"; response.send(message); });
Une requête avec URL http://localhost:3000/places/switzerland/ticino/lugano/ produira un objet de ce type :
request.params = { country: 'switzerland', region: 'ticino', city: 'lugano' }
La réponse sera donc "Welcome to lugano, ticino, switzerland!!". Avec un URL de type http://localhost:3000/places/italy/lombardia/milano/ la réponse sera plutôt "Welcome to milano, lombardia, italy!!". Dans les deux cas, la même route sera exécutée, même si les path diffèrent dans leur structure, parce qu'ils présentent la même structure paramétrique.
- request.query
Cette propriété est similaire à la propriété request.path mais s'applique aux éléments du path qui se réfèrent au query string, c'est-à-dire l'association clé/valeurs qui peut être ajoutée à la fin d'un url sous cette forme :
http://localhost:3000/students/list.html?order=lastname&sort=asc&start=0&limit=20
Le query string commence après le caractère ? et chaque association clé/valeur est séparée par le caractère &. L'association se fait avec le symbole d'affectation = utilisé également dans JavaScript selon la notation key = value. Dans cet exemple de url, on aurait un objet query.string qui contient ces valeurs :
request.query = { order: "lastname", sort: "asc", start: 0, limit: 20 }
La différence entre paramètres et query string peut parfois être arbitraire. De manière générale, on utilise les paramètres lorsqu'on connait à l'avance le nombre spécifique d'informations variables qui seront transmises à travers l'url et lorsque les paramètres sont obligatoires. Le query string est par contre utile lorsque ce nombre n'est pas connu à l'avance ou peut varier. On pourrait très bien imaginer d'accéder à la page http://localhost:3000/students/list.html sans spécifier de paramètres et la page affichera automatiquement les premiers 20 étudiants triés par leur nom de famille.
- request.body
Cette propriété est associée aux éléments qui sont passés dans le body d'une requête, par exemple à travers la méthode POST d'un formulaire HTML. À partir d'Express.js 4.x. cette propriété résulte undefined
et il est nécessaire d'utiliser un middleware pour que les données passées à travers le body de la requête soient élaborées pour donner un objet comme dans le cas de request.path ou request.query. Il faut également noter à ce sujet que les données qu'on peut passer à travers le body peuvent être plus complexes et nombreuses, d'où l'intérêt d'utiliser un middleware qui prend en charge cette opération comme par exemple:
- Le module body-parser qui permet de gérer différents types de données dans le body (e.g. form, JSON, etc.)
- Le module multer qui permet de gérer les fichiers uploadés à travers un formulaire
Voir la section sur les modules/middleware utiles pour plus de détails.
- request.cookie
Le même principe de request.body s'applique à la propriété request.cookie qui est undefined
et nécessite également l'utilisation d'un module/middleware pour associer les cookies présents dans le client à l'objet de la requête.
- Le module cookie-parser, illustré également plus bas dans cette page, permet cette opération
La réponse
L’objet réponse correspond à une « HTTP response » que le serveur envoie au client qui a fait une requête à l’application express(). L’objet réponse possède plusieurs propriétés et méthodes qui permettent d’adapter les deux éléments principaux de la réponse : les headers (i.e. les entêtes) et le body (i.e. le contenu, très souvent du HTML).
Comme c’était le cas pour la requête qui est souvent abrégée en req
dans les tutoriels et la documentation officielle de Express.js, la réponse (i.e. response) figure souvent avec la notation res
. Pour une question de clarté, cette page utilise le nom complet response
, mais cela est seulement conventionnel.
Voici, ci-dessous, une liste non exhaustive des propriétés et méthodes principales associées à la réponse.
- response.send()
Cette méthode représente la manière la plus simple et flexible pour envoyer une réponse au client. Voici un exemple basilaire qui affiche simplement du HTML en réponse à une requête de type GET :
app.get('/mypage.html', function (request, response) {
response.send('<h1>Just html, please!</h1>');
});
Lorsqu’on accède à cette page à travers un client, nous pouvons constater qu'elle possède automatiquement certaines informations dans les entêtes comme par exemple :
- Un status de 200
- Un Content-Length de 27
- Un Content-Type de type text/html avec charset de type utf8
Par contre, si on passe en tant qu’argument un Objet ou un Array JavaScript, la réponse sera automatiquement transformée en format JSON à la fois pour les entêtes et pour le contenu. Voici un exemple :
app.get('/my/stic-1/grade.json', function (request, response) { var grades = { ex1: "4.5", ex2: "4.75", ex3: "5.5" } response.send(grades); });
Contrairement à l’exemple précédent, dans ce cas le Content-Type de la réponse sera application/json plutôt que text/html, et le contenu sera automatiquement affiché en tant que JSON :
{"ex1":"4.5","ex2":"4.75","ex3":"5.5"}
Pour envoyer du JSON il existe également une méthode spécifique response.json() qui marche pratiquement de la même manière, à quelques exceptions près.
- response.sendFile()
Cette méthode permet d'envoyer en réponse le contenu d'un fichier. Le fichier peut être une page HTML, mais également un fichier PDF ou d'autres types de format qui est généralement supporté par des navigateurs web. Exemple :
app.get("/", function (request, response) { //We use the sendFile method to serve a static HTML page //The method accepts the absolute path to the file, so we use the shortcut __dirname response.sendFile(__dirname + '/files/homepage.html'); });
L'intérêt de pouvoir renvoyer un fichier tel qu'il est, mais sans utiliser le middleware express.static(), peut concerner par exemple le contrôle sur un fichier PDF: si la requête présente certaines caractéristiques (e.g. utilisateur a fait le login), alors le fichier sera disponible, autrement on peut afficher un message d'erreur. La méthode sendFile()
accepte également des options, vous pouvez vous référez à la documentation officielle pour plus de détails.
- response.redirect()
Cette méthode permet de rediriger la requête vers une autre destination qui peut être interne (e.g. une autre route) ou externe à l'application (e.g. un autre site). Exemples:
app.get("/internal", function (request, response) { response.redirect('/otherpage'); }); app.get("/external", function (request, response) { response.redirect('http://edutechwiki.unige.ch'); });
- response.status()
Cette méthode permet de définir un code de statut (e.g. 200, 404, etc.) pour la réponse. Il est souvent utilisé avec la méthode response.send()
. Par exemple :
app.get("/profile/:user", function (request, response) { //Si on ne trouve pas un utilisateur avec ce nom response.status(404).send("Cet utilisateur n'existe pas"); });
- response.render()
Cette méthode est utilisée dans le cadre des "templates engines", c'est-à-dire des middlewares (voir plus bas dans cette page) qui permet de passer des données à ce qu'on appelle une "view". Chaque "template engine" possède sa propre syntaxe pour intégrer les données que la view reçoit depuis l'application, mais le fonctionnement est toujours très similaire. La fonction render() accepte généralement deux arguments :
- La référence à la view qui va servir de template
- Un objet avec associations clés/valeurs des données à passer
Voici un exemple théorique, pour des exemples pratiques voir la section sur les middleswares utiles plus bas :
//WARNING : the code to include and use the template middleware is missing from this example
//Define some data that will be included in the templates
var notes = [
{
ex: 1,
grade: 5.75
}, {
ex: 2,
grade: 5.5
}, {
ex: 3,
grade: 6
}
];
//Define a route for the homepage that uses the homepage.ejs template
app.get("/", function (request, response) {
//Use the response.render() method, which accepts the filename of the template and an array of data to pass to it
//In this case we pass a variable with the cours name STIC I and the notes object defined above
//These two variables will be accessible in the template
response.render('homepage.ejs', {
cours: "STIC I",
notes: notes
});
});
Dans la view homepage.ejs nous aurons accès aux variables "cours" et "notes" avec leur contenu respectif.
Le router
L'objet router permet de créer une application à l'intérieur d'une autre application Express.js, ce qui facilite notamment l'organisation du code et des routes. Un router est, encore une fois, un "simple" middleware et il est par conséquent possible d'ajouter un router à une autre application, en choisissant notamment le "mount" point (le path sur lequel le nouveau router va être ajouté aux routes de l'application sous-jacente). Voici un exemple :
var express = require('express');
//Création de l'application principale
var app = express();
//Création d'une "sous-application" avec un Router()
var admin = express.Router();
//Middlewares et routes pour l'app principale
function myAppMiddleware(request, response, next) {
console.log("Middleware de l'app principale");
next();
}
app.use(myAppMiddleware);
app.get("/", function (request, response) {
response.send("Homepage");
});
//Middleware et routes pour la "sous-application"
function myAdminMiddleware(request, response, next) {
console.log("Middleware de la sous-application");
next();
}
admin.use(myAdminMiddleware);
admin.get("/", function (request, response) {
response.send("Homepage Admin");
});
//On ajoute la sous-application à l'app principale avec un "mount point"
app.use("/admin", admin);
//Server
app.listen(3000, console.log("Server listening at port 3000"));
Notre application possède à ce point deux routes, qui sont cependant gérées par deux applications différentes :
- La homepage "/" est gérée par l'application principale
var app = express()
; - La page d'administration "/admin" est gérée par la sous-application/router
var admin = express.Router()
;
Cette démarche est particulièrement pratique si on considère la possibilité de diviser une grande application en différents modules Node.js. De cette manière il est possible d'organiser le code, par exemple dans la perspective de partager le travail avec d'autres développeurs qui s'occupent de fonctionnalités spécifiques.
Modules/Middleware utiles
La philosophie d’Express.js est volontairement très minimaliste, ce qui se reflète principalement sur deux aspects reliés :
- Le framework propose « out of the box » juste les éléments essentiels, mais dès que des fonctionnalités plus avancées sont nécessaires, il faut implémenter des éléments « externes » (parfois ces éléments faisaient partie de versions précédentes à la version 4, mais ont été enlevés justement en raison de cette philosophie) ;
- Le développeur peut donc choisir quels éléments ajouter et parmi les éléments mêmes il a normalement le choix entre plusieurs modules qui proposent des fonctionnalités similaires mais avec des différences au niveau de la syntaxe, du fonctionnement interne, etc. Par exemple, Express.js ne propose aucun « template engine », mais le développeur peut choisir parmi une longue liste de possibilité selon ses préférences.
En raison de ces faits, tous les modules illustrés par la suite nécessitent l'installation à travers la commande :
npm install nom_du_module
body-parser
Le module body-parser permet de gérer des informations envoyées à travers le body de la requête, comme c'est le cas, par exemple, des formulaires HTML. Veuillez noter que body-parser ne prend pas en compte le requête de type "multipart", par exemple les champs de type file pour uploader des fichiers (à ce propos, voir par exemple le module multer).
Pour installer le module utilisez la commande :
npm install body-parser
Ensuite vous pouvez l'inclure dans votre application avec le code
var bodyParser = require('body-parser');
Ce module peut être adapté avec plusieurs options, dans l'exemple qui suit nous allons mettre en place une configuration de base pour ajouter de manière globale les éléments d'un formulaire web à l'object request.body
. Imaginons d'avoir une page web qui contient le formulaire suivant, qui sert pour ajouter une note obtenue pour l'un des exercices du cours STIC I:
<form action="/add" method="post">
<label>Exercice:</label> <input type="number" name="ex">
<label>Grade:</label> <input type="number" min="0" max="6" step="0.25" name="grade">
<input type="submit" value="Add">
</form>
Nous avons un formulaire avec deux champs :
- Le champ ex de type "number"
- Le champ grade de type "number"
Voici comment on peut facilement gérer les informations envoyées à la route "/add" en utilisant la méthode urlencoded()
du module body-parser qui permet de récupérer le contenu des deux champs à travers l'objet request.body
:
var express = require('express')
var bodyParser = require('body-parser')
var app = express()
//Appliquer le middleware à toutes les routes
//Le middleware s'occupe d'intercepter le body de la requête et de rendre chaque donnée disponible dans l'objet request.body
app.use(bodyParser.urlencoded({ extended: false }))
//Définir la route qui va gérer les informations réçues par le formulaire
app.post("/add", function (request, response) {
//Obtenir le numéro de l'exercice à travers le champ du formulaire avec name="ex"
var exercice = request.body.ex;
//Obtenir la note à travers le champ du formulaire avec name="grade"
var grade = request.body.grade;
response.send("Vous avez obtenu un score de " + grade + " pour votre exercice " + exercice);
});
cookie-parser
Le module cookie-parser utilise le même principe du module body-parser, mais appliqué aux cookies et donc à l'objet request.cookies
. Pour installer le module utilisez la commande :
npm install cookie-parser
Et pour l'inclure dans votre application le code :
var cookieParser = require('cookie-parser') //Ajouter le middleware de manière globale à l'application app.use(cookieParser())
À ce stade, vous avez accès aux cookies envoyés par la requête de votre application dans l'objet request.cookies
. Par exemple imaginons que votre application nécessite du cookie 'secretKey' pour identifier un utilisateur, la valeur sera accessible à travers la notation :
request.cookies.secretKey
Pour ajouter la valeur d'un cookie il faut par contre utiliser la notation suivante :
response.cookie('NomDuCookie', 'valeur du cookie');
Template engines
L'un des avantages d'une page dynamique par rapport à une page HTML statique consiste en la possibilité d'intégrer des données qui sont tirées d'autres sources par rapport au code "écrit à la main" par le développeur. L'application plus répandue dans ce sens concerne l'intégration de données tirées d'une base de données (e.g. un blog, un forum, etc.). Pour faciliter ce type d'intégration, Express.js met à disposition des template engines, c'est-à-dire des fichiers qui contiennent :
- Du HTML "traditionnel" pour gérer la structure de la page
- Une syntaxe particulière qui permet d'intégrer des données, par exemple sous forme de "placeholder" pour une variable ou de cycle pour l'itération d'un array ou d'un objet
Lorsqu'on utilise un template engine, le mécanisme de création d'une page suit les étapes suivantes :
- Le système cherche le fichier "template" associé à la page, ce qu'on appelle une "view"
- Le système passe au template des données déterminées par la logique de l'application (e.g. récupérées dans une base de données) sous forme de variables
- Le système remplace les "placeholders" des variables dans le template avec les valeurs passées
- La page HTML "construite" dynamiquement est passée en réponse
Pour que ce mécanisme marche, il faut qu'Express.js soit au courant de deux informations :
- Où se trouvent les "views" à utiliser comme template pour les pages à rendre
- Quel template engine utiliser, c'est-à-dire quel type de syntaxe permet de remplacer les "placeholders" avec le contenu des variables
Pour ce faire on utilise la méthode set()
associée à l'application par exemple de cette manière :
//Définir où se trouvent les "views" app.set('views', './directory/avec/les/fichiers/template') //Définir quel template engine utiliser app.set('view engine', 'NomDuTemplateEngine')
Dans l'esprit "non-opinionated" d'Express.js, il n'existe pas de template engine associé automatiquement à l'application. Au contraire, il en existe plusieurs parmi lesquels le développeur peut choisir. De ce fait, il faudra d'abord installer le module du template engine avec la commande
npm install NomDuModuleTemplateEngine
Chaque template engine propose sa propre syntaxe et les views sont normalement associées à une extension déterminée par le template engine. Par exemple, un template engine très utilisé avec Express.js s'appelle jade et les views sont représentées par des fichiers du style myView.jade. L'illustration des différents templates engines et leur syntaxe va au-delà des objectifs de cette page. Nous nous limiterons à proposer deux exemples avec deux engines différents. Pour plus d'informations et pour une liste des engines disponibles, voir la documentation officielle sur les template engines.
Jade
Jade est un template engine très utilisé avec Express.js, bien que sa syntaxe pourrait résulter assez complexe pour un neophyte. Pour utiliser jade il faut d'abord installer le module :
npm install jade
Ensuite, il faut informer l'application sur la position des views .jade et du fait que ce template engine sera utilisé lorsqu'on utilise la méthode response.render()
:
app.set('views', './views'); app.set('view engine', 'jade');
Imaginons à ce point l'utilisation d'une view "homepage.jade" pour rendre la réponse de la route "/" d'une application. D'abord l'arborescence de notre application doit être la suivante :
|- node_modules |- express |- jade |- views |- homepage.jade app.js
Voyons le contenu du fichier app.js qui représente notre application :
//Include the express module and create the app
var express = require('express');
var app = express();
//Include the template module that must be installed with npm install ejs
//There are different template engines (e.g. jade), in this case we use ejs --> http://www.embeddedjs.com/
var jade = require('jade');
//Define the folder where templates are stored
app.set('views', './views');
//Define the template engine
app.set('view engine', 'jade');
//Define the route for the homepage
app.get("/", function (request, reponse) {
//Define two variables to pass to the view
var title = 'My homepage';
var message = 'Welcome to my Homepage!!';
//Render the homepage.jade view and pass the two variables to it
response.render('homepage.jade', {
title: title,
message: message
});
});
//Listen to port 3000
app.listen(3000, console.log("Server listening to port 3000"));
Notre view homepage.jade reçoit donc deux variables (title, et message). Selon la syntaxe propre à ce template engine, on peut imaginer une simple homepage avec ce code jade:
doctype html html(lang=en) head title= title body h1= title p= message
Le passage des variables se fait avec la notation element= variable
. Le résultat de la réponse de la homepage sera donc le suivant :
<!DOCTYPE html>
<html>
<head>
<title>My homepage</title>
</head>
<body>
<h1>My homepage</h1>
<p>Welcome to my Homepage!!</p>
</body>
</html>
- Pour plus de détails voir ce Tutoriel sur SitePoint
- Un template de site personnel qui utilise Bootstrap
EJS
Un autre template engine assez utilisé avec Express.js est EJS qui, comparé à Jade, garde une syntaxe plus proche du HTML traditionnel. Pour installer le module il faut utiliser la commande :
npm install ejs
La démarche est ensuite similaire à celle de jade. Voici un exemple complet de page avec EJS. Code pour l'application :
//Include the express module and create the app
var express = require('express');
var app = express();
//Include the template module that must be installed with npm install ejs
//There are different template engines (e.g. jade), in this case we use ejs --> http://www.embeddedjs.com/
var ejs = require('ejs');
//Define the folder where templates are stored
app.set('views', './views');
//Define the template engine
app.set('view engine', 'ejs');
//Define some data that will be included in the templates
var notes = [
{
ex: 1,
grade: 5.75
}, {
ex: 2,
grade: 5.5
}, {
ex: 3,
grade: 6
}
];
//Define a route for the homepage that uses the homepage.ejs template
app.get("/", function (request, response) {
//Use the response.render() method, which accepts the filename of the template and an array of data to pass to it
//In this case we pass a variable with the cours name STIC I and the notes object defined above
//These two variables will be accessible in the template
response.render('homepage.ejs', {
cours: "STIC I",
notes: notes
});
});
//Launch the webserver
app.listen(3000, console.log("Server listening to port 3000"));
Et le code de la view homepage.ejs :
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Page using a template</title>
</head>
<body>
<!-- EJS uses a template language that integrates JavaScript into the template code -->
<!-- We use the template language to print the content of the cours variable, which is passed from the route -->
<h1>My grades in <%= cours %></h1>
<ul>
<!-- We use the template language to built a for loop to print each element of the notes array passed from the route -->
<% for(var i=0; i<notes.length; i++) {%>
<li>Exercice <%= notes[i].ex %>: <%= notes[i].grade %></li>
<% } %>
</ul>
</body>
</html>
Vous pouvez noter comme cette syntaxe est beaucoup plus proche du HTML traditionnel, avec les variables qui sont contenues dans des "blocs" de type :
- <%= ... %> pour le output
- <% ... %> pour du code (e.g. un cycle for)
Le HTML final de la page sera le suivant :
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Page using a template</title>
</head>
<body>
<!-- EJS uses a template language that integrates JavaScript into the template code -->
<!-- We use the template language to print the content of the cours variable, which is passed from the route -->
<h1>My grades in STIC I</h1>
<ul>
<!-- We use the template language to built a for loop to print each element of the notes array passed from the route -->
<li>Exercice 1: 5.75</li>
<li>Exercice 2: 5.5</li>
<li>Exercice 3: 6</li>
</ul>
</body>
</html>
Socket.io
Socket.io est un module de Node.js qui peut être utilisé tout seul, mais qui s'intègre avec Express.js pour le développement d'applications web qui utilisent la technologie des Web sockets. Cette technologie permet de créer des connexions "double binding" entre les clients et le serveur.
- Voir la page Socket.io pour des exemples avec Express.js
- Voir le site officiel du projet
Intégration avec des bases de données
Les applications Express.js peuvent facilement être liées à des bases de données à travers des modules propres à différents types de databases tels que :
- MySQL
- MongoDB
- SQLite
- ...
Veuillez noter que les modules installent exclusivement des "couches" intermédiaires qui permettent de lier Node.js avec les bases de données, mais les logiciels qui supportent les bases de données doivent déjà être disponibles sur le système.
Pour plus d'informations consulter le guide sur l'intégration avec les bases des données.
LokiJS
Une alternative qui peut être utile dans le cadre d'une petite application (ou d'un prototype) qui ne nécessite pas de base de données "complexe" est représentée par le module LokiJS. LokiJS est en effet un database "in-memory" qui existe sous-forme de simple fichier JSON, mais qui peut-être manipulé avec les opérations habituelles des bases de données (c'est-à-dire CRUD - creating, retrieving, updating, deleting).
LokiJS peut être utilisé côté-serveur (donc en tant que module Node.js), mais également coté-client comme base de données intégrée dans le navigateur. Pour des applications de type client-serveur, dans lesquelles plusieurs utilisateurs partagent les mêmes informations contenues dans une base de données partagée, l'utilisation en tant que module Node est conseillée.
- Voir le site officiel pour plus de détails
TingoDB
Une autre database "in-memory", similaire à LokiJS, est TingoDB. L'intérêt de cette solution est représentée par le fait que TingoDB utilise les mêmes commandes que MongoDB, une base de données NoSQL souvent utilisée dans des gros projets. L'avantage est donc de pouvoir passer très facilement d'une petite application, avec une base de données "in-memory" (TingoDB), à une application plus grande, avec une base de données dédiée (MongoDB).
- Voir le site officiel pour plus de détails (le site propose peu d'explications sur la syntaxe car elle est la même que MongoDB)
Object Data Manager
Pour des applications plus complexes il existe des Object Data Manager (ODM) qui permettent d'interagir avec des bases de données à travers des objects, et donc de manipuler les données comme si elles étaient de "simples" objets JavaScript avec des propriétés et des méthodes.
Les deux ODM les plus utilisés avec Node.js sont :
- Mongoose : à utiliser avec la base de données MongoDB qui est une base de données de type No-SQL
- SequelizeJS : à utiliser avec les bases de données de type SQL comme MySQL, Sqlite, etc.
Les ODM nécessitent bien entendu que les bases de données soient installées sur la machine (en local ou sur le serveur).
Autres middlewares
Le site officiel d'Express.js propose une liste plus exhaustive de middlewares utiles.
Le générateur d'application Express
Express.js propose également un module qui permet de créer des applications avec une structure qui facilite l'organisation des fichiers qui composent une application de grande taille. Le générateur peut être installé globalement sur le système avec la commande :
npm install express-generator -g
Ensuite, il sera possible de créer des applications avec la commande :
express myapp
où "myapp" représente le nom de votre application, ainsi que le nom du dossier qui contient les fichiers qui composent l'application. Veuillez noter que le générateur, tout en gardant la philosophie "unopinionated", propose néanmoins des choix initiaux tels que
- La présence d'un express.static() middleware
- La présence des middlewares body-parser et cookie-parser
- L'utilisation d'un template engine (jade par default)
- Une gestion des erreurs (exemple page 404)
Vous pouvez voir la liste des modules qui seront installés en utilisant la commande npm install
dans le fichier packages.json :
{
"name": "myapp",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"body-parser": "~1.13.2",
"cookie-parser": "~1.3.5",
"debug": "~2.2.0",
"express": "~4.13.1",
"jade": "~1.11.0",
"morgan": "~1.6.1",
"serve-favicon": "~2.3.0"
}
}
Vous pouvez passer à la commande express
plusieurs arguments qui vous permettent de configurer à la base certains aspects de l'application tels que :
- le template engine, par exemple
express --view=ejs myApp
va remplacer jade par ejs - un pré-processeur pour les feuilles de style, par exemple
express --css=less myApp
va automatiquement utiliser (et transformer) des fichiers styles.less en styles.css (voir LESS CSS) - un fichier .gitignore à travers l'option
--git
Ces options peuvent se combiner, par exemple :
express --view=ejs --css=less --git myExpressApp
Pour plus d'informations consultez le guide sur le site officiel à propos du générateur.
Liens
Ressources
Tutoriels
- Tutoriel sur OpenClassrooms (français)
- Tutoriel de type NodeSchool sur Express.js (également disponible en français, à utiliser avec la ligne de commande)
- Express.js Tutorial: Building RESTful APIs with Node and Express