035.2 Leçon 2
Certification : |
Web Development Essentials |
---|---|
Version : |
1.0 |
Thème : |
035 Programmation de serveur NodeJS |
Objectif : |
035.2 Bases de NodeJS Express |
Leçon: |
2 sur 2 |
Introduction
Les serveurs web disposent de mécanismes très polyvalents pour produire des réponses aux demandes des clients. Pour certaines requêtes, il suffit au serveur web de fournir une réponse statique, non traitée, car la ressource demandée est la même pour tous les clients. Par exemple, lorsqu’un client demande une image qui est accessible à tous, il suffit que le serveur envoie le fichier contenant l’image.
Mais lorsque les réponses sont générées dynamiquement, elles peuvent avoir besoin d’être mieux structurées que de simples lignes écrites dans le script du serveur. Dans ce cas, il est pratique pour le serveur web de pouvoir générer un document complet, qui peut être interprété et rendu par le client. Dans le contexte du développement d’applications web, les documents HTML sont généralement créés sous forme de modèles et conservés séparément du script du serveur, qui insère les données dynamiques à des endroits prédéterminés dans le modèle approprié, puis envoie la réponse formatée au client.
Les applications web consomment souvent des ressources statiques et dynamiques. Un document HTML, même s’il a été généré dynamiquement, peut contenir des références à des ressources statiques telles que des fichiers CSS et des images. Pour démontrer comment Express peut aider à gérer ce type de demande, nous allons d’abord configurer un exemple de serveur qui fournit des fichiers statiques, puis mettre en œuvre des routes qui génèrent des réponses structurées, basées sur des modèles.
Fichiers statiques
La première étape consiste à créer le fichier JavaScript qui sera exécuté en tant que serveur. Suivons le même schéma que celui abordé dans les leçons précédentes pour créer une application Express simple : créez d’abord un répertoire appelé server
, puis installez les composants de base avec la commande npm
:
$ mkdir server $ cd server/ $ npm init $ npm install express
Pour le point d’entrée, n’importe quel nom de fichier peut être utilisé, mais ici nous allons utiliser le nom de fichier par défaut : index.js
. Le listing suivant montre un fichier index.js
basique qui sera utilisé comme point de départ pour notre serveur :
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
Vous n’avez pas besoin d’écrire du code explicite pour envoyer un fichier statique. Express dispose d’un logiciel médiateur middleware à cet effet, appelé express.static
. Si votre serveur doit envoyer des fichiers statiques au client, chargez simplement le middleware express.static
au début du script :
app.use(express.static('public'))
Le paramètre public
indique le répertoire qui stocke les fichiers que le client peut demander. Les chemins demandés par les clients ne doivent pas inclure le répertoire public
, mais seulement le nom du fichier, ou le chemin d’accès au fichier par rapport au répertoire public
. Pour demander le fichier public/layout.css
, par exemple, le client fait une demande à /layout.css
.
Sortie formatée
Si l’envoi de contenu statique est simple, le contenu généré dynamiquement peut varier considérablement. La création de réponses dynamiques avec des messages courts permet de tester facilement les applications dans leurs premiers stades de développement. Par exemple, voici une route de test qui renvoie simplement au client un message envoyé par la méthode HTTP POST
. La réponse peut juste reproduire le contenu du message en texte brut, sans aucun formatage :
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
Une route comme celle-ci est un bon exemple à utiliser lors de l’apprentissage d’Express et à des fins de diagnostic, où une réponse brute envoyée avec res.send()
est suffisante. Mais un serveur utile doit être capable de produire des réponses plus complexes. Nous allons maintenant passer au développement de ce type de route plus sophistiqué.
Notre nouvelle application, au lieu de se contenter de renvoyer le contenu de la requête en cours, conserve une liste complète des messages envoyés dans les requêtes précédentes par chaque client et renvoie la liste de chaque client lorsqu’il en fait la demande. Une réponse fusionnant tous les messages est une option, mais d’autres modes de sortie formatés sont plus appropriés, surtout lorsque les réponses deviennent plus élaborées.
Pour recevoir et stocker les messages du client envoyés pendant la session en cours, nous devons d’abord inclure des modules supplémentaires pour gérer les cookies et les données envoyées via la méthode HTTP POST
. L’exemple de serveur suivant a pour seul but d’enregistrer les messages envoyés via POST
et d’afficher les messages précédemment envoyés lorsque le client émet une requête GET
. Il y a donc deux routes pour le chemin /
. La première route répond aux demandes faites avec la méthode HTTP POST
et la seconde répond aux demandes faites avec la méthode HTTP GET
:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.use(express.static('public'))
const cookieParser = require('cookie-parser')
app.use(cookieParser())
const { v4: uuidv4 } = require('uuid')
app.use(express.urlencoded({ extended: true }))
// Array to store messages
let messages = []
app.post('/', (req, res) => {
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Locate cookie in the request
let uuid = req.cookies.uuid
// If there is no uuid cookie, create a new one
if ( uuid === undefined )
uuid = uuidv4()
// Add message first in the messages array
messages.unshift({uuid: uuid, message: req.body.message})
// Collect all previous messages for uuid
let user_entries = []
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
// Update cookie expiration date
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// Send back JSON response
res.json(user_entries)
})
app.get('/', (req, res) => {
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Locate cookie in the request
let uuid = req.cookies.uuid
// Client's own messages
let user_entries = []
// If there is no uuid cookie, create a new one
if ( uuid === undefined ){
uuid = uuidv4()
}
else {
// Collect messages for uuid
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
}
// Update cookie expiration date
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// Send back JSON response
res.json(user_entries)
})
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
Nous avons gardé la configuration des fichiers statiques en haut, car il sera bientôt utile de fournir des fichiers statiques tels que layout.css
. En plus du middleware cookie-parser
introduit dans le chapitre précédent, l’exemple inclut également le middleware uuid
pour générer un numéro d’identification unique passé comme cookie à chaque client qui envoie un message. S’ils ne sont pas déjà installés dans le répertoire du serveur d’exemple, ces modules peuvent être installés avec la commande npm install cookie-parser uuid
.
Le tableau global appelé messages
stocke les messages envoyés par tous les clients. Chaque élément de ce tableau consiste en un objet avec les propriétés uuid
et message
.
Ce qui est vraiment nouveau dans ce script est la méthode res.json()
, utilisée à la fin des deux routes pour générer une réponse au format JSON avec le tableau contenant les messages déjà envoyés par le client :
// Send back JSON response
res.json(user_entries)
JSON est un format de texte brut qui vous permet de regrouper un ensemble de données dans une structure unique qui est associative : c’est-à-dire que le contenu est exprimé sous forme de clés et de valeurs. JSON est particulièrement utile lorsque les réponses sont destinées à être traitées par le client. Grâce à ce format, un objet ou un tableau JavaScript peut être facilement reconstruit du côté client avec toutes les propriétés et tous les index de l’objet original sur le serveur.
Comme nous structurons chaque message en JSON, nous refusons les requêtes qui ne contiennent pas application/json
dans leur en-tête accept
:
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
Une requête faite avec une commande curl
simple pour insérer un nouveau message ne sera pas acceptée, car curl
par défaut ne spécifie pas application/json
dans l’en-tête accept
:
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt Not Found
L’option -H "accept : application/json"
modifie l’en-tête de la requête pour spécifier le format de la réponse, qui cette fois sera acceptée et répondra dans le format spécifié :
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt -H "accept: application/json" ["My first message"]
La récupération des messages en utilisant l’autre route se fait de manière similaire, mais cette fois en utilisant la méthode HTTP GET
:
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -H "accept: application/json" ["Another message","My first message"]
Modèles
Les réponses dans des formats tels que JSON sont pratiques pour communiquer entre programmes, mais l’objectif principal de la plupart des serveurs d’applications web est de produire du contenu HTML pour la consultation humaine. Intégrer du code HTML dans du code JavaScript n’est pas une bonne idée, car mélanger les langages dans un même fichier rend le programme plus sensible aux erreurs et nuit à la maintenance du code.
Express peut fonctionner avec différents moteurs de modèles qui séparent le HTML pour le contenu dynamique ; la liste complète se trouve sur le site des moteurs de modèles Express. L’un des moteurs de modèles les plus populaires est Embedded JavaScript (EJS), qui vous permet de créer des fichiers HTML avec des balises spécifiques pour l’insertion de contenu dynamique.
Comme les autres composants d’Express, EJS doit être installé dans le répertoire où s’exécute le serveur :
$ npm install ejs
Ensuite, le moteur EJS doit être défini comme le moteur de rendu par défaut dans le script du serveur (vers le début du fichier index.js
, avant les définitions de route) :
app.set('view engine', 'ejs')
La réponse générée avec le modèle est envoyée au client avec la fonction res.render()
, qui reçoit comme paramètres le nom du fichier du modèle et un objet contenant les valeurs qui seront accessibles depuis le modèle. Les routes utilisées dans l’exemple précédent peuvent être réécrites pour générer des réponses HTML ainsi que JSON :
app.post('/', (req, res) => {
let uuid = req.cookies.uuid
if ( uuid === undefined )
uuid = uuidv4()
messages.unshift({uuid: uuid, message: req.body.message})
let user_entries = []
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
})
app.get('/', (req, res) => {
let uuid = req.cookies.uuid
let user_entries = []
if ( uuid === undefined ){
uuid = uuidv4()
}
else {
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
}
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
})
Notez que le format de la réponse dépend de l’en-tête accept
trouvé dans la requête :
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
Une réponse au format JSON est envoyée uniquement si le client le demande explicitement. Sinon, la réponse est générée à partir du modèle index
. Le même tableau user_entries
alimente à la fois la sortie JSON et le modèle, mais l’objet utilisé comme paramètre pour ce dernier a aussi la propriété title: "My messages"
, qui sera utilisée comme titre dans le modèle.
HTML Templates
Comme les fichiers statiques, les fichiers contenant les modèles HTML résident dans leur propre répertoire. Par défaut, EJS suppose que les fichiers de modèles se trouvent dans le répertoire views/
. Dans l’exemple, un modèle nommé index
a été utilisé, donc EJS recherche le fichier views/index.ejs
. Le listing suivant est le contenu d’un modèle simple views/index.ejs
qui peut être utilisé avec le code de l’exemple :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><%= title %></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/layout.css">
</head>
<body>
<div id="interface">
<form action="/" method="post">
<p>
<input type="text" name="message">
<input type="submit" value="Submit">
</p>
</form>
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
</div>
</body>
</html>
La première balise spéciale EJS est l’élément <title>
dans la section <head>
:
<%= title %>
Pendant le processus de rendu, cette balise spéciale sera remplacée par la valeur de la propriété title
de l’objet passé en paramètre à la fonction res.render()
.
La majeure partie du modèle est constituée de code HTML classique. Le modèle contient donc le formulaire HTML permettant d’envoyer de nouveaux messages. Le serveur de test répond aux méthodes HTTP GET
et POST
pour le même chemin /
, d’où les attributs action="/"
et method="post"
dans la balise de formulaire.
D’autres parties du modèle sont un mélange de code HTML et de balises EJS. EJS dispose de balises à des fins spécifiques dans le modèle :
<% … %>
-
Insère un contrôle de flux. Aucun contenu n’est directement inséré par cette balise, mais elle peut être utilisée avec des structures JavaScript pour choisir, répéter ou supprimer des sections du code HTML. Exemple de démarrage d’une boucle :
<% messages.forEach( (message) => { %>
<%# … %>
-
Définit un commentaire, dont le contenu est ignoré par l’analyseur syntaxique. Contrairement aux commentaires écrits en HTML, ces commentaires ne sont pas visibles pour le client.
<%= … %>
-
Insère le contenu échappatoire de la variable. Il est important d’échapper au contenu inconnu pour éviter l’exécution de code JavaScript, qui peut ouvrir des brèches pour les attaques de type cross-site scripting (XSS). Exemple :
<%= title %>
<%- … %>
-
Insère le contenu de la variable sans échappement.
Le mélange de code HTML et de balises EJS est évident dans l’extrait où les messages du client sont rendus sous forme de liste HTML :
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
Dans cet extrait, la première balise <% … %>
lance une instruction forEach
qui parcourt en boucle tous les éléments du tableau message
. Les délimiteurs <%
et %>
vous permettent de contrôler les extraits de HTML. Un nouvel élément de liste HTML, <li><%= message %></li>
, sera produit pour chaque élément de messages
. Avec ces changements, le serveur enverra la réponse en HTML lorsqu’une requête comme la suivante sera reçue :
$ curl http://myserver:8080/ --data message="This time" -c cookies.txt -b cookies.txt <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>My messages</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/layout.css"> </head> <body> <div id="interface"> <form action="/" method="post"> <p> <input type="text" name="message"> <input type="submit" value="Submit"> </p> </form> <ul> <li>This time</li> <li>in HTML</li> </ul> </div> </body> </html>
La séparation entre le code de traitement des requêtes et le code de présentation de la réponse rend le code plus propre et permet à une équipe de répartir le développement de l’application entre des personnes ayant des spécialités distinctes. Un concepteur web, par exemple, peut se concentrer sur les fichiers de modèle dans views/
et les feuilles de style associées, qui sont fournis en tant que fichiers statiques stockés dans le répertoire public/
sur le serveur d’exemple.
Exercices guidés
-
Comment configurer
express.static
pour que les clients puissent demander des fichiers dans le répertoireassets
? -
Comment le type de réponse, qui est spécifié dans l’en-tête de la demande, peut-il être identifié dans une route d’Express ?
-
Quelle méthode du paramètre de route
res
(response) génère une réponse au format JSON à partir d’un tableau JavaScript appelécontent
?
Exercices d’exploration
-
Par défaut, les fichiers de modèles d’Express se trouvent dans le répertoire
views
. Comment modifier ce paramètre pour que les fichiers de modèles soient stockés danstemplates
? -
Supposons qu’un client reçoive une réponse HTML sans titre (c’est-à-dire
<title></title>
). Après avoir vérifié le modèle EJS, le développeur trouve la balise<title><% title %></title>
dans la sectionhead
du fichier. Quelle est la cause probable du problème ? -
Utilisez les balises de modèle EJS pour écrire une balise HTML
<h2></h2>
avec le contenu de la variable JavaScripth2
. Cette balise ne doit être rendue que si la variableh2
n’est pas vide.
Résumé
Cette leçon couvre les méthodes de base fournies par Express.js pour générer des réponses statiques et formatées mais dynamiques. Peu d’efforts sont nécessaires pour configurer un serveur HTTP pour les fichiers statiques et le système de modèles EJS offre un moyen facile de générer du contenu dynamique à partir de fichiers HTML. Cette leçon aborde les concepts et procédures suivants :
-
Utilisation de
express.static
pour les réponses aux fichiers statiques. -
Comment créer une réponse qui correspond au champ de type de contenu dans l’en-tête de la requête.
-
Réponses structurées en JSON.
-
Utilisation des balises EJS dans les modèles HTML.
Réponses aux exercices guidés
-
Comment configurer
express.static
pour que les clients puissent demander des fichiers dans le répertoireassets
?Un appel à
app.use(express.static('assets'))
doit être ajouté au script du serveur. -
Comment le type de réponse, qui est spécifié dans l’en-tête de la demande, peut-il être identifié dans une route d’Express ?
Le client définit les types acceptables dans le champ d’en-tête
accept
, qui est mis en correspondance avec la propriétéreq.headers.accept
. -
Quelle méthode du paramètre de route
res
(response) génère une réponse au format JSON à partir d’un tableau JavaScript appelécontent
?La méthode
res.json()
:res.json(content)
.
Réponses aux exercices d’exploration
-
Par défaut, les fichiers de modèles d’Express se trouvent dans le répertoire
views
. Comment modifier ce paramètre pour que les fichiers de modèles soient stockés danstemplates
?Le répertoire peut être défini dans les paramètres initiaux du script avec
app.set('views', './templates')
. -
Supposons qu’un client reçoive une réponse HTML sans titre (c’est-à-dire
<title></title>
). Après avoir vérifié le modèle EJS, le développeur trouve la balise<title><% title %></title>
dans la sectionhead
du fichier. Quelle est la cause probable du problème ?La balise
<%= %>
doit être utilisée pour entourer le contenu d’une variable, comme dans<%= title %>
. -
Utilisez les balises de modèle EJS pour écrire une balise HTML
<h2></h2>
avec le contenu de la variable JavaScripth2
. Cette balise ne doit être rendue que si la variableh2
n’est pas vide.<% if ( h2 != "" ) { %> <h2><%= h2 %></h2> <% } %>