Introducción a node.js y express
templating
Si se desea enviar una página html estática o dinámica en función de los parámetros enviados en la solicitud la manera básica inicial de hacerlo sería algo parecido a:
const fs = require("fs"); const express = require("express"); const app = express(); app.get("/ayuda", (req, res) => { fs.stat("ayuda.html", (err, informacion) => { if (!err) { fs.readFile("ayuda.html", (err, datos) => { if (err) { res.send(500, "error en el acceso al archivo"); } else { res.setHeader("content-type", "text/html"); res.send(200, datos); } }) } else { res.send(404, "la página no existe"); } }); });
Por suerte, con express, el envío de página estáticas queda solucionado con el middelware static ya visto. Pero ¿que pasa con las páginas cuyo contenido depende de parámetros generalmente enviados en la solicitud por parte del cliente? Una primera aproximación consistiría en poner marcas dentro del archivo y una vez recuperado el contenido con el método readFile, sustituir las marcas por el contenido de la información dinámica. Por ejemplo, si en el archivo hemos puesto la marca @nombre@, con un simple replace podríamos cambiarlo por Andrés Gómez y luego enviarlo al cliente. La codificación para cada archivo y cada parámetro se antoja bastante ardua. Esta filosofía de trabajo es en la que se basa el templating o uso de plantillas. Existen varios middleware que nos ayudan a la gestión de contenido dinámico de una forma mucho más sencilla. Vamos a ver como se haría usando uno de estos middleware (todos son muy parecidos, variando prácticamente la sintaxis y las opciones entre ellos). Vamos a usar handlebars.
En handlebars las marcas se incluyen entre el código html encerrando el nombre de la marca entre dobles llaves:
<h1> Informe de ventas del año {{anio}}</h1>
Cuando handlebars interprete el código, sustituirá la marca {{anio}} por el valor que se le pase con nombre anio. Si el parámetro contiene código html encerraremos la marca entre tres llaves {{{marca}}} para que se interprete correctamente el código html.
La mayoría de las páginas de nuestra aplicación web, van a tener una distribución y componentes parecidos. En el o los archivos de layout es donde vamos a poner esta parte común que se mezclarán con las vistas (views), se interpretarán las marcas sustituyendo por su contenido y se enviarán al cliente. Este proceso se denomina renderización (render). El siguiente es un ejemplo de archivo de layout:
main.handlebars
<!doctype html> <html> <head> <title>Biblioteca Provincial</title> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> <link rel="stylesheet" href="/css/main.css"> </head> <body> {{{body}}} </body> </html>
Ahora si tenemos un view llamado home.handlebars con el siguiente contenido:
<h1>Director {{nombre}}</h1> <p>lore ipsum .....</p>
E invocamos el método render del objeto response:
app.get("/", (req, res) => { res.render("home", { nombre: "Alfredo Sánchez" }); })
Se le enviará al cliente el siguiente código html:
<!doctype html> <html> <head> <title>Biblioteca Provincial</title> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> <link rel="stylesheet" href="/css/main.css"> </head> <body> <h1>Director Alfredo Sánchez</h1> <p>lore ipsum .....</p> </body> </html>
Los archivos que contienen las vistas de handlebars son buscadas, por defecto, en la carpeta views debajo de la carpeta de la aplicación. Los archivos de layout bajo la carpeta layouts que estará bajo views. Los archivos de handlebars por defecto deben tener la extensión .handlebars
Nuestro archivo index.js quedaría
const express = require("express");
const app = express();
const hb = require("express-handlebars");
// activar handlebars
app.set("view engine", "handlebars");
app.engine("handlebars", hb({
defaultLayout: "main",
}));
app.get("/", (req, res) => {
res.render("home", { nombre: "Alfredo Sánchez" });
})
//si llega aquí, es una página no encontrada
app.use(function(req, res, next) {
res.type("text/plain");
res.status(404);
res.send("no econtrado");
});
app.listen(8080, () => {
console.log('servidor funcionando en puerto 8080');
});
Nótese como es activado handlebars primero indicando a express que el motor de vistas es handlebars:
app.set("view engine", "handlebars");
Y luego configurando el motor:
app.engine("handlebars", hb({ defaultLayout: "main", }));
La opción de configuración del motor defaultLayout indica cual es al archivo de layout que por defecto se usara para renderizar las vistas. Cuando se renderiza una vista se puede elegir un layout distinto de la siguiente manera:
res.render(“datos”, {layout:”otro”});
Existen otras opciones además de defaultLayout de configuración del motor:
- extname Permite cambiar la extensión de los archivos de handlebars. Por ejemplo: extname: “.hbs” indica que los archivos de handlebars tienen extensión .hbs en lugar de .handelbars
- partialsDir Indica la carpeta en la que se encontrarán los parciales (más adelante se hablará de ellos) por defecto carpeta partials bajo views.
- layoutsDir Indica la carpeta en la que se encontrarán los layouts. Por defecto carpeta layouts bajo views
Para activar el caching al servir vistas debemos ejecutar:
app.enable('view cache');
Ya hemos visto como se referencian los parámetros que se pasan a la vista para su sustitución:
El nombre del parámetro se encierra entre pares de llaves {{dato}} y si el contenido del parámetro tiene código html y deseamos que el navegador lo interprete como tal, lo encerramos entre triples llaves {{{dato}}}.
Si el parámetro pasado es a su vez un objeto con diferentes propiedades, incluso con arrays, podemos referir estas propiedades haciendo uso de la notación punto igual a la utilizada en javascript. Por ejemplo, si se solicita la renderización de la vista cliente.handlebars de la siguiente forma:
res.render(“cliente”,{ perfil:{nombre:”Andrés”,apellidos:”García Pérez”}});
Dentro del archivo de vista, la referencia a {{perfil.nombre}} será sustituida por Andrés y {{perfil.apellidos}} por García Pérez.
handlebars además del uso de estas expresiones simples permite el uso de otras expresiones más elaboradas.
Se pueden codificar plantillas parciales o trozos de plantillas (partials) en archivos separados que luego son incrustados dentro de plantillas más completas. Por ejemplo, podríamos tener un partial con la descripción de la cabecera de todas las páginas del sitio en un archivo header.handlebars e incorporarlo a nuestro layout por defecto main.handlebars de la siguiente forma:
index.js
const express = require("express"); const app = express(); const hb = require("express-handlebars"); // activar handlebars app.set("view engine", "handlebars"); app.engine("handlebars", hb({ defaultLayout: "main", })); app.get("/", (req, res) => { res.render("home", { nombre: "Alfredo Sánchez" }); }) //si llega aquí, es una página no encontrada app.use(function(req, res, next) { res.type("text/plain"); res.status(404); res.send("no econtrado"); }); app.listen(8080, () => { console.log('servidor funcionando en puerto 8080'); });
home.handlebars
<p>lore ipsum .....</p>
main.handlebars
<!doctype html> <html> <head> <title>Biblioteca Provincial</title> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> <link rel="stylesheet" href="/css/main.css"> </head> <body> {{>header}} {{{body}}} </body> </html>
header.handlebars
<header> <h1>Biblioteca provincial</h1> <h2>Director {{nombre}}</h2> </header>
Por defecto los partials se buscan en la carpeta views/partials. La notación para incluir un partial es {{>partial}}.
Tenemos también la posibilidad de definir funciones que devuelven el valor a incrustar, que puede ser código html. en handlebars se denominan helpers. Se registran en el engine:
index.js
const express = require("express");
const app = express();
const hb = require("express-handlebars");
// activar handlebars
app.set("view engine", "handlebars");
app.engine("handlebars", hb({
defaultLayout: "main",
helpers: {
"monedas": (precio) => {
const monedas = ["$", "€", "£", "¥"];
const cambio = [1, 0.86, 0.73, 113.59];
let strHTML = "<UL>";
cambio.map((valor, indice) => {
strHTML += "<li>" + Intl.NumberFormat("de-DE",
{ minimumFractionDigits: 2,
maximumFractionDigits: 2 }).format((precio * valor))
+ " " + monedas[indice] + "</li>";
})
strHTML += "</UL>";
return strHTML;
}
}
}));
app.get("/", (req, res) => {
res.render("home", { nombre: "Alfredo Sánchez", precio: 100.10 });
})
//si llega aquí, es una página no encontrada
app.use(function(req, res, next) {
res.type("text/plain");
res.status(404);
res.send("no econtrado");
});
app.listen(8080, () => {
console.log('servidor funcionando en puerto 8080');
});
Y se usan en las plantillas, ya sean view, layout o partial:
home.handlebars
<p>lore ipsum .....</p> <p> precio {{{monedas precio }}} </p>
Se invocan indicando el nombre del helper y a continuación la lista de argumentos separados por espacio. Se ha encerrado la llamada entre triples llaves porque el helper devuelve código html.
handlebars dispone de varios helper predefinidos (built-in).
- #if permite mostrar o no un bloque en función de si el argumento pasado devuelve algo distinto a false, undefined, null, "", 0, o [].
<div>
{{#if autor}}
<h1>{{nombre}} {{apellidos}}</h1>
{{else}}
<h1>autor desconocido</h1>
{{/if}}
</div>
La entrada else es opcional.
<div>
{{#unless autor}}
<h1>autor desconocido</h1>
{{else}}
<h1>{{nombre}} {{apellidos}}</h1>
{{/if}}
</div>
Supongamos que tenemos una vista empleado.handlebars que muestra tanto el nombre y apellidos de un trabajador y los sueldos del último año. Para mostrar los datos de un trabajador en particular podríamos renderizar la vista:
res.render("empleado", { layout: null, datos: { nombre: "Alfredo", apellidos: "García Castellón", sueldos: [{ mes: "enero", sueldo: 1200.34 }, { mes: "febrero", sueldo: 1200.34 }, { mes: "marzo", sueldo: 1256.89 }, { mes: "abril", sueldo: 1256.89 }, { mes: "mayo", sueldo: 1299.10 }, { mes: "junio", sueldo: 1299.10 }, { mes: "julio", sueldo: 2428.55 }, { mes: "agosto", sueldo: 1345.67 }, { mes: "septiembre", sueldo: 1345.67 }, { mes: "octubre", sueldo: 1345.67 }, { mes: "noviembre", sueldo: 1345.67 }, { mes: "diciembre", sueldo: 2563.34 } ] } });
Cuando se desea que no se aplique ningún layout a la vista lo indicamos con layout:null. La vista quedaría codificada:
<!doctype html> <html lang="es"> <head> <title>Empresa S.A.</title> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> </head> <body> <h1>Ficha del empleado</h1> <h2>{{datos.apellidos}},{{datos.nombre}}</h2> <div>salarios</div> <div> {{#each datos.sueldos}} <div>{{mes}} - {{sueldo}}</div> {{/each}} </div> </body> </html>
El helper #each lleva, al igual que #if, un terminador /each para delimitar el bucle. Una variación respecto a #if 1 es que #each cambia lo que se denomina el contexto. Dentro del ámbito del bloque #each las referencia a datos se presuponen al ámbito de la lista que indica #each. En el ejemplo se ve que no ha sido necesario indicar que el campo mes y sueldo pertenecen a la lista sueldos. Si dentro del bucle necesitáramos referirnos a un datos del contexto anterior podremos referirnos a el mediante ../ en referencia al contexto padre. Por ejemplo si dentro del bloque queremos acceder al nombre del empleado pondríamos ../datos.nombre
Dentro del ámbito de aplicación de #each podemos usar this para referirnos al contexto actual, a @index para referirnos al índice actual de la lista si fuera un array, a @first para referirnos a la primera casilla del array y @last para referirnos a la última. También disponemos de @key para referirnos al nombre de la propiedad si estamos iterando sobre la lista de propiedades de un objeto.
Vamos a ver una variación del anterior ejemplo que incluya estos conceptos:
app.get("/empleado", (req, res) => { res.render("empleado", { layout: null, datos: { nombre: "Alfredo", apellidos: "García Castellón", sueldos: [1200.34, 1200.34, 1256.89, 1256.89, 1299.10, 1299.10, 2428.55, 1345.67, 1345.67, 1345.67, 1345.67, 2563.34 ] } }); });
Vamos a añadir un helper a helpers del engine, para recuperar el nombre del mes en función del número de mes, entre 0 y 11.
"nombreMes": (mes) => { const aNombresMes = ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"]; return aNombresMes[mes]; }
La vista quedaría:
<!doctype html>
<html lang="es">
<head>
<title>Empresa S.A.</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
</head>
<body>
<h1>Ficha del empleado</h1>
<h2>{{datos.apellidos}},{{datos.nombre}}</h2>
<div>salarios</div>
<div>
{{#each datos.sueldos}}
<div>{{nombreMes @index}} - {{this}}</div>
{{/each}}
</div>
</body>
</html>
Se usó this para referirnos al contenido de la casilla actual ya que no contiene un objeto sino un simple valor literal. Véase como se recuperó el nombre del mes haciendo referencia @index
<!doctype html> <html lang="es"> <head> <title>Empresa S.A.</title> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> </head> <body> {{#with datos}} <h1>Ficha del empleado</h1> <h2>{{apellidos}}, {{nombre}}</h2> <div>salarios</div> <div> {{#each sueldos}} <div>{{nombreMes @index}} - {{this}}</div> {{/each}} </div> {{/with}} </body> </html>
(1) #if en realidad si cambia el contexto, lo que hace es cambiar el contexto a una copia del contexto padre, por eso parece que no cambia. Téngase esto en cuenta si queremos acceder al contexto anterior al #if, habrá que indicar ../.. y no solo ../