Introducción
Tiempo estimado: 10m
Ésta es la primera lección del curso de desarrollo de aplicaciones webs con Node mediante Express. No es un curso de Node, se asume que el estudiante ya tiene esos conocimientos; tampoco se introduce HTML5 ni CSS. No se presenta ningún framework cliente como Angular, Backbone.js, Ember.js o React, ni sus conocimientos son necesarios para el curso, aunque sí se recomienda que al menos uno de ellos se encuentre en el currículo del estudiante.
Para comenzar, se presenta el framework Express, la columna vertebral del servidor web. También se lista algunas organizaciones que están usando Node y Express, mostrando así la aceptación de la plataforma Node para el desarrollo de aplicaciones. A continuación, se muestra el concepto de pila web donde se enumera los componentes de software que participan en una aplicación web escrita en Node, tanto en el lado servidor como en el cliente. Finalmente, se resume el plan de estudio del curso.
Al finalizar la lección, el estudiante sabrá: • Qué es Express.
• Qué es una pila web. Express
Express es el framework de facto para el desarrollo de aplicaciones webs y servicios REST mediante Node con sus más de seis millones de descargas mensuales. Se utiliza para la implementación del lado back-end de la aplicación web. Inicialmente, lo diseñó y desarrolló TJ Holowaychuk. Actualmente, su desarrollo está en manos de la Node.js Foundation, entre cuyos miembros encontramos compañías como Google, Groupon, IBM, Intel, Joyent, Microsoft, PayPal, Red Hat, SAP y Yahoo!
Es de código abierto y se puede utilizar gratuitamente, tanto en aplicaciones personales como comerciales. Entre las organizaciones que lo usan encontramos Adobe, Amazon, CBS, Dow Jones, eBay, Financial Times, Flickr, Fox, Google, Groupon, HP, IBM, LinkedIn, Microsoft, Mozilla, MySpace, Netflix, Samsung, Telefónica, The New York Times, PayPal, Pearson, Pinterest, Rdio, Skype, Uber, Walmart y Yahoo!.
Recordemos que un framework es una pieza de software que implementa o proporciona una determinada funcionalidad o infraestructura. Siendo así más fácil el desarrollo y la reutilización de software. Huelga decir que debido a esta reutilización de componentes, el framework debe tenerse muy en cuenta durante la fase de diseño del software a desarrollar. Cuando se selecciona un framework en el desarrollo de un componente o aplicación, el framework es pieza angular en el diseño. Atendiendo al uso de uno u otro, el diseño puede ser distinto.
Pila web
El objeto principal de este curso es mostrar una pila para el desarrollo de aplicaciones webs en la plataforma Node. Una pila web (web stack) es la combinación de componentes de software usados para el desarrollo y la producción de aplicaciones webs. Una pila web de JavaScript (full-JavaScript web stack) es aquella que utiliza como único lenguaje de programación JavaScript.
A la hora de atender una pila web, se suele distinguir entre la parte servidora y la cliente. Parte servidora de la pila web
Por un lado, hay que distinguir el lado servidor. En Node, se puede distinguir básicamente lo siguiente:
En empresas pequeñas y medianas, donde los recursos financieros son escasos en muchas ocasiones, e incluso en algunas empresas grandes, se suele utilizar software de código abierto y gratuito como, por ejemplo, Linux, Node, CouchDB, Cassandra, MariaDB, PostgreSQL y Redis. Algunas empresas prefieren no arriesgar y usar software de fabricantes reconocidos mundialmente como, por ejemplo, Microsoft u Oracle, lo que aumenta los costes, pero se sienten más seguros. Es importante tener en cuenta que hay mucho software de código abierto y/o gratuito que suele implementarlo un grupo de programadores profesionales de la comunidad. Y en muchos de los desarrollos suele haber personal de organizaciones privadas que participan y ayudan en su desarrollo y después proporcionan soporte. Por ejemplo, detrás de Apache Cassandra se encuentra DataStax, una empresa con capital de millones de dólares y oficinas en EE.UU., Europa y Asia; detrás de PostgreSQL se encuentran EnterpriseDB, VMware y Red Hat, también con millones de dólares de capital y oficinas en EE.UU., Europa y Asia. Tanto DataStax como EnterpriseDB, VMware y Red Hat dan soporte 24x7 a aquellas empresas que lo necesitan; al igual que Microsoft y Oracle lo hacen con sus productos.
Por lo general, en entornos productivos se utiliza Linux como sistema operativo como, por ejemplo, Red Hat Enterprise, Ubuntu, CentOS o Debian. Estas máquinas pueden ser máquinas físicas o bien virtuales bajo VMware o KVM. Actualmente, algunas empresas han decidido virtualizar todo o la mayor parte de su entorno productivo, siendo frecuente que las máquinas servidoras no se encuentren en máquinas físicas dedicadas sino virtuales. En entornos de desarrollo, también es muy común el uso de virtualización porque reduce drásticamente los costes y su administración.
Cuando se usa Node, se suele utilizar como servidor web el framework Express. En muchas ocasiones, se combina con un servidor web como Apache o Nginx. Además, se suele utilizar Socket.IO para proporcionar comunicación bidireccional entre el cliente web y la aplicación servidora en tiempo real.
Cuando se usa una pila web JavaScript completa con Node, a nivel de sistema de gestión de bases de datos se suele utilizar bases de datos SQL, principalmente, MariaDB, MySQL, PostgreSQL, SQL Server y SQLite o bien NoSQL como Cassandra, CouchDB, MongoDB o Redis. Para el acceso a la base de datos desde la aplicación web se utiliza drivers de Node; existe drivers para la mayoría del top 10 de bases de datos.
Parte cliente de la pila web
En el lado cliente, también hay pila como, por ejemplo, la que se ilustra a continuación, que no es completa ni mucho menos:
Se puede utilizar frameworks como Angular de Google o React de Facebook. Además, se puede utilizar el
protocolo WebSocket, mediante Socket.IO, para la comunicación entre el cliente y el servidor, tal como vimos en el lado servidor. Además no puede faltar ni HTML5, ni CSS, ni como no JavaScript.
Informaci n del cursoó
Este curso es la primera parte de dos dedicados exclusivamente al framework Express.
Tiene como objetivo presentar los fundamentos del desarrollo de aplicaciones webs servidoras con Express. Dejándose para el siguiente, los aspectos avanzados como, por ejemplo, el desarrollo de APIs REST, la negociación de contenido, la compresión de contenido, el desarrollo de aplicaciones compuestas, la autenticación, etc.
Al finalizarlo, el estudiante sabrá: • Qué es Express.
• Cómo desarrollar aplicaciones Express.
• Cómo usar nodemon para monitorizar los cambios en los archivos y reiniciar automáticamente la aplicación web.
• Cómo usar Handlebars como motor de plantillas.
• Cómo procesar los datos remitidos por los usuarios mediante formularios en el lado servidor. • Cómo usar generadores para facilitar y mejorar el desarrollo.
Este curso se recomienda a aquellas personas que deseen mejorar su conocimiento de Node para extraer todo su jugo mediante el desarrollo de aplicaciones webs en el lado servidor.
No es un curso de Node ni HTML. Asume que el estudiante ya sabe programar en ambos lenguajes. Plan de estudio
El curso tiene una duración aproximada de 8 horas. Se divide en 16 lecciones, cada una de ellas con una parte de teoría y generalmente una de práctica. El enfoque a seguir es muy sencillo: ir lección a lección; primero hay que leer la teoría y, después, realizar la práctica. Se recomienda encarecidamente que el estudiante realice cada lección, tanto teoría como práctica, en el mismo día, con el menor número de interrupciones a lo largo de su estudio.
A continuación, se enumera las distintas lecciones y el tiempo estimado para su estudio:
Lección Teoría Práctica Descripción
1 Introducción 10min - Esta lección.
2 Aplicaciones Express 10min 10min Descripción de una aplicación de Express.
3 Pila de middleware 10min 25min Descripción de la pila de middleware de las aplicaciones
Express.
4 Contenido estático 15min 15min Cómo servir contenido estático en una aplicación Express.
5 Objeto petición 15min 10min Descripción del objeto request de las aplicaciones Express.
6 Registro de eventos 15min 15min Cómo llevar a cabo el registro de mensajes de una aplicación Express.
7 Objeto respuesta 25min 15min Descripción del objeto response de las aplicaciones Express.
8 nodemon 5min 15min Uso de nodemon para monitorizar cambios del proyecto y reiniciar la aplicación Express automáticamente.
9 Rutas 15min 15min Cómo definir y procesar las URLs específicas de la aplicación
Express.
10 Introducción a las plantillas 15min - Introducción al concepto de motor de plantillas.
11 Handlebars 25min 25min Descripción detallada del motor de plantillas Handlebars.
12 Redireccionamiento HTTP 5min 5min Cómo redireccionar recursos a otras URLs.
13 Cookies 20min 15min Cómo usar cookies para almacenar datos.
14 Estados de sesión 10min 15min Cómo usar el estado de sesión para almacenar datos particulares de las sesiones.
15 Formularios HTML 10min 10min Cómo procesar los datos remitidos por los usuarios mediante formularios HTML.
16 Generadores 10min 25min Cómo usar el generador oficial de Express y el de Justo para facilitar la creación de aplicaciones Express.
Informaci n de publicaci nó ó
Título Fundamentos de Express (Volumen 1)
Autor Raúl G. González - [email protected]
Primera edición Agosto de 2016
Versión actual 1.0.0
Versión de Express 4.14
Contacto [email protected]
Aplicaciones Express
Tiempo estimado: 10m
Una aplicación (application) no es más que un programa diseñado para hacer algo. Las aplicaciones webs (web applications) tienen como función servir recursos, documentos, archivos, contenido, etc. a través de una red como, por ejemplo, Internet. Las aplicaciones Express (Express applications) son aplicaciones webs servidoras desarrolladas mediante el framework Express.
La lección comienza introduciendo el concepto de aplicación Express. Cuáles son sus componentes. Y los archivos más importantes de la aplicación. A continuación, presentamos cómo configurar la aplicación mediante el uso de opciones. Finalizamos introduciendo el concepto de controlador de petición, pieza angular de las aplicaciones Express.
Al finalizar la lección, el estudiante sabrá: • Qué es una aplicación Express.
• Cuáles son los archivos claves de una aplicación Express. • Cómo configurar opciones de aplicación.
• Qué son los controladores de petición. Introducci nó
Tal como vimos en la primera lección, Express es un framework para el desarrollo de aplicaciones webs bajo la plataforma Node. Las aplicaciones escritas usando este framework se conocen formalmente como aplicaciones Express (Express application). Son servidoras, escuchan en un determinado puerto, se ejecutan bajo Node y sirven recursos mediante el protocolo HTTP.
La lógica de Express se encuentra en el módulo express descargable mediante NPM. Debe incorporarse a las dependencias del proyecto. Recordemos, propiedad dependencies del archivo package.json.
Los componentes más importantes de una aplicación Express son:
• Los controladores de petición, los cuales se encargan de procesar parcial o totalmente las peticiones recibidas por la aplicación.
• La pila de middleware que contiene el flujo de procesamiento que debe seguir toda petición HTTP recibida por la aplicación. Mediante este flujo, por un lado, se procesa las peticiones y, por otro lado, se escribe la respuesta a remitir al cliente.
• El encaminador, componente que se encarga de atender determinadas peticiones HTTP según su path o ruta. Mediante este elemento, podemos fijar controladores que se ejecuten sólo con determinadas peticiones.
• El motor de plantillas que facilita la generación de contenido dinámico. Estructura de directorios de una aplicaci n Expressó
Toda aplicación Express presenta una estructura de directorios en la que se almacena los distintos archivos que la forman. Express no tiene definida ninguna estructura especial, pero se recomienda utilizar una estructura similar en todos los proyectos de la organización, evitando así que cada una parezca de un padre y una madre.
Para su creación, se puede utilizar el generador oficial de Express, express-generator, o el de Justo.js, justo-generator-express. La función de un generador no es más que crear una estructura inicial para la aplicación. Esto puede hacerse con ambos generadores. El de Justo.js, además, añade comandos adicionales para crear ciertos componentes de la aplicación como encaminadores, rutas, vistas, etc.
Archivo app js.
Una vez creada la estructura de directorios de la aplicación, lo siguiente es crear la aplicación propiamente dicha. El script app.js, ubicado en la raíz del proyecto, es el archivo elegido por convenio para contener la lógica de arranque de la aplicación.
Este archivo tiene básicamente los siguientes objetivos: • Crear la aplicación Express.
• Configurar la aplicación.
• Enlazar la aplicación a una dirección de IP y puerto.
Creaci n de la aplicaci nó ó
Para crear la aplicación, no hay más que invocar la función express(), obtenida al importar el módulo express:
//imports
import express from "express"; …
//creación de la aplicación const app = express();
Configuraci n de la aplicaci nó ó
Una vez creada la aplicación, lo siguiente es configurarla. Generalmente, tal como veremos a lo largo del curso, consiste en indicar el motor de plantillas, los componentes de middleware a usar para procesar las peticiones HTTP, el registro de eventos, etc.
He aquí un ejemplo ilustrativo, que ayudará a ir abriendo boca:
//config
app.set("env", process.env.NODE_ENV || "development"); app.set("config", require(`./config/${app.get("env")}`)); app.set("host", process.env.HOST || app.get("config").host); app.set("port", process.env.PORT || app.get("config").port); //config template engine
app.set("views", path.join(__dirname, "views")); app.set("view engine", "hbs");
app.set("view cache", app.get("env") == "production"); app.engine("hbs", hbs.__express); hbs.registerPartials(path.join(__dirname, "views/partials/")); //middleware app.use(require("helmet")()); app.use(require("express-session")({ name: "sinfo", cookie: {maxAge: "60000"}, secret: "1468654010998", resave: false, saveUninitialized: false,
genid: function(req) { return Date.now().toString(); } }));
app.use(require("morgan")("combined"));
app.use(require("serve-favicon")(path.join(__dirname, "public", "/images/favicon.png"), { maxAge: 60000 })); app.use(require("serve-static")(path.join(__dirname, "public"), { maxAge: 60000 })); app.use(require("cookie-parser")("1468654005796")); app.use(require("body-parser").urlencoded({ extended: true }));
Solicitud de socket de red
Como paso final, hay que poner la aplicación Express a escuchar en un determinado puerto y dirección IP:
http.createServer(app).listen(app.get("port"), app.get("host"), function() { console.log("Listening...");
});
Opciones de aplicaci nó
Las opciones de aplicación (application settings) son parámetros a través de los cuales configurar determinados aspectos de la aplicación, los cuales iremos presentando a lo largo del curso. Las opciones se configuran mediante los métodos set(), enable() y disable() del objeto aplicación. Mediante set(), fijamos el valor de una opción:
set(name, value)
Parámetro Tipo de datos Descripción
name string Nombre de la opción.
value object Valor a asignar a la opción.
Mediante enable() y disable(), fijamos valores de opciones booleanas. Con enable(), fijamos true; y con disable(), false:
enable(name) disable(name)
Parámetro Tipo de datos Descripción
name string Nombre de la opción.
He aquí unos ejemplos ilustrativos:
app.set("views", path.join(__dirname, "views")); app.enable("trust proxy");
Para consultar los valores de las opciones, se utiliza los métodos get(), enabled() y disabled(). El primero devuelve el valor de la opción; el segundo si el valor de la opción es true; y el tercero, si es false:
get(name) : object enabled(name) : boolean disabled(name) : boolean
Parámetro Tipo de datos Descripción
name string Nombre de la opción.
Opci n de modo de entornoó
El modo de entorno (environment mode) indica el entorno de ejecución de la aplicación. De cara a Express, básicamente, se distinguen dos entornos: el de producción y el de desarrollo. El entorno de desarrollo (development environment) es aquel en el que se escribe o implementa la aplicación. Generalmente, nuestra máquina. En cambio, el entorno de producción (production environment) es aquel en el que se encuentra la aplicación para su uso por parte de los usuarios.
Atendiendo al entorno de ejecución de la aplicación, se suele configurar unas cosas u otras. Por ejemplo, cuando estamos en desarrollo, cuanta más información dispongamos de los errores que se producen, mejor. En cambio, en producción, se suele limitar la información de depuración porque puede ralentizar el sistema y dar información a los usuarios que no les interesa.
Mediante la opción env, se informa a Express bajo qué entorno de ejecución se encuentra la aplicación: development o production. De manera predeterminada, su valor lo toma de la variable de entorno NODE_ENV, o sea, del valor devuelto por process.env.NODE_ENV. Si no existe esta variable o no tiene valor, automáticamente se pondrá a development. Pero podemos fijar su valor explícitamente mediante el método set(), tal como acabamos de ver hace unos instantes.
Es muy común que las aplicaciones presenten configuraciones específicas según cada entorno. En estos casos, se usa proposiciones similares a:
if (app.get("env") == "development") {
//configuración específica del entorno de desarrollo
} else if (app.get("env") == "production") {
//configuración específica del entorno de producción }
Veamos un ejemplo ilustrativo:
if (app.get("env") == "development") { app.use(function(err, req, res, next) { res.status(err.status || 500); res.render("error", { message: err.message, error: err }); }); } Opci n x powered byó -
-De manera predeterminada, Express añade automáticamente el campo de cabecera X-Powered-By: Express a las respuestas HTTP remitidas por la aplicación. Por cuestiones de seguridad, no se recomienda adjuntar esta cabecera, porque proporciona información sobre el tipo de aplicación que se encuentra detrás. Algunos usuarios podrían utilizar esa información para explotar agujeros o problemas de seguridad del framework.
Por ello, se recomienda configurar la aplicación para que este campo de cabecera no se añada, mediante la opción booleana x-powered-by. Para desactivarla, no hay más que usar el método disable():
app.disable("x-powered-by"); Opciones host y port
Mediante las opciones host y port, especificamos la interfaz de red y el puerto en el que debe escuchar la aplicación Express. Actualmente, estas opciones no las usa internamente Express. Pero se recomienda crearla durante la fase de configuración y usarla en el momento de atarla a una dirección de IP y puerto. Así pues, tendremos algo como:
//crear aplicación app = express(); //configurar aplicación app.set(…); app.set(…); app.set("config", require(`./config/${app.get("env")}`)); app.set("host", process.env.HOST || app.get("config").host); app.set("port", process.env.PORT || app.get("config").port); //…
//atar aplicación a IP y puerto
http.createServer(app).listen(app.get("port"), app.get("host"), function() { …
});
Las opciones no definidas por Express se conocen formalmente como opciones personalizadas (custom settings). Son un buen lugar donde almacenar información útil de la aplicación.
Controladores de petici nó
Un controlador de petición (request handler) es una función que procesa total o parcialmente una petición HTTP. Se registran principalmente en la pila de middleware o en los encaminadores, componentes que veremos más adelante en este curso.
La signatura de estos controladores es:
function(req, res) function(req, res, next)
Parámetro Tipo de datos Descripción
req Request Solicitud HTTP en procesamiento.
res Response Respuesta HTTP que se está generando.
next function Función que debe invocar el controlador para indicarle a Express que ejecute el siguiente componente de la pila de procesamiento: next([error]).
Cuando Express invoca la función, le pasará como argumentos: la solicitud HTTP, que podrá accederse mediante el parámetro req; la respuesta HTTP, que se puede acceder mediante el parámetro res; y una función especial, next(), a través de la cual la función indica al motor si debe seguir invocando los restantes controladores registrados en la aplicación.
Este último parámetro, también conocido como función de continuación de flujo, tiene dos sobrecargas:
function next() function next(error)
Cuando no le pasamos ningún argumento, estamos indicando que el controlador ha finalizado correctamente y hay que pasar al siguiente. En cambio, si le pasamos un argumento, éste se comportará como error y le indica al motor que ejecute la función de control de errores. Más adelante en el curso, veremos más acerca de este tipo de funciones. Por ahora, asumamos que todo va siempre bien.
Es posible, para determinadas solicitudes HTTP, que no sea necesario que el mensaje recibido del cliente tenga que atravesar toda la pila. En ocasiones, algunos controladores pueden dar por finalizado el procesamiento, porque han generado toda la respuesta o se consideran el último eslabón de la cadena para esa determinada petición. Para que una petición fluya a lo largo de todo el flujo de procesamiento o de parte de él se utiliza el parámetro función next().
Cuando una función termina su procesamiento debe invocar el parámetro next() para informar, así, que ha finalizado su funcionalidad, pero no da por terminado el flujo de procesamiento. Y por lo tanto hay que invocar el siguiente controlador registrado. Si no se hace, el motor asumirá que este controlador ha finalizado el procesamiento de la petición HTTP y la respuesta está generada completamente, por lo que no continuará ejecutando más controladores. Y dará por terminado el procesamiento de la petición HTTP.
Por ejemplo, supongamos que deseamos implementar un registro de eventos que escriba una entrada por cada solicitud HTTP recibida. Esta función de middleware no genera nada en el objeto respuesta. Sólo lee la petición HTTP, extrae la información que necesita y escribe la entrada en el registro de eventos. Nada más. Pues este tipo de funciones debe invocar la función next() para indicarle así a Express que continúe con la función que le sigue. Si no lo hace, el procesamiento finalizará sin que la respuesta se haya redactado.
Otro ejemplo. Ahora, consideremos una función que se encarga de servir determinados archivos estáticos del disco. Si el archivo no se encuentra en disco, la función no puede hacer su trabajo. En vez de generar un error, lo que hace es indicarle al motor que continúe con el flujo de procesamiento. La función no ha podido hacer nada, pero igual alguna de las funciones que le sigue puede hacer algo. En cambio, si la función localiza el archivo, generará la respuesta a remitir al cliente. En este caso, está claro que nadie más debe hacer nada en la respuesta por lo que se considera el único o el último que debe participar en su redacción. Por lo que no invocará el parámetro función next(), indicándole así al motor que la respuesta ya se ha generado y no hay que continuar con lo que resta del flujo de procesamiento.
He aquí un ejemplo ilustrativo de una función controladora que escribe en la salida estándar información sobre la petición HTTP en procesamiento:
app.use(function(req, res, next) {
console.log(req.ip, req.method, req.originalUrl); next();
});
Aplicaciones Express
(Práctica)
Tiempo estimado: 10min
El objeto de esta práctica es asentar y consolidar los conceptos presentados en la parte teórica de la lección. Al finalizarla, el estudiante:
• Habrá creado una aplicación Express. • Habrá registrado controladores de petición. Objetivos
En esta práctica, vamos a crear una sencilla aplicación Express con un controlador que devuelva el mismo contenido para todo recurso solicitado por los usuarios. La idea es presentar los elementos claves de una aplicación, así como ver la creación de controladores de petición.
A lo largo del curso, en la parte teórica se presentará Express usando la especificación ES6/ES2015 de JavaScript. Dejamos la especificación ES5 para las prácticas. Finalizado este curso, se recomienda encarecidamente el uso de ES2015 o superior.
Creaci n del proyectoó
Vamos a crear el proyecto de la aplicación. En vez de usar un generador, lo haremos todo a mano, para reducir al mínimo los archivos y para obligarnos a comprender cómo funciona Express. Pero una vez se encuentre cómodo, debe de utilizar un generador, por ejemplo, express-generator o justo-generator-express. Al final del curso, se dedica una lección a estos generadores.
1. Abrir una consola.
2. Crear el directorio de la práctica. 3. Ir al directorio de la práctica.
4. Crear el archivo package.json con el siguiente contenido:
{
"name": "express-app", "version": "0.1.0", "private": true, "scripts": {
"start": "node ./app.js" },
"dependencies": { "express": "*" }
}
5. Crear el archivo app.js con el siguiente contenido:
"use strict"; //imports
const express = require("express"); const http = require("http"); //app
const app = express();
const index = "<!doctype html>\n" + "<html>\n" +
"<head><title>Express app</title></head>\n" +
"<body><p>¡Hola Mundo!</p></body>\n" + "</html>"; //configuración app.set("port", 8080); app.disable("x-powered-by"); //controladores
app.get("*", function(req, res) { res.send(index); }); //listen http.createServer(app).listen(app.get("port"), function() { console.log("Listening..."); });
Observe cómo se ha configurado las opciones port y x-powered-by. Una mendiante set() y la otra mediante disable().
Por otra parte, observe el controlador de peticiones HTTP. En este caso, no hace uso de ningún parámetro next(), ni siquiera lo define. Eso significa que cuando él termine, cualquier controlador que se haya definido tras él no se ejecutará. En este caso, no se ha definido ninguno, pero si lo hubiéramos hecho, no se ejecutaría.
6. Instalar dependencias:
> npm install
7. Iniciar la aplicación Express:
> npm start
No es necesario hacerlo con npm, pero es muy común hacerlo así. 8. Abrir el navegador.
9. Ir a http://localhost:8080.
Debe aparecer el mensaje ¡Hola Mundo! 10. Ir a http://localhost:8080/no-importa.
Debe aparecer otra vez el mensaje ¡Hola Mundo!
Pila de middleware
Tiempo estimado: 10min
Una vez introducidos los conceptos de aplicación Express y de controladores de petición, uno de los primeros componentes a comprender es la pila de middleware o, lo que es lo mismo, el flujo de procesamiento de la aplicación.
Comenzamos la lección distinguiendo los dos tipos básicos de aplicación, las monolíticas y las multicapa. Seguimos con los componentes de middleware y la pila de middleware. A continuación, mostramos cómo registrar funciones de middleware en la pila. Finalizamos describiendo cómo controlar los errores producidos durante el procesamiento de la petición HTTP en curso.
Al finalizar la lección, el estudiante sabrá: • Qué es un componente de middleware.
• Qué es la pila de middleware o flujo de procesamiento. • Cómo registrar funciones de middleware en la pila.
• Qué diferencia hay entre las funciones normales de middleware y las de control de errores. • Cuándo se invoca las funciones normales y cuándo las de control de errores.
Introducci nó
Básicamente, el desarrollo de aplicaciones webs se puede hacer desde dos puntos de vista, de manera monolítica o multicapa. Una aplicación monolítica (monolithic application) es aquella en la que se desarrolla todo en un único componente, capa o controlador. En cambio, una aplicación multicapa (multitier application) es aquella que utiliza varios componentes, capas o controladores, cada uno de los cuales con una funcionalidad y procesamiento bien claro y definido.
Actualmente, preferimos los entornos multicapa porque son más sencillos de desarrollar y, por encima de todo, de mantener y probar. Como no podía ser de otra manera, Express permite el desarrollo de aplicaciones multicapa y lo hace mediante el uso de la pila de middleware.
Antes de presentar la pila, hay que tener claro que es el middleware. Un componente de middleware (middleware component) no es más que el término formal con que se conoce a una pieza de software reutilizable. La cual realiza una determinada funcionalidad de procesamiento de las peticiones HTTP. Así, por ejemplo, tenemos componentes de middleware para llevar a cabo el proceso de autenticación, la aplicación de restricciones de seguridad, la publicación de contenido estático, etc.
Por su parte, la pila de middleware (middleware stack), también conocida como flujo de procesamiento (processing flow) o conducto (pipeline), contiene la secuencia de funciones de middleware que procesan, una detrás de otra, las peticiones HTTP recibidas de los clientes para construir, entre todas ellas, las respuestas HTTP a remitir como contestación. Todo hay que decirlo, algunas funciones no participan en la redacción de la respuesta como, por ejemplo, el middleware de registro de eventos que escribe en un archivo o en la salida estándar información sobre la solicitada en procesamiento.
La idea que se esconde bajo este sistema de middleware es que toda petición que reciba la aplicación pase por el flujo de funciones de middleware registradas en la pila y que, entre todas ellas, lleven a cabo su tratamiento, generándose la respuesta a remitir al cliente.
En Express, la pila está formada por funciones, conocidas formalmente como funciones de middleware (middleware functions) o controladores de petición (request handlers), disponibles a través de componentes de middleware. A modo de ejemplo, consideremos el componente serve-static. Se utiliza cuando deseamos que la aplicación sirva contenido estático. Este componente dispone de una función, no middleware, que recibe la ruta del directorio que contiene los archivos que puede servir estáticamente. Y devuelve la función de middleware que hay que registrar en el flujo de procesamiento para que así pueda servirlos cuando sea necesario.
Tal como veremos a lo largo del curso, un componente de middleware puede ser básicamente dos cosas: • Una función de middleware por sí misma.
• Una función que devuelve funciones de middleware.
En cualquier caso, lo importante a recordar es que en el flujo de procesamiento de la aplicación sólo debemos registrar funciones de middleware o controladores de petición.
El componente de la aplicación Express que se encarga de ejecutar ordenadamente las distintas funciones de middleware registradas en la pila de procesamiento, se conoce formalmente como motor de middleware (middleware engine).
Todos los componentes de middleware tienen acceso tanto a la petición en procesamiento como a la respuesta a remitir al cliente. Así pues, pueden analizar su contenido como, por ejemplo, sus cabeceras HTTP o el cuerpo del mensaje, y tras analizar la parte que les corresponde, generar, si es necesario, la parte de la respuesta HTTP asignada a su funcionalidad. Grosso modo, cuando se recibe una petición HTTP, la aplicación se la pasa al motor de middleware, el cual va invocando, una a una en orden de registro, las distintas funciones registradas. Tras finalizar la ejecución de la pila, el motor le pasa la respuesta HTTP generada por el middleware a la aplicación para su envío al cliente.
Como toda función de middleware o controlador de petición tiene acceso a la respuesta HTTP que se remitirá al cliente, está claro que podrá consultar cualquier modificación o añadidura que haya realizado cualquiera de los componentes anteriores de la pila. Por ejemplo, el middleware encargado de la generación de entradas en el registro de eventos sólo trabaja sobre la solicitud HTTP, no así sobre la respuesta.
Funciones de middleware
Como ya sabemos, una función de middleware es una función JavaScript que realiza una determinada funcionalidad de la aplicación. Es un controlador de petición. Puede trabajar sobre el objeto que representa la petición HTTP recibida del cliente y/o el objeto que representa la respuesta HTTP que la aplicación acabará remitiendo al cliente como contestación.
La función, al ser un controlador de petición, debe presentar la siguiente signatura:
function(req, res) function(req, res, next)
Parámetro Tipo de datos Descripción
req Request Solicitud HTTP en procesamiento.
res Response Respuesta HTTP que se está generando.
next function Función que debe invocar el middleware para indicarle al motor de middleware que ejecute el siguiente componente de la pila de procesamiento: next([error]).
Registro de funciones de middleware
Mediante el registro de middleware (middleware register) se añade, al final de la pila de procesamiento, una función de middleware. Se realiza mediante el método use() de la aplicación:
use(fn)
use(route, fn)
Parámetro Tipo de datos Descripción
route string Ruta a la que se aplicará el componente de middleware.
Si no se especifica, se asumirá que se debe ejecutar para toda petición.
fn function Función que implementa la lógica del componente de middleware.
Con el registro de funciones de middleware lo que estamos haciendo es añadir o dotar de más funcionalidad a la aplicación.
Orden de registro
El orden en que se registra las funciones de middleware es importante. Si un determinado componente utiliza algo generado por otro, es necesario registrar primero la función de la que depende para que de esta manera el motor de middleware la invoque primero y, así, la segunda pueda acceder a cualquier objeto generado por éste.
Middleware de control de errores
Básicamente, hay dos tipos de funciones de middleware, las normales y las de control de errores.
Una función normal (normal function) es aquella que se ejecuta mientras no se produzca error. Las vistas hasta ahora. Tienen dos o tres parámetros: la solicitud, la respuesta y la función de continuación de flujo, next(). En cambio, una función de control de errores (error-handling function) es aquella que atiende y procesa un error comunicado a través de la función de continuación de flujo.
Las funciones de error se registran también mediante el método use() de la aplicación, pero tienen una signatura distinta de las normales:
function(error, req, res, next)
Parámetro Tipo de datos Descripción
error object Error propagado mediante una invocación next(error) anterior.
req Request Petición HTTP en procesamiento.
res Response Respuesta HTTP que se está generando.
next function Función que debe invocar la función de control de errores para indicarle al motor de middleware que ejecute la siguiente de la pila de procesamiento.
La función de continuación de flujo, next(), tiene un comportamiento distinto según se invoque con o sin argumento de error:
• Si no le pasamos ningún argumento, invocará la siguiente función normal registrada en la pila. Esto es así tanto si lo hacemos desde una función normal como desde una de control de errores.
• Si le pasamos un argumento, invocará la siguiente función de control de errores registrada en la pila. Esto es así tanto si lo hacemos desde una función normal como desde una de control de errores.
Generalmente, las funciones de control de errores se registran después de las funciones normales. He aquí un ejemplo ilustrativo:
app.use(function(req, res, next) { //normal
next(); });
app.use(function(req, res, next) { //otra normal
});
app.use(function(err, req, res, next) { //control de errores
next(err); });
app.use(function(err, req, res, next) { //otra de control de errores
});
Si todo va bien en el flujo de procesamiento, las funciones de error no se ejecutan nunca. Recordemos, sólo cuando alguna función de middleware ejecuta la función next() con un argumento, el cual se considera como el error. Y ojo, si tenemos varias, para que se siga con la cadena de control de errores, es necesario que las
controladoras de error invoquen la función next() con el error, porque si se ejecuta sin error, se devolverá el flujo a las funciones normales. Si una deja de hacerlo, las funciones de error que le sigan no serán invocadas por el motor de middleware.
Finalmente, hay que decir que si la última función de error registrada en la pila invoca next() con el error, la aplicación mostrará el error por la consola.
Captura de excepciones
Cuando una función de middleware, sea cual sea su tipo, propaga un error mediante la sentencia throw, la aplicación Express lo captura. Finaliza el flujo de procesamiento normal. A continuación, el motor de middleware busca la función de control de errores en la pila que siga a aquella que propagó el error. Y finalmente, genera una respuesta HTTP con código de estado 500 Internal Server Error y se lo remite al cliente.
Pila de middleware
(Práctica)
Tiempo estimado: 25min
El objeto de esta práctica es asentar y consolidar los conceptos presentados en la parte teórica de la lección. Al finalizarla, el estudiante:
• Habrá escrito funciones de middleware.
• Habrá registrado funciones de middleware en la pila de middleware.
• Habrá trabajado con funciones de middleware normales y de control de errores. Objetivos
En esta práctica, vamos a crear una aplicación Express que muestre, por un lado, cómo registrar funciones de middleware en una aplicación Express y, por otro lado, cómo trabajar con la función de continuación de flujo next().
Creaci n del proyectoó
Para comenzar, vamos a crear el proyecto de la aplicación. 1. Abrir una consola.
2. Crear el directorio de la práctica. 3. Ir al directorio de la práctica.
4. Crear el archivo package.json con el siguiente contenido:
{
"name": "express-app", "version": "0.1.0", "private": true, "scripts": {
"start": "node ./app.js" },
"dependencies": { "express": "*" }
}
5. Crear el archivo app.js con el siguiente contenido:
"use strict"; //imports
const express = require("express"); const http = require("http"); //app
const app = express();
const index = "<!doctype html>\n" + "<html>\n" +
"<head><title>Express app</title></head>\n" + "<body><p>¡Hola Mundo!</p></body>\n" +
"</html>"; //config app
app.get("*", function(req, res) { res.send(index); });
//listen http.createServer(app).listen(8080, function() { console.log("Listening..."); }); 6. Instalar dependencias: > npm install
7. Iniciar la aplicación Express:
> npm start
8. Abrir el navegador. 9. Ir a http://localhost:8080.
Debe aparecer el mensaje ¡Hola Mundo!
Registro de funciones normales de middleware
En este punto, vamos a jugar un poco con las funciones normales de middleware, recordemos, aquellas que se ejecutan siempre que todo va bien. La idea es crear tres funciones. La primera mostrará por la consola información sobre la petición HTTP bajo procesamiento; la segunda añadirá el texto Hello a la respuesta HTTP; mientras que la tercera World! Con esto en mente, observe que varias funciones pueden participar en la generación de la respuesta. No siendo necesario que toda la respuesta la genere la misma función. Y además, si es necesario, una función no tiene por qué hacer nada sobre la respuesta.
1. Editar el archivo app.js.
2. Suprimir la definición de la constante index. 3. Suprimir lo siguiente:
//config app
app.get("*", function(req, res) { res.send(index); });
4. Añadir las siguientes funciones de middleware :
//logger
app.use(function(req, res, next) {
console.log(req.ip, req.method, req.originalUrl); next();
});
//genera Hello
app.use(function(req, res, next) { console.log("genera Hello"); res.setHeader("Content-Type", "text/plain"); res.write("Hello"); next(); }); //genera World!
app.use(function(req, res, next) { console.log("genera World!"); res.write("World!"); res.end(); next(); }); 5. Guardar cambios. 6. Ir a la consola. 7. Detener la aplicación. 8. Arrancarla de nuevo: > npm start 9. Ir al navegador.
10. Solicitar de nuevo http://localhost:8080.
Debe aparecer el mensaje HelloWorld! 11. Ir a la consola.
12. Revisar los mensajes que han mostrado las tres funciones.
Es muy probable que aparezca una petición del recurso /favicon.ico. Es normal. Más adelante en el curso, haremos hincapié en él.
Pruebas con la funci n nextó ()
A continuación, vamos a ver qué ocurre cuando una función de middleware no invoca el parámetro next(): 1. Editar el archivo app.js.
2. Suprimir la invocación del parámetro next() en la función que genera Hello:
//genera Hello
app.use(function(req, res, next) { console.log("genera Hello"); res.setHeader("Content-Type", "text/plain"); res.write("Hello"); }); 3. Guardar cambios. 4. Ir a la consola. 5. Reiniciar la aplicación.
6. Ir al navegador y solicitar de nuevo http://localhost:8080.
El navegador parecerá que se ha quedado colgado. En Chrome, más tarde o temprano, recibiremos un mensaje como el siguiente:
7. Ir a la consola y comprobar qué mensajes aparecen.
Sólo debe aparecer el que informa del recurso solicitado y el de la función que genera Hello. O sea, los mensajes de las dos primeras funciones. Esto se debe a que el motor de middleware está esperando que la segunda función invoque el parámetro next().
8. Editar el archivo app.js.
9. Dejar la función, que genera Hello, como sigue:
//genera Hello
app.use(function(req, res, next) { console.log("Hello");
res.setHeader("Content-Type", "text/plain"); res.write("Hello"); setTimeout(function() { next(); }, 5000); });
Lo que estamos haciendo es añadir un retardo de cinco segundos, tras el cual, la función invocará el parámetro next().
10. Guardar cambios. 11. Ir a la consola.
12. Reiniciar la aplicación.
13. Ir al navegador y solicitar http://localhost:8080.
Esta vez, la respuesta llega pero cinco segundos más tarde, por la espera que hemos añadido. Esto indica algo muy sencillo: el motor invoca una función de middleware y espera a que ésta ejecute next() para continuar. De esta manera, una función de middleware puede ejecutar código asíncronamente y, cuando ha terminado, invocar next() para que el motor continúe con la siguiente función de la pila.
Registro de funciones de control de errores
Para finalizar, vamos a registrar funciones de middleware que controlen los errores propagados por la pila de middleware mediante el parámetro next():
1. Editar el archivo app.js.
2. Modificar las funciones de middleware que tenemos por lo siguiente:
//logger
app.use(function(req, res, next) {
console.log(req.ip, req.method, req.originalUrl); next();
});
//genera Hello
app.use(function(req, res, next) { console.log("genera Hello"); res.setHeader("Content-Type", "text/plain"); res.write("Hello"); next(); }); //control de errores #1
app.use(function(err, req, res, next) { console.log("control de errores #1"); next(err);
});
//genera World!
app.use(function(req, res, next) { console.log("genera World"); res.write("World!");
res.end(); });
//control de errores #2
app.use(function(err, req, res, next) { console.log("control de errores #2"); });
Por buenas prácticas, se recomienda ubicar las funciones de control de error al final, después de las normales.
3. Guardar cambios.
4. Ir a la consola.
5. Reiniciar la aplicación. 6. Ir al navegador.
7. Solicitar http://localhost:8080.
8. Ir a la consola y comprobar que no se ha ejecutado ninguna de las funciones de control de errores. Como no se ha invocado next() con error, el motor de middleware las omiten.
9. Editar el archivo app.js y modificar la siguiente función:
//genera World!
app.use(function(req, res, next) { console.log("genera World"); res.write("World!"); res.end(); next("mensaje de error"); }); 10. Guardar cambios. 11. Ir a la consola. 12. Reiniciar la aplicación. 13. Solicitar http://localhost:8080.
A pesar de haberse producido un error, la respuesta se ha generado y enviado al cliente.
Por otra parte, observe que se ha ejecutado la función de control de errores #2, no desde la #1. Es importante tener claro que no existe un flujo normal y otro de control de errores. Sólo hay uno. Lo que ocurre es que si se invoca next(), sin argumentos, el motor de middleware pasa a la siguiente función normal registrada en la pila, omitiendo toda función de control de errores que se encuentre. Mientras que si se ejecuta con argumento, a la siguiente de control de errores de la pila, omitiéndose cualquier función normal que se encuentre.
14. Editar el archivo app.js.
15. Añadir la invocación next() con el error al final de la segunda función de control de errores :
//control de errores #2
app.use(function(err, req, res, next) { console.log("control de errores #2"); next(err); }); 16. Guardar cambios. 17. Ir a la consola. 18. Reiniciar la aplicación. 19. Ir al navegador. 20. Solicitar http://localhost:8080.
Ahora, como la aplicación no puede encontrar otra función de control de errores en la pila, la aplicación captura el error y lo muestra en la consola.
21. Ir al archivo app.js.
22. Modificar la siguiente función:
//control de errores #2
app.use(function(err, req, res, next) { console.log("control de errores #2"); next();
});
23. Añadir la siguiente función debajo de logger:
app.use(function(req, res, next) { throw new Error("mensaje de error."); }); 24. Guardar cambios. 25. Ir a la consola. 26. Reiniciar la aplicación. 27. Ir al navegador. 28. Solicitar http://localhost:8080.
Debemos recibir como respuesta un mensaje con código de error 500 Internal Server Error y además la pila de traza del error.
29. Ir a la consola y comprobar que se han ejecutado los controladores de error. 30. Cerrar todo.
Contenido estático
Tiempo estimado: 15min
Una de las primeras cosas que tenemos que aclarar es la diferencia entre contenido estático y dinámico. El objeto de esta lección es presentar cómo servir contenido estático desde una aplicación Express.
Comenzamos la lección introduciendo el concepto de contenido y la diferencia entre contenido estático y dinámico. A continuación, presentamos el componente de middleware serve-static para servir contenido estático. Lo que ayuda a no tener que implementar esta lógica nosotros mismos, aumentando así nuestra productividad. Además, se presenta el concepto de caché HTTP y algunas cabeceras de respuesta configurables en serve-static. Finalmente, se muestra el middleware serve-favicon para servir el favicono del sitio o aplicación web.
Al finalizar la lección, el estudiante sabrá:
• Cómo configurar la aplicación Express para que sirva contenido estático.
• Cómo utilizar el componente de middleware serve-static para servir contenido estático.
• Cómo configurar cabeceras HTTP como Cache-Control, Last-Modified y Accept-Ranges en serve-static. • Cómo configurar el middleware serve-favicon para servir el favicono del sitio o aplicación web.
Introducci nó
El contenido (content) es la secuencia de bytes que forman un recurso web, generalmente, un archivo de texto, un documento HTML o una imagen. Las aplicaciones pueden servir o publicar tanto contenido estático como dinámico.
El contenido estático (static content) es aquel que la aplicación puede servir directamente de disco. Mientras que el contenido dinámico (dynamic content) aquel que requiere su generación cada vez que se solicita.
Hay organizaciones que utilizan un servidor web como Apache o Nginx para servir directamente el contenido estático de una aplicación web, delegando el contenido dinámico en la aplicación Express. Otras prefieren que la aplicación web sirva tanto el contenido estático como el dinámico. Para gustos, colores.
Middleware serve static
-Podemos utilizar el middleware serve-static para configurar carpetas de la aplicación cuyo contenido es estático. La idea es indicarle al middleware que cuando entre una petición web, compruebe si el recurso existe en alguno de los directorios que le hemos indicado y, si así es, lo sirva directamente. En caso de no estarlo, dejará pasar la petición por el flujo de procesamiento con intención de que lo genere la aplicación dinámicamente u otro componente estáticamente.
Para poder utilizar esta función de middleware, debemos importarla y registrarla. No hay que olvidar añadir el paquete serve-static a las dependencias de la aplicación, esto es, a la propiedad dependencies del archivo package.json.
Generalmente, se conoce formalmente como directorio público (public directory) aquel que contiene contenido estático. Por convenio, se utiliza el directorio public de la aplicación. En aplicaciones pequeñas e incluso medianas, suele haber un único directorio público. Las grandes pueden tener, si es necesario, varios.
Funci n serve staticó
-La función serve-static devuelve una función de middleware para servir un determinado directorio público. Tiene la siguiente signatura:
function serve-static(root, options) : function
Parámetro Tipo de datos Descripción root string Directorio público.
options object Opciones de servicio:
• acceptRanges (boolean). ¿Permitir el envío de recursos parciales? Valor predeterminado: true.
• cacheControl (boolean). ¿Activar la cabecera de respuesta Cache-Control? Valor predeterminado: true.
• etag (boolean). ¿Generar la cabecera ETag? Valor predeterminado: true.
• extensions (string[]). Extensiones a añadir a los recursos solicitados para encontrar el archivo a servir. Ejemplo: ["html", "htm"].
Valor predeterminado: ninguno.
• index (boolean o string). ¿Enviar el archivo index.html cuando se solicite un directorio? true, sí; false, no; o bien el archivo a remitir.
Valor predeterminado: index.html.
• lastModified (boolean). ¿Enviar la cabecera Last-Modified? Valor predeterminado: true.
• maxAge (number). Número de milisegundos que puede cachear el cliente el archivo.
Valor predeterminado: 0.
• redirect (boolean). ¿Redirigir a / cuando el recurso solicitado sea un directorio? Valor predeterminado: true.
• setHeaders (function). Función que debe invocar la función de middleware para fijar las cabeceras de la respuesta: fn(res, path, stat). res representa el objeto respuesta; path, la ruta del archivo a remitir; y stat, el objeto stat del archivo a remitir.
Registro de la funci n de middlewareó
Recordemos que la función serve-static devuelve una función de middleware con la que servir un determinado directorio público. Es por tanto la función devuelta lo que hay que registrar en la aplicación mediante el método use(). Si tenemos varios directorios públicos, invocaremos la función una vez para cada directorio, registrando las distintas funciones devueltas en la aplicación según su orden de prioridad.
Por convenio, el paquete serve-static que representa la función homónima se importa como serveStatic. He aquí un ejemplo ilustrativo:
//imports
import express from "express";
import serveStatic from "serve-static"; …
//registro
const app = express();
app.use(serveStatic(path.join(__dirname, "public")), {index: ["html"]}); …
La función de middleware debe ser una de las primeras funciones del ciclo de procesamiento. Si el recurso solicitado se encuentra en su directorio público, rellena la respuesta HTTP a remitir al cliente y cancela el resto del flujo de procesamiento, esto es, la pila de middleware. Pero si no lo encuentra, no cancela el flujo, ni propaga error. Simplemente, no hace nada y deja que las funciones siguientes rellenen la respuesta. De ahí que se
recomiende que se encuentre al comienzo del flujo de procesamiento. Cach HTTPé
En Internet, una de las cosas que más valoran los usuarios es la rapidez con que el navegador visualiza las páginas webs. Esto depende de muchas cosas. Una de ellas, la velocidad con que el navegador recibe los recursos de los servidores webs. Para ello, el protocolo HTTP permite que los clientes, los proxies y las aplicaciones dispongan de cachés.
Una caché (cache) no es más que un almacén o contenedor en el que se guarda datos accedidos con mucha frecuencia o recientemente. Su objetivo es mejorar su velocidad de acceso. Por ejemplo, un proxy puede almacenar en su caché los recursos más frecuentemente accedidos y remitirlos directamente sin tener que enviar las solicitudes clientes a las aplicaciones servidoras. Por otra parte, los navegadores disponen de una pequeña caché donde almacenar las últimas páginas accedidas por el usuario. De tal manera que pueden utilizar la copia local, en vez de solicitársela al servidor y esperar a que éste se la envíe de nuevo.
Por lo general, es más rápido el acceso a datos cacheados que la generación del recurso o bien la solicitud y envío de una copia. Esto es muy útil cuando se genera contenido dinámico donde una parte procede de una base de datos. También lo es con contenido estático, porque si el recurso se encuentra en la caché local, no hay que volver a solicitarlo y se puede servir directamente.
El principal objetivo de la caché es reducir la latencia (latency), el tiempo que transcurre desde que el usuario solicita el recurso y lo obtiene. En el 99% de los casos, suministrar un recurso directamente de una caché presenta una latencia menor que solicitárselo a su correspondiente servidor o aplicación web y esperar su recepción.
La operación mediante la cual se introduce un dato en la caché se conoce formalmente como cacheo (caching). Ubicaci n de la cachó é
Cada vez que se solicita y sirve un recurso web, intervienen varios componentes o dispositivos webs, cada uno de los cuales puede disponer de su propia caché con objeto de responder más rápidamente.
La caché ubicada en el cliente se conoce como caché cliente (client cache) o caché local (local cache). La mantiene, generalmente, el navegador. En ella, guarda las últimas páginas accedidas por el usuario y la usa para obtener una copia cuando el usuario vuelve a pedir alguna de ellas.
Por otra parte, tenemos cachés en los dispositivos intermedios, generalmente, proxies. Un proxy web (web proxy) es una aplicación que intermedia entre los clientes y los servidores webs. Estos dispositivos suele tener la capacidad de mantener copias de los recursos que pasan por sus manos en una caché proxy (proxy cache). Cada vez que un proxy recibe una petición cliente, comprueba primero si dispone de una copia cacheada del recurso solicitado. Si así es, la sirve directamente sin remitírsela al servidor web. Si no lo es, se la remite al servidor web y espera su respuesta. Entonces, la cachea y se la envía al cliente. Así la próxima vez dispondrá de una copia del recurso y podrá servirla directamente.
Es muy importante tener claro que los proxies no siempre cachean lo que pasa por ellos. Pero su uso suele ser muy útil cuando se sabe que el contenido no cambiará y, por lo tanto, se puede almacenar en una caché proxy. Finalmente, tenemos la caché servidora (server cache), en nuestro caso, una específica de la aplicación Express. Es importante tener claro que serve-static no dispone de caché, pero sí puede notificar a las cachés proxies y clientes si el recurso remitido no cambiará en un determinado período de tiempo y, por lo tanto, pueden
cachearlo sin miedo a cambios inesperados. Expiraci nó
La expiración (expiration) es el intervalo de tiempo que se puede mantener un recurso cacheado. Transcurrido éste, se considera que la copia ya no es válida. El dispositivo tendrá que volver a solicitar el recurso al servidor web si el usuario vuelve a accederlo.
¿Quién fija este período de tiempo? Como no puede ser de otra manera, quien publica el recurso, esto es, el servidor web o, en nuestro caso, la aplicación Express.
Cabecera Cache Control
-Mediante la cabecera HTTPCache-Control nuestra aplicación Express puede notificar si se puede cachear el recurso y durante cuánto tiempo puede hacerse.
serve-cache proporciona dos opciones de configuración: cacheControl y maxAge. Mediante cacheControl, se indica si serve-static debe añadir esta cabecera cuando sirva un recurso estático. Mientras que con maxAge, el período de expiración.
Cabecera Last Modified
-Un servidor web puede comunicar al cliente la fecha de la última modificación de un recurso mediante la cabecera HTTPLast-Modified:
Last-Modified: Fecha-HTTP
Cuando la opción lastModified de serve-static se encuentra activa, el middleware añadirá a la respuesta esta cabecera, usando como valor la fecha de última modificación del archivo remitido.
HTTP presenta cabeceras adicionales mediante las cuales un cliente puede hacer solicitudes como: sírveme el recurso si se ha modificado posteriormente a la fecha que te indico. De esta manera, pregunta si puede utilizar una copia cacheada cuyo período de expiración ha vencido, siempre y cuando el recurso no haya sufrido cambios. Cabecera Accept Ranges
-HTTP permite que los clientes soliciten el contenido completo de un recurso o sólo parte de él. Los servidores webs deben poder servir como mínimo los recursos al completo. Si proporcionan la funcionalidad de publicación parcial, pueden comunicárselo a los clientes mediante la cabecera Accept-Ranges:
Accept-Ranges: bytes|none
Mediante el valor bytes, el servidor le comunica a los clientes que puede servir contenido parcial de un recurso. Mediante none, que no lo hace.
serve-static puede servir total o parcialmente un archivo. Para configurar la publicación parcial, debe añadir la cabecera Accept-Ranges a sus respuestas. Mediante la opción acceptRanges fijamos si debe hacerlo. Si se fija a false, no la añadirá y sólo servirá recursos al completo.
Middleware serve favicon
-Un favicono (favicon) es una pequeña imagen asociada a un sitio web que muestran principalmente los navegadores en la pestaña de la página. Generalmente, es de 16 x 16 píxels. He aquí unos ejemplos:
Se trata del archivo favicon.*, generalmente, favicon.ico o favicon.png. Este archivo se indica en la página web mediante un elemento <link> como el siguiente:
<link rel="icon" type="image/png" href="/images/favicon.png">
Generalmente, los navegadores solicitan automáticamente el archivo favicon.ico para cada recurso web que solicitan. Cuando reciben una página web, si ésta indica el elemento <link> solicitará el favicono ahí indicado, en vez de favicon.ico.
Aunque el favicono es estático y, por lo tanto, se puede servir mediante el middleware serve-static, por lo general se prefiere serve-favicon. Principalmente, porque este middleware cachea el favicono en la memoria principal, por lo que servirlo será más rápido que si hay que recurrir a disco constantemente.
Funci n serve faviconó
-La función serve-favicon devuelve una función de middleware para servir el favicono de la aplicación. Recordemos generalmente el archivo favicon.ico o favicon.png. Su signatura es como sigue:
function serve-favicon(path) : function
function serve-favicon(path, options) : function
Parámetro Tipo de datos Descripción
path string Ruta del archivo favicono.
options object Opciones de servicio:
• maxAge (number). Período de expiración en milisegundos. Valor predeterminado: un año.
La opción maxAge tiene como objeto indicar el período de expiración a adjuntar en la cabecera Cache-Control de la respuesta, tal como vimos anteriormente. Como el favicono es un archivo que no suele cambiar con frecuencia, se recomienda que los dispositivos lo almacenen en su caché para evitar que la aplicación web tenga que responder contínuamente a las peticiones de los navegadores sobre el favicono.
Registro de la funci n de middlewareó
Al igual que serve-static, serve-favicon no es una función de middleware propiamente dicha. Es una función que devuelve la función de middleware a registrar en la aplicación. Se recomienda hacerlo al comienzo del flujo de procesamiento. Antes que serve-static.
Por convenio, el paquete serve-favicon se importa como serveFavicon o simplemente favicon. He aquí un ejemplo ilustrativo:
//imports
import express from "express";
import serveFavicon from "serve-favicon"; …
//registro
const app = express();
app.use(serveFavicon(path.join(__dirname, "public/images/favicon.png")), {maxAge: 43200000}); app.use(serveStatic(path.join(__dirname, "public")));
…
Contenido estático
(PRáctica)
Tiempo estimado: 15min
El objeto de esta práctica es asentar y consolidar los conceptos presentados en la parte teórica de la lección. Al finalizarla, el estudiante:
• Habrá servido contenido estático mediante el middleware serve-static. • Habrá configurado varios directorios públicos mediante serve-static.
• Habrá configurado el middleware serve-favicon para servir el favicono de la aplicación. Objetivos
En esta práctica, vamos a crear una aplicación Express que sirva contenido estático mediante el middleware serve-static. En un primer intento, usaremos un único directorio público; en el segundo, dos.
Creaci n del proyectoó
Para comenzar, vamos a crear el proyecto de la aplicación: 1. Abrir una consola.
2. Crear el directorio de la práctica. 3. Ir al directorio de la práctica.
4. Crear el archivo package.json con el siguiente contenido:
{
"name": "express-app", "version": "0.1.0", "private": true, "scripts": {
"start": "node ./app.js" },
"dependencies": { "express": "*" }
}
5. Crear el archivo app.js con el siguiente contenido:
"use strict"; //imports
const express = require("express"); //app
const app = express();
const index = "<!doctype html>\n" + "<html>\n" +
"<head><title>Express app</title></head>\n" + "<body><p>¡Hola Mundo!</p></body>\n" +
"</html>"; //config app
app.get("/", function(req, res) { res.send(index); }); //listen
app.listen(8080, function() {
console.log("Listening..."); });
6. Instalar dependencias:
> npm install
7. Iniciar la aplicación Express:
> npm start
8. Abrir el navegador. 9. Ir a http://localhost:8080:
Configuraci n de un directorio p blicoó ú
En este punto, ya tenemos la aplicación inicial. Ahora, vamos a usar serve-static para servir contenido estático. 1. Editar el archivo package.json.
2. Añadir serve-static como dependencia:
"dependencies": { "express": "*", "serve-static": "*" }
3. Guardar cambios.
4. Crear el directorio público, public, en el directorio del proyecto. 5. Crear el archivo one.txt en el directorio public:
ONE!
6. Crear el archivo two.html en el directorio público:
<!doctype html> <html> <head> <title>Contenido estático</title> </head> <body> <p>
Esto es un ejemplo de contenido estático. </p>
</body> </html>
7. Editar el archivo app.js.
8. Importar el middleware serve-static y el módulo path:
//imports
const express = require("express");
const serveStatic = require("serve-static"); const path = require("path");
9. Configurar serve-static para que sirva el contenido estático del directorio public:
//config app
app.use(serveStatic(path.join(__dirname, "public"), {index: "index.html"})); app.get("/", function(req, res) { res.send(index); });
Recordemos que la variable global __dirname contiene el directorio donde reside el script de Node en ejecución.
10. Guardar cambios. 11. Ir a la consola.
12. Detener la aplicación web.
13. Instalar nuevas dependencias configuradas en el archivo package.json:
> npm update
14. Arrancar de nuevo la aplicación web:
> npm start
15. Ir al navegador.
16. Ir a http://localhost:8080.
Debería mostrar el contenido ¡Hola Mundo! 17. Ir a http://localhost:8080/one.txt.
Debería mostrar el contenido ONE! 18. Ir a http://localhost:8080/two.html.
Debería mostrar Esto es un ejemplo de contenido estático. 19. Ir a http://localhost:8080/three.jpg.
Express devolverá una respuesta 404 indicando que no encuentra el recurso. Es importante recordar que esta respuesta no la remite el middleware serve-static, sino el propio Express. Cuando serve-static no encuentra el recurso solicitado por el usuario, no remite una respuesta de error, sino que pasa al siguiente componente de middleware del flujo de procesamiento. Sólo si la encuentra, cancela el resto del flujo. Configuraci n de varios directorios p blicosó ú
Ahora, vamos a configurar una segunda carpeta pública mediante otra función serve-static: 1. Crear la carpeta public2 en el directorio del proyecto.
2. Crear el archivo three.txt en el directorio public2:
THREE!
3. Editar el archivo app.js.
4. Configurar la carpeta public2 como pública:
app.use(serveStatic(path.join(__dirname, "public"), {index: "index.html"})); app.use(serveStatic(path.join(__dirname, "public2"), {index: "index.html"})); app.get("/", function(req, res) { res.send(index); });
5. Guardar cambios. 6. Ir a la consola. 7. Reiniciar la aplicación. 8. Ir a http://localhost:8080. 9. Ir a http://localhost:8080/one.txt. 10. Ir a http://localhost:8080/two.html. 11. Ir a http://localhost:8080/three.txt.
Cuando el primer serve-static no encuentra el recurso en el directorio public, pasa la solicitud al siguiente componente de middleware del flujo de procesamiento. En nuestro caso, al serve-static que se encarga de servir el contenido del directorio public2. El cual es finalmente el que sirve el recurso three.txt.
Configuraci n del favicono de la aplicaci nó ó
En este punto, vamos a configurar el middleware serve-favicon para servir el favicono de la aplicación: 1. Editar el archivo package.json.
2. Añadir serve-favicon a las dependencias: