035.2 Lezione 2
Certificazione: |
Web Development Essentials |
---|---|
Versione: |
1.0 |
Argomento: |
035 Programmazione Server con Node.js |
Obiettivo: |
035.2 Fondamenti di Node.js Express |
Lezione: |
2 di 2 |
Introduzione
I server web hanno meccanismi molto versatili per produrre risposte alle richieste dei client. Per alcune richieste è sufficiente che il server web fornisca una risposta statica, non elaborata, perché la risorsa richiesta è la stessa per qualsiasi client. Per esempio, quando un client richiede un’immagine accessibile a tutti, è sufficiente che il server invii il file contenente l’immagine.
Ma quando le risposte vengono generate dinamicamente, potrebbe essere necessario strutturarle meglio delle semplici righe scritte nello script del server. In questi casi, è conveniente che il server web sia in grado di generare un documento completo, che può essere interpretato e visualizzato dal client. Nel contesto dello sviluppo di applicazioni web, i documenti HTML vengono comunemente creati come modelli e tenuti separati dallo script del server, che inserisce i dati dinamici in posizioni predeterminate nel modello appropriato e quindi invia la risposta formattata al client.
Le applicazioni Web spesso impiegano risorse sia statiche sia dinamiche. Un documento HTML, anche se è stato generato dinamicamente, può avere riferimenti a risorse statiche come file CSS e immagini. Per dimostrare in che modo Express aiuta a gestire questo tipo di domanda, configureremo un server di esempio che fornisce file statici; quindi implementeremo percorsi che generano risposte strutturate basate su modelli.
File Statici
Il primo passo è creare il file JavaScript che verrà eseguito come server. Seguiamo lo stesso schema trattato nelle lezioni precedenti per creare una semplice applicazione Express: prima creiamo una directory chiamata server
poi installiamo i componenti di base con il comando npm
:
$ mkdir server $ cd server/ $ npm init $ npm install express
Per il punto di ingresso, può essere usato qualsiasi nome di file, ma qui useremo il nome di default: index.js
. Il seguente elenco mostra un file di base index.js
che sarà usato come punto di partenza per il nostro server:
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}`)
})
Non è necessario scrivere codice esplicito per inviare un file statico. Express ha un middleware per questo scopo, chiamato express.static
. Se il tuo server ha bisogno di inviare file statici al client, basta caricare il middleware express.static
all’inizio dello script:
app.use(express.static('public'))
Il parametro public
indica la directory che contiene i file che il client può richiedere. I percorsi richiesti dai client non devono includere la directory public
, ma solo il nome del file, o il percorso del file relativo alla directory public. Per richiedere il file public/layout.css
, per esempio, il client fa una richiesta a /layout.css
.
Output Formattato
Mentre l’invio di contenuti statici è semplice, i contenuti generati dinamicamente possono variare ampiamente. Creare risposte dinamiche con messaggi brevi rende facile testare le applicazioni nelle loro fasi iniziali di sviluppo. Per esempio, quello che segue è un percorso di test che rimanda semplicemente al client un messaggio inviato con il metodo HTTP POST
. La risposta può semplicemente replicare il contenuto del messaggio in chiaro, senza alcuna formattazione:
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
Una rotta come questa è un buon esempio da usare quando si impara Express e per scopi diagnostici, dove una risposta grezza inviata con res.send()
è sufficiente. Ma un server eficace deve essere in grado di produrre risposte più complesse. Passeremo ora a sviluppare un tipo di rotta più sofisticata.
La nostra nuova applicazione, invece di inviare solo il contenuto della richiesta corrente, mantiene una lista completa dei messaggi inviati nelle richieste precedenti da ogni client e invia la lista di ogni client quando richiesto. Una risposta che unisce tutti i messaggi è un’opzione, ma altre modalità di output formattate sono più appropriate, specialmente quando le risposte diventano più elaborate.
Per ricevere e memorizzare i messaggi del client inviati durante la sessione corrente, dobbiamo prima includere moduli extra per gestire i cookie e i dati inviati tramite il metodo HTTP POST
. L’unico scopo del seguente server di esempio è quello di registrare i messaggi inviati tramite POST
e visualizzare i messaggi precedentemente inviati quando il client emette una richiesta GET
. Quindi ci sono due percorsi per il percorso /
. Il primo percorso soddisfa le richieste fatte con il metodo HTTP POST
e il secondo soddisfa le richieste fatte con il metodo 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}`)
})
Abbiamo mantenuto la configurazione dei file statici all’inizio del file, perché sarà presto utile fornire file statici come layout.css
. Oltre al middleware cookie-parser
introdotto nel capitolo precedente, l’esempio include anche il middleware uuid
per generare un numero identificativo unico passato come cookie a ogni client che invia un messaggio. Se non sono già installati nella directory del server di esempio, questi moduli possono essere installati con il comando npm install cookie-parser uuid
.
L’array globale chiamato messages
memorizza i messaggi inviati da tutti i client. Ogni elemento di questo array consiste in un oggetto con le proprietà uuid
e message
.
Ciò che è veramente nuovo in questo script è il metodo res.json()
, usato alla fine delle due rotte per generare una risposta in formato JSON con l’array contenente i messaggi già inviati dal client:
// Send back JSON response
res.json(user_entries)
JSON è un formato di testo semplice che permette di raggruppare un insieme di dati in una singola struttura associativa: il contenuto è espresso come chiavi e valori. JSON è particolarmente utile quando le risposte devono essere elaborate dal client. Usando questo formato, un oggetto o un array JavaScript può essere facilmente ricostruito sul lato client con tutte le proprietà e gli indici dell’oggetto originale sul server.
Poiché stiamo strutturando ogni messaggio in JSON, rifiutiamo le richieste che non contengono application/json
nella loro intestazione accept
:
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
Una richiesta fatta con un semplice comando curl
per inserire un nuovo messaggio non sarà accettata, perché curl
per default non specifica application/json
nell’intestazione accept:
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt Not Found
L’opzione -H "accept: application/json"
cambia l’intestazione della richiesta per specificare il formato della risposta, che questa volta sarà accettata e risponderà nel formato specificato:
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt -H "accept: application/json" ["My first message"]
Ottenere messaggi usando l’altro percorso è fatto in modo simile, ma questa volta usando il metodo HTTP GET
:
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -H "accept: application/json" ["Another message","My first message"]
I Template
Le risposte in formati come JSON sono convenienti per comunicare tra i programmi, ma lo scopo principale della maggior parte dei server di applicazioni web è quello di produrre contenuti HTML per l’utilizzo umano. Incorporare codice HTML all’interno di codice JavaScript non è una buona idea, perché mischiare i linguaggi nello stesso file rende il programma più suscettibile di errori e complica la manutenzione del codice.
Express può lavorare con diversi template engine che separano l’HTML per il contenuto dinamico; l’elenco completo può essere trovato sul sito Express template engines site. Uno dei motori di template più popolari è Embedded JavaScript (EJS), che permette di creare file HTML con tag specifici per l’inserimento di contenuto dinamico.
Come altri componenti di Express, EJS deve essere installato nella directory dove il server è in esecuzione:
$ npm install ejs
Successivamente, il motore EJS deve essere impostato come visualizzatore predefinito nello script del server (vicino all’inizio del file index.js
, prima delle definizioni delle rotte):
app.set('view engine', 'ejs')
La risposta generata con il template viene inviata al client con la funzione res.render()
, che riceve come parametri il nome del file del template e un oggetto contenente valori che saranno accessibili dall’interno del template stesso. Le rotte usate nell’esempio precedente possono essere riscritte per generare sia risposte HTML sia 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})
})
Si noti che il formato della risposta dipende dall’intestazione accept trovata nella richiesta:
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
Una risposta in formato JSON viene inviata solo se il client la richiede esplicitamente. Altrimenti, la risposta è generata dal template index
. Lo stesso array user_entries
alimenta sia l’output JSON che il template, ma l’oggetto usato come parametro per quest’ultimo ha anche la proprietà title: "My messages"
, che sarà usato come titolo all’interno del template.
Template HTML
Come i file statici, i file che contengono i template HTML risiedono nella loro propria directory. Per impostazione predefinita, EJS assume che i file dei template siano nella directory views/
. Nell’esempio è stato usato un template chiamato index
, quindi EJS cerca il file views/index.ejs
. Il seguente elenco è il contenuto di un semplice template views/index.ejs
che può essere usato con il codice di esempio:
<!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>
Il primo tag speciale EJS è l’elemento <title>
nella sezione <head>
:
<%= title %>
Durante il processo di visualizzazione questo tag speciale sarà sostituito dal valore della proprietà title
dell’oggetto passato come parametro alla funzione res.render()
.
La maggior parte del template è costituita da codice HTML convenzionale, quindi il template contiene il modulo HTML per inviare nuovi messaggi. Il server di test risponde ai metodi HTTP GET
e POST
per lo stesso percorso /
, da cui gli attributi action="/"
e method="post"
nel tag del modulo.
Altre parti del modello sono una combinazione di codice HTML e tag EJS. EJS ha tag per scopi specifici all’interno del modello:
<% … %>
-
Inserisce il controllo del flusso. Nessun contenuto è inserito direttamente da questo tag, ma può essere usato con strutture JavaScript per scegliere, ripetere o sopprimere sezioni di HTML. Esempio di avvio di un ciclo:
<% messages.forEach( (message) => { %>
<%# … %>
-
Definisce un commento, il cui contenuto viene ignorato dal parser. A differenza dei commenti scritti in HTML, questi commenti non sono visibili al client.
<%= … %>
-
Inserisce il contenuto sotto escape della variabile. È importante fare l’escape del contenuto sconosciuto per evitare l’esecuzione di codice JavaScript, che può aprire scappatoie per attacchi di cross-site scripting (XSS). Esempio:
<%= title %>
<%- … %>
-
Inserisce il contenuto della variabile senza escape.
Il mix di codice HTML e tag EJS è evidente nelle seguenti righe di codice dove i messaggi del client sono resi come una lista HTML:
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
Il primo tag <% … %>
inizia un’istruzione forEach
che scorre tutti gli elementi dell’array message
. I delimitatori <% e %>
ti permettono di controllare le parti di codice HTML. Un nuovo elemento della lista HTML, <li><%= message %></li>
, sarà prodotto per ogni elemento di messages
. Con queste modifiche, il server invierà la risposta in HTML quando viene ricevuta una richiesta come la seguente:
$ 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 separazione tra il codice per elaborare le richieste e il codice per presentare la risposta rende il codice più pulito e permette a un team di dividere lo sviluppo dell’applicazione tra persone con specialità distinte. Un web designer, per esempio, può concentrarsi sui file template in views/
e sui relativi fogli di stile, che sono forniti come file statici memorizzati nella directory public/
del server di esempio.
Esercizi Guidati
-
Come dovrebbe essere configurato
express.static
in modo che i client possano richiedere i file nella directoryassets
? -
Come si può identificare il tipo di risposta, che è specificato nell’intestazione della richiesta, in una rotta Express?
-
Quale metodo del parametro della rotta
res
(response) genera una risposta in formato JSON da un array JavaScript chiamatocontent
?
Esercizi Esplorativi
-
Per impostazione predefinita, i file dei template Express si trovano nella directory
views
. Come si può modificare questa impostazione in modo che i file dei template siano memorizzati intemplates
? -
Supponiamo che un client riceva una risposta HTML senza titolo (cioè
<title></title>
). Dopo aver verificato il template EJS, lo sviluppatore trova il tag<title><% title %></title>
nella sezionehead
del file. Qual è la probabile causa del problema? -
Usa i tag template EJS per scrivere un tag HTML
<h2></h2>
con il contenuto della variabile JavaScripth2
. Questo tag dovrebbe essere mostrato solo se la variabileh2
non è vuota.
Sommario
Questa lezione tratta dei metodi di base che Express.js fornisce per generare risposte strutturate e formattate ma dinamiche. È richiesto poco sforzo per impostare un server HTTP per i file statici e il sistema di template di EJS fornisce un modo semplice per generare contenuto dinamico da file HTML. Questa lezione tratta i seguenti concetti e procedure:
-
Usare
express.static
per le risposte ai file statici. -
Come creare una risposta che corrisponda al campo del tipo di contenuto nell’intestazione della richiesta.
-
Risposte strutturate in JSON.
-
Usare i tag EJS nei template basati su HTML.
Risposte agli Esercizi Guidati
-
Come dovrebbe essere configurato
express.static
in modo che i client possano richiedere i file nella directoryassets
?Una richiesta a
app.use(express.static('assets'))
dovrebbe essere aggiunta allo script del server. -
Come si può identificare il tipo di risposta, che è specificato nell’intestazione della richiesta, in una rotta Express?
Il client imposta i tipi accettabili nel campo dell’intestazione
accept
, che è mappato sulla proprietàreq.headers.accept
. -
Quale metodo del parametro della rotta
res
(response) genera una risposta in formato JSON da un array JavaScript chiamatocontent
?Il metodo
res.json()
:res.json(content)
.
Risposte agli Esercizi Esplorativi
-
Per impostazione predefinita, i file dei template Express si trovano nella directory
views
. Come si può modificare questa impostazione in modo che i file dei template siano memorizzati intemplates
?La directory può essere definita nelle impostazioni iniziali dello script con
app.set('views', './templates')
. -
Supponiamo che un client riceva una risposta HTML senza titolo (cioè
<title></title>
). Dopo aver verificato il template EJS, lo sviluppatore trova il tag<title><% title %></title>
nella sezionehead
del file. Qual è la probabile causa del problema?+ Il tag
<%= %>
dovrebbe essere usato per racchiudere il contenuto di una variabile, come in<%= title %>
. -
Usa i tag template EJS per scrivere un tag HTML
<h2></h2>
con il contenuto della variabile JavaScripth2
. Questo tag dovrebbe essere mostrato solo se la variabileh2
non è vuota.<% if ( h2 != "" ) { %> <h2><%= h2 %></h2> <% } %>