035.2 Lección 2
Certificación: |
Conceptos básicos de desarrollo web |
---|---|
Versión: |
1.0 |
Tema: |
035 Programación NodeJS server |
Objetivo: |
035.2 Conceptos básicos de NodeJS Express |
Lección: |
2 de 2 |
Introducción
Los servidores web tienen mecanismos muy versátiles para producir respuestas a las solicitudes de los clientes. Para algunas solicitudes, es suficiente que el servidor web proporcione una respuesta estática y sin procesar, porque el recurso solicitado es el mismo para cualquier cliente. Por ejemplo, cuando un cliente solicita una imagen que sea accesible para todos, es suficiente que el servidor envíe el archivo que contiene la imagen.
Pero cuando las respuestas se generan dinámicamente, es posible que deban estar mejor estructuradas que las líneas simples escritas en el script del servidor. En tales casos, es conveniente que el servidor web pueda generar un documento completo, que puede ser interpretado y renderizado por el cliente. En el contexto del desarrollo de aplicaciones web, los documentos HTML se crean comúnmente como plantillas y se mantienen separados del script del servidor, que inserta datos dinámicos en lugares predeterminados en la plantilla adecuada y luego envía la respuesta formateada al cliente.
Las aplicaciones web suelen consumir recursos tanto estáticos como dinámicos. Un documento HTML, incluso si se generó dinámicamente, puede tener referencias a recursos estáticos como archivos e imágenes CSS. Para demostrar cómo Express ayuda a manejar este tipo de demanda, primero configuraremos un servidor de ejemplo que entrega archivos estáticos y luego implementaremos rutas que generen respuestas estructuradas basadas en plantillas.
Archivos estáticos
El primer paso es crear el archivo JavaScript que se ejecutará como servidor. Sigamos el mismo patrón cubierto en lecciones anteriores para crear una aplicación Express simple: primero cree un directorio llamado server
y luego instale los componentes base con el comando npm
:
$ mkdir server $ cd server/ $ npm init $ npm install express
Para el punto de entrada, se puede usar cualquier nombre de archivo, pero aquí usaremos el nombre de archivo predeterminado: index.js
. La siguiente lista muestra un archivo básico index.js
que se utilizará como punto de partida para nuestro servidor:
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}`)
})
No tiene que escribir código explícito para enviar un archivo estático. Express tiene middleware para este propósito, llamado express.static
. Si su servidor necesita enviar archivos estáticos al cliente, simplemente cargue el middleware express.static
al comienzo del script:
app.use(express.static('public'))
El parámetro public
indica el directorio que almacena los archivos que el cliente puede solicitar. Las rutas solicitadas por los clientes no deben incluir el directorio public
, sino solo el nombre del archivo o la ruta al archivo relativa al directorio public
. Para solicitar el archivo public/layout.css
, por ejemplo, el cliente realiza una solicitud a /layout.css
.
Salida formateada
Si bien enviar contenido estático es sencillo, el contenido generado dinámicamente puede variar ampliamente. La creación de respuestas dinámicas con mensajes cortos facilita la prueba de aplicaciones en sus etapas iniciales de desarrollo. Por ejemplo, la siguiente es una ruta de prueba que simplemente envía al cliente un mensaje que envió mediante el método HTTP POST
. La respuesta puede simplemente replicar el contenido del mensaje en texto sin formato, sin ningún formato:
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
Una ruta como esta es un buen ejemplo para usar cuando se aprende Express y para propósitos de diagnóstico, donde una respuesta sin procesar enviada con res.send()
es suficiente. Pero un servidor útil debe poder producir respuestas más complejas. Continuaremos ahora para desarrollar ese tipo de ruta más sofisticado.
Nuestra nueva aplicación, en lugar de simplemente devolver el contenido de la solicitud actual, mantiene una lista completa de los mensajes enviados en solicitudes anteriores por cada cliente y devuelve la lista de cada cliente cuando se solicita. Una respuesta que combine todos los mensajes es una opción, pero otros modos de salida formateados son más apropiados, especialmente a medida que las respuestas se vuelven más elaboradas.
Para recibir y almacenar los mensajes del cliente enviados durante la sesión actual, primero debemos incluir módulos adicionales para manejar las cookies y los datos enviados a través del método HTTP POST
. El único propósito del siguiente servidor de ejemplo es registrar los mensajes enviados a través de POST
y mostrar los mensajes enviados anteriormente cuando el cliente emite una solicitud GET
. Así que hay dos rutas para la ruta /
. La primera ruta cumple con las solicitudes realizadas con el método HTTP POST
y la segunda cumple con las solicitudes realizadas con el método 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}`)
})
Mantuvimos la configuración de archivos estáticos en la parte superior, porque pronto será útil proporcionar archivos estáticos como layout.css
. Además del middleware cookie-parser
presentado en el capítulo anterior, el ejemplo también incluye el middleware uuid
para generar un número de identificación único que se pasa como una cookie a cada cliente que envía un mensaje. Si aún no está instalado en el directorio del servidor de ejemplo, estos módulos se pueden instalar con el comando npm install cookie-parser uuid
.
El arreglo global llamado messages
almacena los mensajes enviados por todos los clientes. Cada elemento de este arreglo consta de un objeto con las propiedades uuid
y message
.
Lo que es realmente nuevo en este script es el método res.json()
, usado al final de las dos rutas para generar una respuesta en formato JSON con el arreglo que contiene los mensajes ya enviados por el cliente:
// Send back JSON response
res.json(user_entries)
JSON es un formato de texto sin formato que le permite agrupar un conjunto de datos en una sola estructura que es asociativa: es decir, el contenido se expresa como claves y valores. JSON es particularmente útil cuando el cliente va a procesar las respuestas. Con este formato, un objeto o arreglo de JavaScript se puede reconstruir fácilmente en el lado del cliente con todas las propiedades e índices del objeto original en el servidor.
Debido a que estamos estructurando cada mensaje en JSON, rechazamos las solicitudes que no contienen application/json
en su encabezado accept
:
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
No se aceptará una solicitud realizada con un comando simple curl
para insertar un nuevo mensaje, porque curl
por defecto no especifica application/json
en el encabezado accept
:
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt Not Found
La opción -H "accept: application/json"
cambia el encabezado de la solicitud para especificar el formato de la respuesta, que esta vez será aceptado y respondido en el formato especificado:
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt -H "accept: application/json" ["My first message"]
Obtener mensajes usando la otra ruta se hace de una manera similar, pero esta vez usando el método HTTP GET
:
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -H "accept: application/json" ["Another message","My first message"]
Plantillas
Las respuestas en formatos como JSON son convenientes para la comunicación entre programas, pero el objetivo principal de la mayoría de los servidores de aplicaciones web es producir contenido HTML para consumo humano. Incrustar código HTML dentro del código JavaScript no es una buena idea, porque mezclar lenguajes en el mismo archivo hace que el programa sea más susceptible a errores y perjudica el mantenimiento del código.
Express puede trabajar con diferentes motores de plantillas que separan el HTML para contenido dinámico; la lista completa se puede encontrar en el Sitio de motores de plantillas express. Uno de los motores de plantillas más populares es Embedded JavaScript (EJS), que le permite crear archivos HTML con etiquetas específicas para la inserción de contenido dinámico.
Al igual que otros componentes Express, EJS debe instalarse en el directorio donde se ejecuta el servidor:
$ npm install ejs
A continuación, el motor EJS debe configurarse como el renderizador predeterminado en el script del servidor (cerca del comienzo del archivo index.js
, antes de las definiciones de ruta):
app.set('view engine', 'ejs')
La respuesta generada con la plantilla se envía al cliente con la función res.render()
, que recibe como parámetros el nombre del archivo de la plantilla y un objeto que contiene valores que serán accesibles desde dentro de la plantilla. Las rutas utilizadas en el ejemplo anterior se pueden reescribir para generar respuestas HTML y 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})
})
Tenga en cuenta que el formato de la respuesta depende del encabezado accept
que se encuentra en la solicitud:
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
Se envía una respuesta en formato JSON solo si el cliente la solicita explícitamente. De lo contrario, la respuesta se genera a partir de la plantilla index
. El mismo arreglo user_entries
alimenta tanto la salida JSON como la plantilla, pero el objeto utilizado como parámetro para esta última también tiene la propiedad title: "My messages"
, que se utilizará como título dentro de la plantilla.
Plantillas HTML
Al igual que los archivos estáticos, los archivos que contienen plantillas HTML residen en su propio directorio. De forma predeterminada, EJS asume que los archivos de plantilla están en el directorio views/
. En el ejemplo, se utilizó una plantilla llamada index
, por lo que EJS busca el archivo views/index.ejs
. La siguiente lista es el contenido de una plantilla simple views/index.ejs
que se puede usar con el código de ejemplo:
<!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 primera etiqueta EJS especial es el elemento <title>
en la sección <head>
:
<%= title %>
Durante el proceso de renderizado, esta etiqueta especial será reemplazada por el valor de la propiedad title
del objeto pasado como parámetro a la función res.render()
.
La mayor parte de la plantilla se compone de código HTML convencional, por lo que la plantilla contiene el formulario HTML para enviar nuevos mensajes. El servidor de prueba responde a los métodos HTTP GET
y POST
para la misma ruta /
, de ahí los atributos action="/"
y method="post"
en la etiqueta del formulario.
Otras partes de la plantilla son una mezcla de código HTML y etiquetas EJS. EJS tiene etiquetas para propósitos específicos dentro de la plantilla:
<% … %>
-
Inserta el control de flujo. Esta etiqueta no inserta contenido directamente, pero se puede usar con estructuras de JavaScript para elegir, repetir o suprimir secciones de HTML. Ejemplo iniciando un bucle:
<% messages.forEach( (message) => { %>
<%# … %>
-
Define un comentario, cuyo contenido es ignorado por el analizador. A diferencia de los comentarios escritos en HTML, estos comentarios no son visibles para el cliente.
<%= … %>
-
Inserta el contenido de escape de la variable. Es importante escapar del contenido desconocido para evitar la ejecución de código JavaScript, que puede abrir lagunas para los ataques de Scripting entre sitios (XSS). Ejemplo:
<%= title %>
<%- … %>
-
Inserta el contenido de la variable sin escapar.
La combinación de código HTML y etiquetas EJS es evidente en el fragmento donde los mensajes del cliente se representan como una lista HTML:
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
En este fragmento, la primera etiqueta <% … %>
inicia una declaración forEach
que recorre todos los elementos del arreglo message
. Los delimitadores <%
y %>
le permiten controlar los fragmentos de HTML. Se producirá un nuevo elemento de lista HTML, <li><%= message %></li>
, para cada elemento de messages
. Con estos cambios, el servidor enviará la respuesta en HTML cuando se reciba una solicitud como la siguiente:
$ 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 separación entre el código para procesar las solicitudes y el código para presentar la respuesta hace que el código sea más limpio y permite que un equipo divida el desarrollo de aplicaciones entre personas con distintas especialidades. Un diseñador web, por ejemplo, puede centrarse en los archivos de plantilla en views/
y hojas de estilo relacionadas, que se proporcionan como archivos estáticos almacenados en el directorio public/
del servidor de ejemplo.
Ejercicios guiados
-
¿Cómo debería configurarse
express.static
para que los clientes puedan solicitar archivos en el directorioassets
? -
¿Cómo se puede identificar el tipo de respuesta, que se especifica en el encabezado de la solicitud, dentro de una ruta Express?
-
¿Qué método del parámetro de ruta
res
(respuesta) genera una respuesta en formato JSON a partir de un arreglo de JavaScript llamadocontent
?
Ejercicios de exploración
-
Por defecto, los archivos de plantilla Express están en el directorio
views
. ¿Cómo se puede modificar esta configuración para que los archivos de plantilla se almacenen entemplates
? -
Supongamos que un cliente recibe una respuesta HTML sin título (es decir,
<title> </title>
). Después de verificar la plantilla EJS, el desarrollador encuentra la etiqueta<title><% title %></title>
en la secciónhead
del archivo. ¿Cuál es la causa probable del problema? -
Utilice etiquetas de plantilla EJS para escribir una etiqueta HTML
<h2></h2>
con el contenido de la variable JavaScripth2
. Esta etiqueta debe ser renderizada solo si la variableh2
no está vacía.
Resumen
Esta lección cubre los métodos básicos que proporciona Express.js para generar respuestas estáticas y formateadas pero dinámicas. Se requiere poco esfuerzo para configurar un servidor HTTP para archivos estáticos y el sistema de plantillas EJS proporciona una manera fácil de generar contenido dinámico a partir de archivos HTML. Esta lección abarca los siguientes conceptos y procedimientos:
-
Uso de
express.static
para respuestas de archivos estáticos. -
Cómo crear una respuesta para que coincida con el campo de tipo de contenido en el encabezado de la solicitud.
-
Respuestas estructuradas en JSON.
-
Uso de etiquetas EJS en plantillas basadas en HTML.
Respuestas a los ejercicios guiados
-
¿Cómo debería configurarse
express.static
para que los clientes puedan solicitar archivos en el directorioassets
?Se debe agregar una llamada a
app.use(express.static('assets'))
al script del servidor. -
¿Cómo se puede identificar el tipo de respuesta, que se especifica en el encabezado de la solicitud, dentro de una ruta Express?
El cliente establece tipos aceptables en el campo de encabezado
accept
, que se asigna a la propiedadreq.headers.accept
. -
¿Qué método del parámetro de ruta
res
(respuesta) genera una respuesta en formato JSON a partir de un arreglo de JavaScript llamadocontent
?El método
res.json()
:res.json(content)
.
Respuestas a los ejercicios de exploración
-
Por defecto, los archivos de plantilla Express están en el directorio
views
. ¿Cómo se puede modificar esta configuración para que los archivos de plantilla se almacenen entemplates
?El directorio se puede definir en la configuración inicial del script con
app.set('views', './templates')
. -
Supongamos que un cliente recibe una respuesta HTML sin título (es decir,
<title> </title>
). Después de verificar la plantilla EJS, el desarrollador encuentra la etiqueta<title><% title %></title>
en la secciónhead
del archivo. ¿Cuál es la causa probable del problema?La etiqueta
<%= %>
debe usarse para encerrar el contenido de una variable, como en<%= title %>
. -
Use etiquetas de plantilla EJS para escribir una etiqueta HTML
<h2></h2>
con el contenido de la variable JavaScripth2
. Esta etiqueta debe representarse solo si la variableh2
no está vacía.<% if ( h2 != "" ) { %> <h2><%= h2 %></h2> <% } %>