Tutorial Express - parte 5: Visualización de datos

Índice

Ahora estamos listos para agregar las páginas que muestran los libros del sitio web de LocalLibrary y otros datos. Las páginas incluirán una página de inicio que muestra cuántos registros tenemos de cada tipo de modelo y una lista y páginas de detalles para todos nuestros modelos. En el camino, obtendremos experiencia práctica en la obtención de registros de la base de datos y en el uso de plantillas.

Descripción general

En nuestros artículos tutoriales anteriores, definimos modelos Mongoose que podemos usar para interactuar con una base de datos y creamos algunos registros de biblioteca iniciales. Luego creamos todas las rutas necesarias para el sitio web de LocalLibrary, pero con funciones de “controlador ficticio” (estas son funciones de controlador de esqueleto que simplemente devuelven un mensaje “no implementado” cuando se accede a una página).

El siguiente paso es proporcionar implementaciones adecuadas para las páginas que muestran la información de nuestra biblioteca (veremos cómo implementar páginas con formularios para crear, actualizar o eliminar información en artículos posteriores). Esto incluye actualizar las funciones del controlador para obtener registros utilizando nuestros modelos y definir plantillas para mostrar esta información a los usuarios.

Comenzaremos brindando información general/temas básicos que explican cómo administrar operaciones asincrónas en funciones de controlador y cómo escribir plantillas usando Pug. Luego, proporcionaremos implementaciones para cada una de nuestras páginas principales de “solo lectura” con una breve explicación de cualquier característica especial o nueva que utilicen.

Al final de este artículo, debe tener una buena comprensión integral de cómo funcionan en la práctica las rutas, las funciones asincrónicas, las vistas y los modelos

Control de flujo asíncrono usando async

El código del controlador para algunas de nuestras páginas de LocalLibrary dependerá de los resultados de varias solicitudes asíncronas, que pueden ser necesarias para ejecutarse en un orden particular o en paralelo. Para administrar el control de flujo y mostrar páginas cuando tengamos toda la información requerida disponible, usaremos el popular módulo async de node.

Hay otras formas de administrar el comportamiento asíncrono y el control de flujo en JavaScript, incluidas funciones relativamente recientes del lenguaje JavaScript como Promises.

Async tiene muchos métodos útiles. Algunas de las funciones más importantes son:

  • async.parallel() para ejecutar cualquier operación que deba realizarse en paralelo.
  • async.series() para cuando necesitamos asegurarnos de que las operaciones asíncronas se realicen en serie.
  • async.waterfall() para operaciones que deben ejecutarse en serie, y cada operación depende de los resultados de las operaciones anteriores.

¿Por qué es necesario?

La mayoría de los métodos que usamos en Express son asíncronos: especificas una operación para realizar, pasando una devolución de llamada. El método regresa inmediatamente y la devolución de llamada se invoca cuando se completa la operación solicitada. Por convención en Express, las funciones de devolución de llamada pasan un valor de error como primer parámetro (o nulo en caso de éxito) y los resultados de la función (si los hay) como segundo parámetro.

Si un controlador solo necesita realizar una operación asíncrona para obtener la información requerida para representar una página, entonces la implementación es fácil: representamos la plantilla en la devolución de llamada. El siguiente fragmento de código muestra esto para una función que representa el conteo de un modelo SomeModel (usando el método Mongoose countDocuments):

1
2
3
4
5
6
7
8
9
10
11
12
exports.some_model_count = function (req, res, next) {
  SomeModel.countDocuments(
    { a_model_field: "match_value" },
    function (err, count) {
      // Do something if there is an err.
      // …

      // On success, render the result by passing count into the render function (here, as the variable 'data').
      res.render("the_template", { data: count });
    }
  );
};

¿Qué sucede si necesitas realizar varias consultas asíncronas y no puedes representar la página hasta que se hayan completado todas las operaciones? Una implementación ingenua podría “conectar en cadena” las solicitudes, iniciar solicitudes posteriores en la devolución de llamada de una solicitud anterior y presentar la respuesta en la devolución de llamada final. El problema con este enfoque es que nuestras solicitudes tendrían que ejecutarse en serie, aunque podría ser más eficiente ejecutarlas en paralelo. Esto también podría resultar en un código anidado complicado, comúnmente conocido como callback hell.

Una solución mucho mejor sería ejecutar todas las solicitudes en paralelo y luego tener una única devolución de llamada que se ejecute cuando se hayan completado todas las consultas. ¡Este es el tipo de operación de flujo que el módulo Async facilita!

Operaciones asíncronas en paralelo

El método async.parallel() se usa para ejecutar varias operaciones asíncronas en paralelo.

El primer argumento para async.parallel() es una colección de funciones asíncronas para ejecutar (una matriz, un objeto u otro iterable). A cada función se le pasa una devolución de llamada (err, result) que debe llamar al finalizar con un error err (que puede ser nulo) y un valor results opcional.

El segundo argumento opcional de async.parallel() es una devolución de llamada que se ejecutará cuando se hayan completado todas las funciones del primer argumento. La devolución de llamada se invoca con un argumento de error y una colección de resultados que contiene los resultados de las operaciones asíncronas individuales. La colección de resultados es del mismo tipo que el primer argumento (es decir, si pasa una matriz de funciones asíncronas, la devolución de llamada final se invocará con una matriz de resultados). Si alguna de las funciones paralelas informa un error, la devolución de llamada se invoca antes (con el valor de error).

El siguiente ejemplo muestra cómo funciona esto cuando pasamos un objeto como primer argumento. Como puedes ver, los resultados se devuelven en un objeto con los mismos nombres de propiedad que las funciones originales que se pasaron.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async.parallel(
  {
    one(callback) {
      /* … */
    },
    two(callback) {
      /* … */
    },
    // …
    something_else(callback) {
      /* … */
    },
  },
  // optional callback
  function (err, results) {
    // 'results' is now equal to: {one: 1, two: 2, …, something_else: some_value}
  }
);

Si, en cambio, pasas una matriz de funciones como primer argumento, los resultados serán una matriz (los resultados del orden de la matriz coincidirán con el orden original en que se declararon las funciones, no con el orden en que se completaron).

Operaciones asíncronas en serie

El método async.series() se usa para ejecutar varias operaciones asíncronas en secuencia, cuando las funciones posteriores no dependen de la salida de funciones anteriores. Básicamente se declara y se comporta de la misma manera que async.parallel().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async.series(
  {
    one(callback) {
      // …
    },
    two(callback) {
      // …
    },
    // …
    something_else(callback) {
      // …
    },
  },
  // optional callback after the last asynchronous function completes.
  function (err, results) {
    // 'results' is now equal to: {one: 1, two: 2, /* …, */ something_else: some_value}
  }
);

La especificación del lenguaje ECMAScript (JavaScript) establece que el orden de enumeración de un objeto no está definido, por lo que es posible que las funciones no se llamen en el mismo orden en que las especifica en todas las plataformas. Si el orden es realmente importante, debes pasar una matriz en lugar de un objeto, como se muestra a continuación.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async.series(
  [
    function (callback) {
      // do some stuff …
      callback(null, "one");
    },
    function (callback) {
      // do some more stuff …
      callback(null, "two");
    },
  ],
  // optional callback
  function (err, results) {
    // results is now equal to ['one', 'two']
  }
);

Operaciones asíncronas dependientes en serie

El método async.waterfall() se usa para ejecutar múltiples operaciones asíncronas en secuencia cuando cada operación depende del resultado de la operación anterior.

La devolución de llamada invocada por cada función asíncrona contiene un valor nulo para el primer argumento y da como resultado argumentos posteriores. Cada función de la serie toma los argumentos de resultados de la devolución de llamada anterior como los primeros parámetros y luego una función de devolución de llamada. Cuando se completan todas las operaciones, se invoca una devolución de llamada final con el resultado de la última operación. La forma en que esto funciona es más clara cuando considera el fragmento de código a continuación (este ejemplo es de la documentación de async):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async.waterfall(
  [
    function (callback) {
      callback(null, "one", "two");
    },
    function (arg1, arg2, callback) {
      // arg1 now equals 'one' and arg2 now equals 'two'
      callback(null, "three");
    },
    function (arg1, callback) {
      // arg1 now equals 'three'
      callback(null, "done");
    },
  ],
  function (err, result) {
    // result now equals 'done'
  }
);

Instalación de async

Instala el módulo asíncrono usando el administrador de paquetes npm para que podamos usarlo en nuestro código. Haz esto de la manera habitual, abriendo una consola en la raíz del proyecto LocalLibrary e introduciendo el siguiente comando:

1
npm install async

Plantillas

Una plantilla es un archivo de texto que define la estructura o el diseño de un archivo de salida, con marcadores de posición que se utilizan para representar dónde se insertarán los datos cuando se represente la plantilla (en Express, las plantillas se denominan vistas).

Opciones de plantilla Express

Express se puede utilizar con muchos motores de representación de plantillas diferentes. En este tutorial usamos Pug (anteriormente conocido como Jade) para nuestras plantillas. Este es el lenguaje de plantillas de Node más popular y se describe a sí mismo como una “sintaxis limpia y sensible a los espacios en blanco para escribir HTML, fuertemente influenciada por Haml”.

Diferentes lenguajes de plantilla usan diferentes enfoques para definir el diseño y marcar marcadores de posición para los datos; algunos usan HTML para definir el diseño, mientras que otros usan diferentes formatos de marcado que se pueden transpilar a HTML. Pug es del segundo tipo; usa una representación de HTML donde la primera palabra en cualquier línea generalmente representa un elemento HTML, y la sangría en las líneas subsiguientes se usa para representar el anidamiento. El resultado es una definición de página que se traduce directamente a HTML, pero es más concisa y posiblemente más fácil de leer.

La desventaja de usar Pug es que es sensible a la sangría y los espacios en blanco (si agregas un espacio adicional en el lugar equivocado, puede obtener un código de error inútil). Sin embargo, una vez que tengas las plantillas en su lugar, son muy fáciles de leer y mantener.

Configuración de plantillas

LocalLibrary se configuró para usar Pug cuando creamos el sitio web esqueleto. Deberías ver el módulo pug incluido como una dependencia en el archivo package.json del sitio web y los siguientes ajustes de configuración en el archivo app.js. La configuración nos dice que estamos usando pug como motor de visualización y que Express debe buscar plantillas en el subdirectorio /views.

1
2
3
// View engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");

Si buscas en el directorio de vistas, verás los archivos .pug para las vistas predeterminadas del proyecto. Estos incluyen la vista de la página de inicio (index.pug) y la plantilla base (layout.pug) que necesitaremos reemplazar con nuestro propio contenido.

1
2
3
4
5
/express-locallibrary-tutorial  //the project root
  /views
    error.pug
    index.pug
    layout.pug

Sintaxis de plantilla

El archivo de plantilla de ejemplo a continuación muestra muchas de las características más útiles de Pug.

Lo primero que debes notar es que el archivo mapea la estructura de un archivo HTML típico, con la primera palabra en (casi) cada línea siendo un elemento HTML, y la sangría se usa para indicar elementos anidados. Entonces, por ejemplo, el elemento del cuerpo está dentro de un elemento html y los elementos de párrafo (p) están dentro del elemento del cuerpo, etc. Los elementos no anidados (por ejemplo, párrafos individuales) están en líneas separadas.

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
28
29
30
31
32
doctype html
html(lang="en")
  head
    title= title
    script(type='text/javascript').
  body
    h1= title

    p This is a line with #[em some emphasis] and #[strong strong text] markup.
    p This line has un-escaped data: !{'<em> is emphasized</em>'} and escaped data: #{'<em> is not emphasized</em>'}.
      | This line follows on.
    p= 'Evaluated and <em>escaped expression</em>:' + title
    <!-- You can add HTML comments directly -->
    // You can add single line JavaScript comments and they are generated to HTML comments
    p A line with a link
      a(href='/catalog/authors') Some link text
      |  and some extra text.

    #container.col
      if title
        p A variable named "title" exists.
      else
        p A variable named "title" does not exist.
      p.
        Pug is a terse and simple template language with a
        strong focus on performance and powerful features.

    h2 Generate a list

    ul
      each val in [1, 2, 3, 4, 5]
        li= val

Los atributos de los elementos se definen entre paréntesis después de su elemento asociado. Dentro de los paréntesis, los atributos se definen en listas separadas por comas o espacios en blanco de los pares de nombres de atributos y valores de atributos, por ejemplo:

1
2
script(type='text/javascript'), link(rel='stylesheet', href='/stylesheets/style.css')
meta(name='viewport' content='width=device-width initial-scale=1')

Los valores de todos los atributos se escapan (por ejemplo, los caracteres como > se convierten en sus equivalentes de código HTML como &gt;) para evitar la inyección de JavaScript o cross-site scripting attacks.

Si una etiqueta va seguida del signo igual, el siguiente texto se trata como una expresión de JavaScript. Entonces, por ejemplo, en la primera línea a continuación, el contenido de la etiqueta h1 será un título variable (ya sea definido en el archivo o pasado a la plantilla desde Express). En la segunda línea, el contenido del párrafo es una cadena de texto concatenada con la variable de título. En ambos casos, el comportamiento predeterminado es escapar de la línea.

1
2
h1= title
p= 'Evaluated and <em>escaped expression</em>:' + title

Si no hay un símbolo de igual después de la etiqueta, el contenido se trata como texto sin formato. Dentro del texto sin formato, puedes insertar datos con y sin escape usando la sintaxis #{} y !{} respectivamente, como se muestra a continuación. También puedes agregar HTML sin procesar dentro del texto sin formato.

1
2
p This is a line with #[em some emphasis] and #[strong strong text] markup.
p This line has an un-escaped string: !{'<em> is emphasized</em>'}, an escaped string: #{'<em> is not emphasized</em>'}, and escaped variables: #{title}.

Casi siempre querrás escapar de los datos de los usuarios (a través de la sintaxis #{}). Los datos en los que se puede confiar (por ejemplo, recuentos de registros generados, etc.) se pueden mostrar sin escapar de los valores.

Puedes utilizar el carácter de barra vertical (|) al principio de una línea para indicar “texto sin formato”. Por ejemplo, el texto adicional que se muestra a continuación se mostrará en la misma línea que el ancla anterior, pero no estará vinculado.

1
2
a(href='http://someurl/') Link text
| Plain text

Pug te permite realizar operaciones condicionales usando if, else, else if y unless, por ejemplo:

1
2
3
4
if title
  p A variable named "title" exists
else
  p A variable named "title" does not exist

También puedes realizar operaciones de bucle/iteración utilizando la sintaxis each-in o while. En el fragmento de código a continuación, hemos recorrido una matriz para mostrar una lista de variables (ten en cuenta el uso de ‘li =’ para evaluar el “val” como una variable a continuación. El valor que itera también se puede pasar al plantilla como una variable!

1
2
3
ul
  each val in [1, 2, 3, 4, 5]
    li= val

La sintaxis también admite comentarios (que se pueden representar en la salida, o no, según elija), mixins para crear bloques de código reutilizables, declaraciones de casos y muchas otras características. Para obtener información más detallada, consulta los documentos de The Pug.

Extender plantillas

En un sitio, es habitual que todas las páginas tengan una estructura común, incluido el marcado HTML estándar para el encabezado, el pie de página, la navegación, etc. En lugar de obligar a los desarrolladores a duplicar este “repetitivo” en cada página, Pug te permite declarar un plantilla base y luego se extiende, reemplazando solo lo que es diferente para cada página específica.

Por ejemplo, la plantilla base layout.pug creada en nuestro proyecto de esqueleto se ve así:

1
2
3
4
5
6
7
doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
  body
    block content

La etiqueta block se usa para marcar secciones de contenido que se pueden reemplazar en una plantilla derivada (si el bloque no se redefine, se usa su implementación en la clase base).

El index.pug predeterminado (creado para nuestro proyecto de esqueleto) muestra cómo anulamos la plantilla base. La etiqueta extends identifica la plantilla base a usar y luego usamos el bloque section_name para indicar el nuevo contenido de la sección que anularemos.

1
2
3
4
5
extends layout

block content
  h1= title
  p Welcome to #{title}

La plantilla base

Ahora que entendemos cómo extender plantillas usando Pug, comencemos creando una plantilla base para el proyecto. Tendrá una barra lateral con enlaces para las páginas que esperamos crear en los artículos del tutorial (por ejemplo, para mostrar y crear libros, géneros, autores, etc.) y un área de contenido principal que anularemos en cada una de nuestras páginas individuales.

Abre /views/layout.pug y reemplace el contenido con el siguiente código.

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
28
29
30
31
32
33
34
35
36
37
38
39
doctype html
html(lang='en')
  head
    title= title
    meta(charset='utf-8')
    meta(name='viewport', content='width=device-width, initial-scale=1')
    link(rel="stylesheet", href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css", integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z", crossorigin="anonymous")
    script(src="https://code.jquery.com/jquery-3.5.1.slim.min.js", integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj", crossorigin="anonymous")
    script(src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js", integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV", crossorigin="anonymous")
    link(rel='stylesheet', href='/stylesheets/style.css')
  body
    div(class='container-fluid')
      div(class='row')
        div(class='col-sm-2')
          block sidebar
            ul(class='sidebar-nav')
              li
                a(href='/catalog') Home
              li
                a(href='/catalog/books') All books
              li
                a(href='/catalog/authors') All authors
              li
                a(href='/catalog/genres') All genres
              li
                a(href='/catalog/bookinstances') All book-instances
              li
                hr
              li
                a(href='/catalog/author/create') Create new author
              li
                a(href='/catalog/genre/create') Create new genre
              li
                a(href='/catalog/book/create') Create new book
              li
                a(href='/catalog/bookinstance/create') Create new book instance (copy)

        div(class='col-sm-10')
          block content

La plantilla usa (e incluye) JavaScript y CSS de Bootstrap para mejorar el diseño y la presentación de la página HTML. El uso de Bootstrap u otro marco web del lado del cliente es una forma rápida de crear una página atractiva que puede escalar bien en diferentes tamaños de navegador, y también nos permite manejar la presentación de la página sin tener que entrar en detalles, simplemente quiero centrarme en el código del lado del servidor aquí!

El diseño debería ser bastante obvio si has leído nuestro manual de plantilla anterior. Ten en cuenta el uso de block content como marcador de posición para el lugar donde se colocará el contenido de nuestras páginas individuales.

La plantilla base también hace referencia a un archivo CSS local (style.css) que proporciona un poco de estilo adicional. Abre /public/stylesheets/style.css y reemplace su contenido con el siguiente código CSS:

1
2
3
4
5
.sidebar-nav {
  margin-top: 20px;
  padding: 0;
  list-style: none;
}

Ahora tenemos una plantilla base para crear páginas con una barra lateral. En las próximas secciones lo usaremos para definir las páginas individuales.

Página de inicio

La primera página que crearemos será la página de inicio del sitio web, a la que se puede acceder desde la raíz del sitio (‘/’) o del catálogo (catalog/). Esto mostrará un texto estático que describe el sitio, junto con “recuentos” calculados dinámicamente de diferentes tipos de registros en la base de datos.

Ya hemos creado una ruta para la página de inicio. Para completar la página, necesitamos actualizar nuestra función de controlador para obtener “recuentos” de registros de la base de datos y crear una vista (plantilla) que podamos usar para representar la página.

Ruta

Creamos nuestras rutas de página de índice en un tutorial anterior. Como recordatorio, todas las funciones de ruta están definidas en /routes/catalog.js:

1
2
// GET catalog home page.
router.get("/", book_controller.index); //This actually maps to /catalog/ because we import the route with a /catalog prefix

Donde el parámetro de la función de devolución de llamada (book_controller.index) se define en /controllers/bookController.js:

Es esta función de controlador la que ampliamos para obtener información de nuestros modelos y luego representarla usando una plantilla (vista).

Controlador

La función de controlador de índice necesita obtener información sobre cuántos registros Book, BookInstance, BookInstance disponibles, Author y Genre tenemos en la base de datos, representar estos datos en una plantilla para crear una página HTML y luego devolverlos en una respuesta HTTP.

Usamos el método countDocuments() para obtener el número de instancias de cada modelo. Esto se llama en un modelo, con un conjunto opcional de condiciones para comparar en el primer argumento y una devolución de llamada en el segundo argumento (como se explica en Uso de una base de datos (con Mongoose), y también puede devolver una Query y luego ejecutar con una devolución de llamada más tarde). La devolución de llamada se invocará cuando la base de datos devuelva el recuento, con un valor de error como primer parámetro (o nulo) y el recuento de documentos como segundo parámetro (o nulo si hubo un error).

1
2
3
4
SomeModel.countDocuments({ a_model_field: "match_value" }, (err, count) => {
  // Do something if there is an err
  // Do something with the count if there was no error
});

Abre /controllers/bookController.js. Cerca de la parte superior del archivo, deberías ver la función index() exportada.

1
2
3
4
5
const Book = require("../models/book");

exports.index = (req, res, next) => {
  res.send("NOT IMPLEMENTED: Site Home Page");
};

Reemplaza todo el código anterior con el siguiente fragmento de código. Lo primero que hace es importar (require()) todos los modelos. Necesitamos hacer esto porque los usaremos para obtener nuestros conteos de documentos. Luego importa el módulo async (que discutimos anteriormente en Control de flujo asíncrono usando async).

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
28
29
30
31
32
33
34
35
const Book = require("../models/book");
const Author = require("../models/author");
const Genre = require("../models/genre");
const BookInstance = require("../models/bookinstance");

const async = require("async");

exports.index = (req, res) => {
  async.parallel(
    {
      book_count(callback) {
        Book.countDocuments({}, callback); // Pass an empty object as match condition to find all documents of this collection
      },
      book_instance_count(callback) {
        BookInstance.countDocuments({}, callback);
      },
      book_instance_available_count(callback) {
        BookInstance.countDocuments({ status: "Available" }, callback);
      },
      author_count(callback) {
        Author.countDocuments({}, callback);
      },
      genre_count(callback) {
        Genre.countDocuments({}, callback);
      },
    },
    (err, results) => {
      res.render("index", {
        title: "Local Library Home",
        error: err,
        data: results,
      });
    }
  );
};

Al método async.parallel() se le pasa un objeto con funciones para obtener los recuentos de cada uno de nuestros modelos. Todas estas funciones se inician al mismo tiempo. Cuando todos han completado, se invoca la devolución de llamada final con los recuentos en el parámetro de resultados (o un error).

En caso de éxito, la función de devolución de llamada llama a res.render(), especificando una vista (plantilla) llamada index y un objeto que contiene los datos que se insertarán en él (esto incluye el objeto de resultados que contiene nuestro modelo). Los datos se proporcionan como pares clave-valor y se puede acceder a ellos en la plantilla mediante la clave.

View

Abre /views/index.pug y reemplaza su contenido con el texto a continuación.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extends layout

block content
  h1= title
  p Welcome to #[em LocalLibrary], a very basic Express website developed as a tutorial example on the Mozilla Developer Network.

  h1 Dynamic content

  if error
    p Error getting dynamic content.
  else
    p The library has the following record counts:

    ul
      li #[strong Books:] !{data.book_count}
      li #[strong Copies:] !{data.book_instance_count}
      li #[strong Copies available:] !{data.book_instance_available_count}
      li #[strong Authors:] !{data.author_count}
      li #[strong Genres:] !{data.genre_count}

La vista es sencilla. Extendemos la plantilla base de layout.pug, anulando el bloque llamado content. El primer encabezado h1 será el texto escapado para la variable de título que se pasó a la función render(); ten en cuenta el uso de h1= para que el siguiente texto se trate como una expresión de JavaScript. Luego incluimos un párrafo que presenta LocalLibrary.

Bajo el encabezado Dynamic content, verificamos si la variable de error que se pasó desde la función render() se ha definido. Si es así, anotamos el error. Si no, obtenemos y enumeramos el número de copias de cada modelo de la variable de datos.

No escapamos de los valores de conteo (es decir, usamos la sintaxis !{}) porque los valores de conteo se calculan. Si la información fue proporcionada por los usuarios finales, escaparíamos de la variable para mostrarla.

Cómo se ve?

En este punto, deberíamos haber creado todo lo necesario para mostrar la página de índice. Ejecute la aplicación y abra su navegador en http://localhost:3000/. Si todo está configurado correctamente, su sitio debería parecerse a la siguiente captura de pantalla.

image-20230124184324539

Página de lista de libros

A continuación, implementaremos nuestra página de lista de libros. Esta página debe mostrar una lista de todos los libros en la base de datos junto con su autor, y cada título de libro es un hipervínculo a su página de detalles de libro asociada.

Controlador

La función del controlador de la lista de libros necesita obtener una lista de todos los objetos Book en la base de datos, ordenarlos y luego pasarlos a la plantilla para su representación.

Abre /controllers/bookController.js. Busca el método del controlador book_list() exportado y reemplázalo con el siguiente código.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Display list of all Books.
exports.book_list = function (req, res, next) {
  Book.find({}, "title author")
    .sort({ title: 1 })
    .populate("author")
    .exec(function (err, list_books) {
      if (err) {
        return next(err);
      }
      //Successful, so render
      res.render("book_list", { title: "Book List", book_list: list_books });
    });
};

El método usa la función find() del modelo para devolver todos los objetos Book, seleccionando devolver solo el title y el author ya que no necesitamos los otros campos (también devolverá el _id y los campos virtuales), y luego ordena los resultados por title alfabéticamente usando el método sort(). Aquí también llamamos populate() en Book, especificando el campo de author; esto reemplazará la identificación del autor del libro almacenado con los detalles completos del autor.

En caso de éxito, la devolución de llamada pasada a la consulta representa la plantilla book_list (.pug), pasando el título y book_list (lista de libros con autores) como variables.

Vista

Crea /views/book_list.pug y copie el texto a continuación.

1
2
3
4
5
6
7
8
9
10
11
12
13
extends layout

block content
  h1= title

  ul
    each book in book_list
      li
        a(href=book.url) #{book.title}
        |  (#{book.author.name})

    else
      li There are no books.

La vista extiende la plantilla base de layout.pug y anula el bloque llamado contect. Muestra el título que le pasamos desde el controlador (a través del método render()) e itera a través de la variable book_list usando la sintaxis each-in-else. Se crea un elemento de lista para cada libro que muestra el título del libro como un enlace a la página de detalles del libro seguido del nombre del autor. Si no hay libros en book_list, se ejecuta la cláusula else y se muestra el texto ‘There are no books’.

Usamos book.url para proporcionar el enlace al registro detallado de cada libro (hemos implementado esta ruta, pero aún no la página). Esta es una propiedad virtual del modelo Book que utiliza el campo _id de la instancia del modelo para producir una ruta URL única.

De interés aquí es que cada libro se define como dos líneas, utilizando la tubería para la segunda línea. Este enfoque es necesario porque si el nombre del autor estuviera en la línea anterior, sería parte del hipervínculo.

image-20230124185318470

Página de instancias de libros

Crea el controlador que muestre todas las instancias de libros. La plantilla es la siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
extends layout

block content
  h1= title

  ul
    each val in bookinstance_list
      li
        a(href=val.url) #{val.book.title} : #{val.imprint} -
        if val.status=='Available'
          span.text-success #{val.status}
        else if val.status=='Maintenance'
          span.text-danger #{val.status}
        else
          span.text-warning #{val.status}
        if val.status!='Available'
          span  (Due: #{val.due_back} )

    else
      li There are no book copies in this library.

image-20230124185718573

Formateo de fechas usando luxon

El renderizado de las fechas es bastante feo:

1
(Due: Sun Jan 22 2023 19:11:04 GMT+0100 (hora estándar de Europa central) )

Vamos a usar la librería luxon

Para instalarla, usamos npm

1
npm install luxon

Crear una propiedad virtual

  1. Abre modles/bookinstance.js

  2. Al principio de la página, importa luxon

    1
    
    const { DateTime } = require("luxon");
    
  3. Añade la propiedad virtual due_back_formatted al modelo después de la propiedad URL

    1
    2
    3
    
    BookInstanceSchema.virtual("due_back_formatted").get(function () {
      return    DateTime.fromJSDate(this.due_back).toLocaleString(DateTime.DATE_MED);
    });
    

Actualizar la vista

Modifica la vista para que ahora renderice esta propiedad virtual.

1
2
3
4
      if val.status != 'Available'
        //span  (Due: #{val.due_back} )
        span  (Due: #{val.due_back_formatted} )

Página de libros

Crea la página para listar los libros. Modifica las fechas de nacimiento y defunción al igual que hicimos en bookinstance

image-20230124191604572

Página de géneros

Crea la página para listar los géneros

image-20230124192352341

Página detalle de género

La página de detalles del género debe mostrar la información de una instancia de género en particular, utilizando su valor de campo _id generado automáticamente como identificador. La página debe mostrar el nombre del género y una lista de todos los libros del género con enlaces a la página de detalles de cada libro.

Controlador

Abre /controllers/genreController.js e importa los módulos async y Book en la parte superior del archivo.

1
2
const Book = require("../models/book");
const async = require("async");

Encuentra el método genre_detail() y sustitúyelo por el siguiente código:

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
28
29
30
31
// Display detail page for a specific Genre.
exports.genre_detail = (req, res, next) => {
  async.parallel(
    {
      genre(callback) {
        Genre.findById(req.params.id).exec(callback);
      },

      genre_books(callback) {
        Book.find({ genre: req.params.id }).exec(callback);
      },
    },
    (err, results) => {
      if (err) {
        return next(err);
      }
      if (results.genre == null) {
        // No results.
        const err = new Error("Genre not found");
        err.status = 404;
        return next(err);
      }
      // Successful, so render
      res.render("genre_detail", {
        title: "Genre Detail",
        genre: results.genre,
        genre_books: results.genre_books,
      });
    }
  );
};

El método usa async.parallel() para consultar el nombre del género y sus libros asociados en paralelo, y la devolución de llamada muestra la página cuando (if) ambas solicitudes se completan correctamente.

El ID del registro de género requerido se codifica al final de la URL y se extrae automáticamente según la definición de la ruta (/genre/:id). Se accede a la ID dentro del controlador a través de los parámetros de solicitud: req.params.id. Se usa en Genre.findById() para obtener el género actual. También se utiliza para obtener todos los objetos Book que tienen el ID de género en su campo de género: Book.find({ 'genre': req.params.id }).

Si el género no existe en la base de datos (es decir, es posible que se haya eliminado), findById() volverá correctamente sin resultados. En este caso, queremos mostrar una página “no encontrada”, por lo que creamos un objeto de error y lo pasamos a la siguiente función de middleware en la cadena.

1
2
3
4
5
6
if (results.genre == null) {
  // No results.
  const err = new Error("Genre not found");
  err.status = 404;
  return next(err);
}

Luego, el mensaje se propagará a través de nuestro código de manejo de errores (esto se configuró cuando generamos el esqueleto de la aplicación; para obtener más información, consulta Manejo de errores).

La vista renderizada es gender_detail y se le pasan variables para el título, el género y la lista de libros de este género (genre_books).

Vista

Crea views/genre_detail.pug y pega el siguiente código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extends layout

block content

  h1 Genre: #{genre.name}

  div(style='margin-left:20px;margin-top:20px')

    h4 Books

    dl
      each book in genre_books
        dt
          a(href=book.url) #{book.title}
        dd #{book.summary}

      else
        p This genre has no books

¿Cómo se ve?

Ejecuta la aplicación y abre el navegador en http://localhost:3000/. Seleccione el enlace All genres, luego selecciona uno de los géneros (por ejemplo, “Fantasy”). Si todo está configurado correctamente, la página debería verse como la siguiente captura de pantalla.

Página detalle del libro

La página de detalles del libro debe mostrar la información de un libro específico (identificado mediante su valor de campo _id generado automáticamente), junto con información sobre cada copia asociada en la biblioteca (BookInstance). Dondequiera que mostremos un autor, género o instancia de libro, estos deben estar vinculados a la página de detalles asociada a ese elemento.

Controlador

Abre /controllers/bookController.js. Busca el método de controlador exportado book_detail() y reemplácelo con el siguiente código.

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
28
29
30
31
32
33
// Display detail page for a specific book.
exports.book_detail = (req, res, next) => {
  async.parallel(
    {
      book(callback) {
        Book.findById(req.params.id)
          .populate("author")
          .populate("genre")
          .exec(callback);
      },
      book_instance(callback) {
        BookInstance.find({ book: req.params.id }).exec(callback);
      },
    },
    (err, results) => {
      if (err) {
        return next(err);
      }
      if (results.book == null) {
        // No results.
        const err = new Error("Book not found");
        err.status = 404;
        return next(err);
      }
      // Successful, so render.
      res.render("book_detail", {
        title: results.book.title,
        book: results.book,
        book_instances: results.book_instance,
      });
    }
  );
};

Vista

Crea /views/book_detail.pug y pega el siguiente código:

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
28
29
30
31
32
33
34
extends layout

block content
  h1 Title: #{book.title}

  p #[strong Author:]
    a(href=book.author.url) #{book.author.name}
  p #[strong Summary:] #{book.summary}
  p #[strong ISBN:] #{book.isbn}
  p #[strong Genre:]
    each val, index in book.genre
      a(href=val.url) #{val.name}
      if index < book.genre.length - 1
        |,

  div(style='margin-left:20px;margin-top:20px')
    h4 Copies

    each val in book_instances
      hr
      if val.status=='Available'
        p.text-success #{val.status}
      else if val.status=='Maintenance'
        p.text-danger #{val.status}
      else
        p.text-warning #{val.status}
      p #[strong Imprint:] #{val.imprint}
      if val.status!='Available'
        p #[strong Due back:] #{val.due_back}
      p #[strong Id:]
        a(href=val.url) #{val._id}

    else
      p There are no copies of this book in the library.

La lista de géneros asociados con el libro se implementa en la plantilla como se muestra a continuación. Esto agrega una coma después de cada género asociado con el libro excepto el último.

1
2
3
4
5
  p #[strong Genre:]
    each val, index in book.genre
      a(href=val.url) #{val.name}
      if index < book.genre.length - 1
        |,

¿Cómo se ve?

Ejecuta la aplicación y abre el navegador en http://localhost:3000/. Selecciona el enlace All books, luego selecciona uno de los libros. Si todo está configurado correctamente, su página debería verse como la siguiente captura de pantalla.

Book Detail Page - Express Local Library site

Página detalle de autor

La página de detalles del autor debe mostrar la información sobre el autor especificado, identificado mediante su valor de campo _id (generado automáticamente), junto con una lista de todos los objetos Book asociados con ese autor.

Controlador

Abre controllers/authorControler.js y pega lo siguiente al principio:

1
2
const async = require("async");
const Book = require("../models/book");

Encuentra el método author_detail y sustitúyelo por el siguiente código:

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
28
29
30
31
// Display detail page for a specific Author.
exports.author_detail = (req, res, next) => {
  async.parallel(
    {
      author(callback) {
        Author.findById(req.params.id).exec(callback);
      },
      authors_books(callback) {
        Book.find({ author: req.params.id }, "title summary").exec(callback);
      },
    },
    (err, results) => {
      if (err) {
        // Error in API usage.
        return next(err);
      }
      if (results.author == null) {
        // No results.
        const err = new Error("Author not found");
        err.status = 404;
        return next(err);
      }
      // Successful, so render.
      res.render("author_detail", {
        title: "Author Detail",
        author: results.author,
        author_books: results.authors_books,
      });
    }
  );
};

El método usa async.parallel() para consultar al autor y sus instancias de libro asociadas en paralelo, y la devolución de llamada muestra la página cuando (if) ambas solicitudes se completan correctamente. El enfoque es exactamente el mismo que se describe en la página de detalles del género anterior.

Vista

Crea views/authorDetail.pug y pega el siguiente código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extends layout

block content

  h1 Author: #{author.name}
  p #{author.date_of_birth} - #{author.date_of_death}

  div(style='margin-left:20px;margin-top:20px')

    h4 Books

    dl
      each book in author_books
        dt
          a(href=book.url) #{book.title}
        dd #{book.summary}

      else
        p This author has no books.

¿Cómo se ve?

Ejecuta la aplicación y abre el navegador en http://localhost:3000/. Selecciona el enlace All authors, luego selecciona uno de los autores. Si todo está configurado correctamente, la página debería verse como la siguiente captura de pantalla.

Author Detail Page - Express Local Library site

Página detalle BookInstance

Realiza la página de detalle para BookInstace

BookInstance Detail Page - Express Local Library site