Guía de manillares: motor de plantillas para Node/JavaScript

Usando Handlebars with Node, podemos crear páginas web dinámicas que se rendericen en el lado del servidor. Usando condicionales, bucles, parciales y funciones auxiliares personalizadas, con Handlebars, nuestras páginas web se convierten en algo más que HTML estático.

Introducción

En este artículo, veremos cómo usar el motor de plantillas Handlebars con Node.js y Express. Cubriremos qué son los motores de plantillas y cómo se pueden usar los Handlebars para crear aplicaciones web Server Side Rendered (SSR).

También discutiremos cómo configurar Handlebars con el marco Express.js y cómo usar ayudantes integrados para crear páginas dinámicas. Finalmente, veremos cómo desarrollar un ayudante personalizado cuando sea necesario.

¿Qué es un motor de plantillas?

En los años 90, cuando Internet se introdujo en el mundo, se usaba principalmente con fines científicos, como la publicación de trabajos de investigación y como canal de comunicación entre universidades y científicos. La mayoría de las páginas web en ese entonces eran estáticas. Una página web estática es la misma para todos los usuarios y no cambia por usuario. Si se va a cambiar algo en una página, se habrá hecho manualmente.

En el mundo moderno, las cosas son mucho más interactivas y adaptadas a cada usuario. Hoy en día, casi todo el mundo tiene acceso a Internet. La mayoría de las aplicaciones web actuales son dinámicas. Por ejemplo, en Facebook, usted y yo veremos fuentes de noticias muy diferentes cuando inicie sesión. Para cada persona, la página seguirá la misma plantilla (es decir, publicaciones secuenciales con nombres de usuario arriba), pero el contenido será diferente.

Este es el trabajo de un motor de plantillas: se define la plantilla para la fuente de noticias y luego, según el usuario actual y la consulta a la base de datos, la plantilla se completa con el contenido recibido.

Podemos usar motores de plantillas tanto en el backend como en el front-end. Si usamos un motor de plantilla en el backend para generar el HTML, lo llamamos Representación del lado del servidor (SSR).

Manillares

Handlebars es popular tanto para plantillas de back-end como de front-end. Por ejemplo, el popular marco front-end Ascua utiliza Handlebars como motor de plantillas.

Handlebars es una extensión del lenguaje de plantillas Bigote, que se enfoca principalmente en la simplicidad y las plantillas mínimas.

Uso de Handlebars con Node.js

Para comenzar, cree una carpeta vacía, abra el símbolo del sistema dentro de esa carpeta y luego ejecute npm init -y para crear un proyecto Node.js vacío con la configuración predeterminada.

Antes de comenzar, debemos instalar las bibliotecas necesarias de Node.js. Puede instalar los módulos Rápido y express-manillar ejecutando:

1
$ npm install --save express express-handlebars

Nota: cuando utilice Handlebars en el lado del servidor, probablemente usará un módulo auxiliar como express-handlebars que integra Handlebars con su marco web. En este artículo, nos centraremos principalmente en la sintaxis de las plantillas, por lo que estamos usando express-handlebars, pero en caso de que estés manejando la compilación y renderizando la plantilla tú mismo, También querrá consultar la referencia de la API de compilación.

Luego, vamos a recrear la estructura de directorios predeterminada de Handlebars. La carpeta views contiene todas las plantillas de manubrios:

1
2
3
4
5
6
.
├── app.js
└── views
    ├── home.hbs
    └── layouts
        └── main.hbs

La carpeta layouts dentro de la carpeta views contendrá los diseños o los envoltorios de plantilla. Esos diseños contendrán la estructura HTML, las hojas de estilo y los scripts que se comparten entre las plantillas.

El archivo main.hbs es el diseño principal. El archivo home.hbs es una plantilla de manillar de ejemplo sobre la que vamos a construir.

Agregaremos más plantillas y carpetas a medida que avancemos.

En nuestro ejemplo, usaremos un script para mantener esto simple. Importemos las bibliotecas requeridas en nuestro archivo app.js:

1
2
const express = require('express');
const exphbs = require('express-handlebars');

Luego, creemos una aplicación Express:

1
const app = express();

Ahora, podemos configurar express-handlebars como nuestro motor de visualización:

1
2
3
4
5
6
app.engine('hbs', exphbs({
    defaultLayout: 'main',
    extname: '.hbs'
}));

app.set('view engine', 'hbs');

De forma predeterminada, la extensión de las plantillas de manillares es .handlebars. Pero en la configuración aquí, lo hemos cambiado a .hbs a través del indicador extname porque es más corto.

Incluyamos los scripts y estilos Oreja en el diseño main.hbs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<html lang="en">
<head>
    <!-- <meta> tags> -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
    <title>Book Face</title>
</head>

<body>
    <div class="container">
        {{{body}}}
    </div>

    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[correo electrónico protegido]/dist/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
</body>
</html>

Y ahora, cambiemos nuestro home.hbs para incluir un mensaje:

1
<h1>Hello World from Handlebars</h1>

Para poder acceder a esta página, debemos configurar un controlador de solicitudes. Configurémoslo en la ruta raíz:

1
2
3
app.get('/', (req, res) => {
    res.render('home');
});

Finalmente, solo necesitamos comenzar a escuchar en un puerto para solicitudes:

1
2
3
app.listen(3000, () => {
    console.log('The web server has started on port 3000');
});

Podemos ejecutar la aplicación con node app.js en la consola, aunque también podemos optar por usar una herramienta como nodemon. Con nodemon, no necesitamos reiniciar el servidor cada vez que hacemos un cambio; cuando cambiamos el código, nodemon actualizará el servidor.

Vamos a instalarlo:

1
$ npm i -g nodemon

Y ejecutar la aplicación con nodemon se realiza a través de:

1
$ nodemon app.js

Visitemos nuestra aplicación a través del navegador:

handlebars configured

Con todo en su lugar, exploremos algunas funciones del manubrio.

Funciones del idioma del manillar

Con el fin de mostrar algunas de las características de los manubrios, crearemos un feed de redes sociales. El feed extraerá datos de una matriz simple, simulando una base de datos.

El feed contendrá publicaciones con imágenes y comentarios. Si no hay comentarios en una imagen, aparecerá el mensaje "Sé el primero en comentar esta publicación".

Actualicemos nuestro home.hbs para comenzar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<nav class="navbar navbar-dark bg-dark">
    <a class="navbar-brand" href="#">Book Face</a>
</nav>

<div class="posts">
    <div class="row justify-content-center">
        <div class="col-lg-7" style="margin-top: 50px;">
            <div class="card">

                <img src="https://picsum.photos/500/500"
                    class="card-img-top" alt="...">
                <div class="card-body">
                    <h5 class="card-title">Posted by Janith Kasun</h5>

                    <ul class="list-group">
                        <li class="list-group-item">This is supposed to be a comment</li>
                        <li class="list-group-item">This is supposed to be a comment</li>
                    </ul>
                </div>
            </div>
        </div>
    </div>
</div>

Como puede ver en esta plantilla de manubrios, hemos agregado una “barra de navegación” y una “tarjeta” con algunos valores de marcador de posición codificados.

Nuestra página ahora se ve así:

handlebars template

Pasar parámetros a plantillas {#pasar parámetros a plantillas}

Ahora, eliminemos estos valores codificados de la página misma y pasémoslos del script a la página. Estos serán reemplazados más tarde con valores de comentario en la matriz:

1
2
3
4
5
6
7
8
9
app.get('/', function (req, res) {
    res.render('home', {
        post: {
            author: 'Janith Kasun',
            image: 'https://picsum.photos/500/500',
            comments: []
        }
    });
});

La publicación contiene campos como autor, imagen y comentarios. Podemos hacer referencia a la publicación en nuestra plantilla de manubrios {{publicación}}:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<div class="posts">
    <div class="row justify-content-center">
        <div class="col-lg-7" style="margin-top: 50px;">
            <div class="card">

                <img src="{{post.image}}"
                    class="card-img-top" alt="...">
                <div class="card-body">
                    <h5 class="card-title">Posted by {{post.author}}</h5>

                    <ul class="list-group">
                        <li class="list-group-item">This is suppose to be a comment</li>
                        <li class="list-group-item">This is suppose to be a comment</li>
                    </ul>
                </div>
            </div>
        </div>
    </div>
</div>

Al hacer referencia a estos valores con el controlador que representa la página, se insertan en el lado del servidor y el usuario recibe HTML aparentemente estático con estos valores ya presentes.

Uso de condiciones {#uso de condiciones}

Como tenemos lógica condicional, es decir, mostramos los comentarios si están presentes y un mensaje si no lo están, veamos cómo podemos usar los condicionales en las plantillas de Handlebars:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div class="posts">
    <div class="row justify-content-center">
        <div class="col-lg-7" style="margin-top: 50px;">
            <div class="card">

                <img src="{{post.image}}" class="card-img-top" alt="...">
                <div class="card-body">
                    <h5 class="card-title">Posted by {{post.author}}</h5>

                    {{#if post.comments}}
                    <ul class="list-group">
                        <!-- Display comment logic -->

                    </ul>
                    {{else}}
                    <ul class="list-group">
                        <li class="list-group-item">Be first to comment on this post!</li>
                    </ul>
                    {{/if}}
                </div>
            </div>
        </div>
    </div>
</div>

Ahora, solo deberías ver la sección "Sé el primero en comentar esta publicación" representada en tu página, ya que la matriz de comentarios está vacía:

handlebars template with navbar and card

El #if es un asistente incorporado en Handlebars. Si la instrucción if devuelve true, se representará el bloque dentro del bloque #if. Si se devuelven false, undefined, null, "", 0 o [], el bloque no se renderizará.

Nuestra matriz está vacía ([]), por lo que el bloque no se procesa.

#if solo acepta una sola condición y no puede usar la sintaxis de comparación de JavaScript (===). Si necesita usar varias condiciones o sintaxis adicional, puede crear una variable en el código y pasarla a la plantilla. Además, puede definir su propio ayudante, lo cual haremos en la última sección.

Uso de bucles

Dado que una publicación puede contener múltiples comentarios, necesitaremos un bucle para revisarlos todos y representarlos. Primero llenemos nuestra matriz con algunos comentarios:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
app.get('/', function (req, res) {
    res.render('home', {
        post: {
            author: 'Janith Kasun',
            image: 'https://picsum.photos/500/500',
            comments: [
                'This is the first comment',
                'This is the second comment',
                'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec fermentum ligula. Sed vitae erat lectus.'
            ]
        }
    });
});

Y ahora, en nuestra plantilla, usaremos el ciclo #each para revisarlos todos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div class="posts">
    <div class="row justify-content-center">
        <div class="col-lg-7" style="margin-top: 50px;">
            <div class="card">

                <img src="{{post.image}}" class="card-img-top" alt="...">
                <div class="card-body">
                    <h5 class="card-title">Posted by {{post.author}}</h5>

                    {{#if post.comments}}
                    <ul class="list-group">
                        {{#each post.comments}}
                        <li class="list-group-item">{{this}}</li>
                        {{/each}}
                    </ul>
                    {{else}}
                    <ul class="list-group">
                        <li class="list-group-item">Be first to comment on this post</li>
                    </ul>
                    {{/if}}
                </div>
            </div>
        </div>
    </div>
</div>

Dentro del bucle #each, puedes usar this para hacer referencia al elemento que está en la iteración actual. En nuestro caso, se refiere a una cadena que luego se procesa:

each helper function in handlebars

Si tiene una matriz de objetos, también puede acceder a cualquier atributo de ese objeto. Por ejemplo, si hay una serie de personas, simplemente puede usar this.name para acceder al campo name.

Ahora, cambiemos los parámetros de nuestra plantilla para que contenga varias publicaciones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
app.get('/', function (req, res) {
    res.render('home', {
        posts: [
            {
                author: 'Janith Kasun',
                image: 'https://picsum.photos/500/500',
                comments: [
                    'This is the first comment',
                    'This is the second comment',
                    'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec fermentum ligula. Sed vitae erat lectus.'
                ]
            },
            {
                author: 'John Doe',
                image: 'https://picsum.photos/500/500?2',
                comments: [
                ]
            }
        ]
    });
});

Ahora también podemos poner un #each para iterar a través de las publicaciones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<div class="posts">
    <div class="row justify-content-center">
        {{#each posts}}
        <div class="col-lg-7" style="margin-top: 50px;">
            <div class="card">
                <img src="{{this.image}}" class="card-img-top" alt="...">
                <div class="card-body">
                    <h5 class="card-title">Posted by {{this.author}}</h5>

                    {{#if this.comments}}
                    <ul class="list-group">
                        {{#each this.comments}}
                        <li class="list-group-item">{{this}}</li>
                        {{/each}}
                    </ul>
                    {{else}}
                    <ul class="list-group">
                        <li class="list-group-item">Be first to comment on this post</li>
                    </ul>
                    {{/if}}
                </div>
            </div>
        </div>
        {{/each}}
    </div>
</div>

Uso parcial

Prácticamente todas las páginas web contienen diferentes secciones. En un nivel básico, estas son las secciones Encabezado, Cuerpo y Pie de página. Dado que el encabezado y el pie de página generalmente se comparten entre muchas páginas, tener esto en todas las páginas web pronto se volverá extremadamente molesto y simplemente redundante.

Afortunadamente, podemos usar Handlebars para dividir estas secciones en plantillas y simplemente incluir estas plantillas como "parciales" en las propias páginas.

En nuestro caso, dado que no tenemos un pie de página, hagamos un archivo header.hbs y posts en un directorio parcial:

1
2
3
4
5
6
7
8
9
.
├── app.js
└── views
    ├── home.hbs
    ├── layouts
    |  └── main.hbs
    └── paritials
       └── header.hbs
       └── posts.hbs

Luego, moveremos el código del encabezado al archivo header.hbs:

1
2
3
<nav class="navbar navbar-dark bg-dark">
    <a class="navbar-brand" href="#">Book Face</a>
</nav>

Y el código de alimentación en el archivo posts.hbs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<div class="posts">
    <div class="row justify-content-center">
        {{#each posts}}
        <div class="col-lg-7" style="margin-top: 50px;">
            <div class="card">

                <img src="{{this.image}}" class="card-img-top" alt="...">
                <div class="card-body">
                    <h5 class="card-title">Posted by {{this.author}}</h5>

                    {{#if this.comments}}
                    <ul class="list-group">
                        {{#each this.comments}}
                        <li class="list-group-item">{{this}}</li>
                        {{/each}}
                    </ul>
                    {{else}}
                    <ul class="list-group">
                        <li class="list-group-item">Be first to comment on this post</li>
                    </ul>
                    {{/if}}
                </div>
            </div>
        </div>
        {{/each}}
    </div>
</div>

Y ahora, podemos incluirlos en el archivo home.hbs:

1
2
3
{{>header}}

{{>posts posts=posts}}

El usuario no verá la diferencia, pero nuestro archivo home.hbs está mucho más limpio ahora. Esto se vuelve súper útil cuando tienes páginas web complejas.

Aquí, simplemente incluimos el archivo header.hbs y pasamos un parámetro posts al campo posts del archivo posts.hbs.

Lo que esto hace es pasar las publicaciones de nuestro controlador al parámetro posts en el archivo de página posts.hbs.

Crear un asistente personalizado {#construir un asistente personalizado}

Como puede ver en la página, tenemos un solo comentario que consume dos líneas. Vamos a crear un ayudante personalizado para resumir ese texto.

Para hacer eso, en la configuración de Handlebars, podemos definir nuestras funciones de ayuda. En nuestro caso, recortaremos los comentarios a 64 caracteres:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
app.engine('hbs', exphbs({
    defaultLayout: 'main',
    extname: '.hbs',
    helpers: {
        getShortComment(comment) {
            if (comment.length < 64) {
                return comment;
            }

            return comment.substring(0, 61) + '...';
        }
    }
}));

Ahora usemos este asistente en nuestra plantilla posts.hbs para resumir los comentarios:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<div class="posts">
    <div class="row justify-content-center">
        {{#each posts}}
        <div class="col-lg-7" style="margin-top: 50px;">
            <div class="card">

                <img src="{{this.image}}" class="card-img-top" alt="...">
                <div class="card-body">
                    <h5 class="card-title">Posted by {{this.author}}</h5>

                    {{#if this.comments}}
                    <ul class="list-group">
                        {{#each this.comments}}
                        <li class="list-group-item">{{getShortComment this}}</li>
                        {{/each}}
                    </ul>
                    {{else}}
                    <ul class="list-group">
                        <li class="list-group-item">Be first to comment on this post</li>
                    </ul>
                    {{/if}}
                </div>
            </div>
        </div>
        {{/each}}
    </div>
</div>

Seguramente, los comentarios ahora están recortados en nuestra página:

handlebars custom helper function

Conclusión

En este artículo, cubrimos los conceptos básicos de Handlebars, un motor de plantillas para Node.js y JavaScript front-end. Usando Handlebars, podemos crear páginas web dinámicas que se muestran en el lado del servidor o en el lado del cliente. Al utilizar las funciones de ayuda condicionales, bucles, parciales y personalizadas de Handlebars, nuestras páginas web se convierten en algo más que HTML estático.

El código también está disponible en GitHub, como de costumbre. También puedes encontrar más información sobre Handlebars en su página web oficial.