Cómo generar un PDF en Node.js con Puppeteer y JavaScript

Cómo generar un archivo PDF y renderizarlo en el navegador usando Puppeteer y Express.

Primeros pasos

Para este tutorial, vamos a usar CheatCode Node.js Boilerplate para darnos un punto de partida para nuestro trabajo. Primero, clonemos una copia de eso en nuestra computadora:

Terminal

git clone https://github.com/cheatcode/nodejs-server-boilerplate.git server

A continuación, instale las dependencias para el repetitivo:

Terminal

cd server && npm install

Después de eso, necesitamos instalar el puppeteer paquete de NPM que nos ayudará a generar nuestro PDF:

Terminal

npm i puppeteer

Finalmente, inicie el servidor de desarrollo:

Terminal

npm run dev

Después de esto, tenemos todo lo que necesitamos para hacer nuestro trabajo.

Creación de una función de generador de PDF

Nuestra primera tarea es escribir la función que usaremos para generar nuestro PDF. Esta función tomará algo de HTML y CSS para el contenido de nuestro PDF y luego lo generará como un PDF real:

/lib/generarPDF.js

import puppeteer from "puppeteer";

export default (html = "") => {};

Aquí, comenzamos importando el puppeteer dependencia que instalamos anteriormente. Esto es lo que usaremos para generar nuestro PDF. Debajo de esa importación, creamos un esqueleto para nuestro generatePDF() función, tomando un solo argumento html como una cadena.

/lib/generarPDF.js

import puppeteer from "puppeteer";

export default async (html = "") => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.setContent(html);
};

Luego, usando el puppeteer paquete que importamos arriba, creamos una instancia de un navegador web con puppeteer.launch() . Tenga en cuenta que aquí, esperamos que la función nos devuelva una Promesa de JavaScript, por lo que agregamos el await palabra clave delante para decir "esperar a que se resuelva la Promesa devuelta por esta función antes de continuar con el resto de nuestro código".

Para que esto también funcione, agregaremos un async palabra clave justo antes de nuestra definición de función arriba. Si no hacemos esto, JavaScript arrojará un error de tiempo de ejecución que dice "esperar es una palabra clave reservada".

Una vez que tengamos nuestro Titiritero browser ejemplo, a continuación, creamos una nueva página con browser.newPage() . Aunque no lo parezca, es como abrir una pestaña en su navegador web (Puppeteer es lo que se conoce como un navegador "sin cabeza", o un navegador web sin GUI o interfaz gráfica de usuario).

Nuevamente, usamos el await palabra clave aquí. Esto se debe a que todos de las funciones que usaremos de Puppeteer devuelven una promesa de JavaScript. Queremos await estas Promesas porque lo que estamos haciendo es un sincrónico proceso (lo que significa que queremos asegurarnos de que cada paso en nuestro código esté completo antes de pasar al siguiente).

Finalmente, con nuestro page disponible, establecemos el contenido de la página:el marcado HTML que constituye lo que veríamos en el navegador si no fuera sin cabeza.

En este punto, si tuviéramos que usar un navegador con una GUI, veríamos todo el HTML/CSS que pasamos en la pantalla.

/lib/generarPDF.js

import puppeteer from "puppeteer";

export default async (html = "") => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.setContent(html);

  const pdfBuffer = await page.pdf();

  await page.close();
  await browser.close();

  return pdfBuffer;
};

Desarrollando el resto de nuestra función, ahora vemos cómo pasamos de renderizar una página en el navegador a obtener un PDF. Aquí llamamos al Titiritero page.pdf() función. Este se encarga de convertir nuestra página HTML al formato PDF.

Tenga en cuenta que estamos llamando a este método en el page variable que creamos arriba y configuramos el contenido. En esencia, esto significa "convertir esta página en un PDF". A page.pdf() , opcionalmente, puede pasar opciones para personalizar la apariencia de su PDF.

Aunque no parezca mucho, esto es todo lo que tenemos que hacer para recuperar nuestro archivo PDF. Notarás que almacenamos la respuesta a page.pdf() en una variable llamada pdfBuffer . Esto se debe a que lo que obtenemos como respuesta es un búfer de archivo que es la representación en memoria de nuestro PDF (es decir, el contenido del archivo antes de que se convierta en un archivo real que tendríamos en nuestra computadora).

Antes de devolver este búfer de archivo desde nuestra función en la parte inferior, nos aseguramos de llamar a page.close() y browser.close() para borrar nuestra instancia de Titiritero en la memoria. Esto es muy importante porque si no lo hace, después de generar nuestro PDF, Titiritero seguirá ocupando memoria. Es decir, cada vez que alguien llame a esta función, se creará una nueva instancia de Titiritero en la memoria. Haz eso suficientes veces y tu servidor se quedará sin memoria provocando un accidente.

Con eso, nuestro generatePDF() la función está completa. Para finalizar el tutorial, creemos una ruta HTTP en nuestro servidor que podamos usar para llamar a nuestro generatePDF() función.

Conectando una ruta para probar nuestro generador de PDF

Para probar nuestra generación de PDF, vamos a crear una ruta HTTP usando el servidor Express configurado para nosotros en CheatCode Node.js Boilerplate con el que estamos construyendo esta aplicación. Para asegurarnos de que nuestro cableado tenga sentido, muy rápido, veamos cómo está configurado nuestro servidor Express y luego dónde vivirá nuestro código.

/index.js

import express from "express";
import startup from "./lib/startup";
import api from "./api/index";
import middleware from "./middleware/index";
import logger from "./lib/logger";

startup()
  .then(() => {
    const app = express();
    const port = process.env.PORT || 5001;

    middleware(app);
    api(app);

    app.listen(port, () => {
      if (process.send) {
        process.send(`Server running at http://localhost:${port}\n\n`);
      }
    });

    process.on("message", (message) => {
      console.log(message);
    });
  })
  .catch((error) => {
    logger.error(error);
  });

Desde la raíz del proyecto, el index.js El archivo contiene todo el código para iniciar nuestro servidor Express. Dentro, la idea es que tengamos un startup() método que se llama before configuramos nuestro servidor HTTP (esto configura nuestros detectores de eventos para errores y, si lo deseamos, cualquier otra cosa que deba cargarse antes de que se inicie nuestro servidor HTTP).

En el .then() devolución de llamada para nuestro startup() método, llamamos al familiar express() función, recibiendo nuestro app instancia a cambio. Con esto, escuchamos las conexiones en el process.env.PORT (normalmente se establece al implementar una aplicación) o el puerto predeterminado 5001 .

Justo encima de nuestra llamada a app.listen() llamamos a dos funciones middleware() y api() que toman en nuestra instancia de aplicación. Estas funciones se utilizan para separar nuestro código para la organización. Vamos a escribir nuestra ruta de prueba para generar un PDF dentro del api() función aquí.

Echemos un vistazo a esa función ahora:

/api/index.js

import generatePDF from "../lib/generatePDF";
import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  app.use("/pdf", (req, res) => {
    // We'll call to generatePDF() here...
  });
};

Tomando en el app instancia que pasamos desde /index.js , aquí configuramos la API para nuestro servidor. De forma predeterminada, este modelo usa GraphQL para su API principal, por lo que aquí llamamos para configurar esa API de GraphQL a través de graphql() , pasando también el app instancia. No usaremos esto para nuestro trabajo en este tutorial.

La parte que nos importa es nuestra llamada a app.use() , pasando el /pdf camino donde esperamos que viva nuestra ruta. Nuestro objetivo es hacer que cuando visitemos esta ruta, llamemos generatePDF() —pasando algo de HTML y CSS— y luego devuélvalo a nuestra ruta. El objetivo es representar nuestro archivo PDF en el navegador (utilizando el visor de PDF integrado del navegador) para que podamos verificar que nuestra función funciona y tener acceso a un botón de descarga gratuita.

/api/index.js

import generatePDF from "../lib/generatePDF";
import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  app.use("/pdf", async (req, res) => {
    const pdf = await generatePDF(`
      <html>
        <head>
          <title>Test PDF</title>
        </head>
        <body>
           // The contents of our PDF will go here...
        </body>
      </html>
    `);

    res.set("Content-Type", "application/pdf");
    res.send(pdf);
  });
};

Para lograr eso, usando el generatePDF() función que escribimos anteriormente y hemos importado en la parte superior, dentro de la función de devolución de llamada para nuestra ruta Express, agregamos el async palabra clave como aprendimos anteriormente y luego llamar a generatePDF() , pasando una cadena de HTML (lo agregaremos a continuación).

Recuerda que cuando llamamos a generatePDF() , esperamos recuperar nuestro PDF como un búfer de archivos (una representación en memoria de nuestro navegador). Lo bueno de esto es que, si le decimos a la solicitud HTTP entrante el formato:Content-Type —de nuestra respuesta, manejará los datos que le enviemos de manera diferente.

Aquí, usamos el .set() método en HTTP res ponse el objeto, diciendo que "queremos establecer el Content-Type encabezado a application/pdf ." El application/pdf parte es lo que se conoce como tipo MIME. Un tipo MIME es un tipo de archivo/dato reconocido universalmente por los navegadores. Usando ese tipo, podemos decirle a nuestro navegador "los datos que estamos enviando en respuesta a su solicitud están en el siguiente formato".

Después de eso, todo lo que tenemos que hacer es llamar al .send() método en res ponse, pasando nuestro pdf búfer de archivos. ¡El navegador se encarga del resto!

Antes de probar esto, desarrollemos nuestro HTML de prueba:

/api/index.js

import generatePDF from "../lib/generatePDF";
import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  app.use("/pdf", async (req, res) => {
    const pdf = await generatePDF(`
      <html>
        <head>
          <title>Test PDF</title>
          <style>
            body {
              padding: 60px;
              font-family: "Hevletica Neue", "Helvetica", "Arial", sans-serif;
              font-size: 16px;
              line-height: 24px;
            }

            body > h4 {
              font-size: 24px;
              line-height: 24px;
              text-transform: uppercase;
              margin-bottom: 60px;
            }

            body > header {
              display: flex;
            }

            body > header > .address-block:nth-child(2) {
              margin-left: 100px;
            }

            .address-block address {
              font-style: normal;
            }

            .address-block > h5 {
              font-size: 14px;
              line-height: 14px;
              margin: 0px 0px 15px;
              text-transform: uppercase;
              color: #aaa;
            }

            .table {
              width: 100%;
              margin-top: 60px;
            }

            .table table {
              width: 100%;
              border: 1px solid #eee;
              border-collapse: collapse;
            }

            .table table tr th,
            .table table tr td {
              font-size: 15px;
              padding: 10px;
              border: 1px solid #eee;
              border-collapse: collapse;
            }

            .table table tfoot tr td {
              border-top: 3px solid #eee;
            }
          </style>
        </head>
        <body>
          <h4>Invoice</h4>
          <header>
            <div class="address-block">
              <h5>Recipient</h5>
              <address>
                Doug Funnie<br />
                321 Customer St.<br />
                Happy Place, FL 17641<br />
              </address>
            </div>
            <div class="address-block">
              <h5>Sender</h5>
              <address>
                Skeeter Valentine<br />
                123 Business St.<br />
                Fake Town, TN 37189<br />
              </address>
            </div>
          </header>
          <div class="table">
            <table>
              <thead>
                <tr>
                  <th style="text-align:left;">Item Description</th>
                  <th>Price</th>
                  <th>Quantity</th>
                  <th>Total</th>
                </tr>
              </thead>
              <tbody>
                <tr>
                  <td style="text-align:left;">Swiss Army Cat</td>
                  <td style="text-align:center;">$32.70</td>
                  <td style="text-align:center;">x1</td>
                  <td style="text-align:center;">$32.70</td>
                </tr>
                <tr>
                  <td style="text-align:left;">Holeless Strainer</td>
                  <td style="text-align:center;">$9.00</td>
                  <td style="text-align:center;">x2</td>
                  <td style="text-align:center;">$18.00</td>
                </tr>
                <tr>
                  <td style="text-align:left;">"The Government Lies" T-Shirt</td>
                  <td style="text-align:center;">$20.00</td>
                  <td style="text-align:center;">x1</td>
                  <td style="text-align:center;">$20.00</td>
                </tr>
              </tbody>
              <tfoot>
                <tr>
                  <td colSpan="2" />
                  <td style="text-align:right;"><strong>Total</strong></td>
                  <td style="text-align:center;">$70.70</td>
                </tr>
              </tfoot>
            </table>
          </div>
        </body>
      </html>
    `);

    res.set("Content-Type", "application/pdf");
    res.send(pdf);
  });
};

En el <head></head> de nuestro HTML, hemos agregado algo de CSS para diseñar el marcado que hemos agregado en nuestro <body></body> etiqueta. Aunque los detalles están fuera del alcance de este tutorial, lo que esto nos brinda es un diseño de factura simple (un caso de uso común para la representación de PDF):

Si visitamos http://localhost:5001/pdf en nuestro navegador web, el lector de PDF incorporado debería activarse y deberíamos ver nuestro PDF representado en la pantalla. Desde aquí, podemos usar el botón de descarga en la parte superior derecha para guardar una copia en nuestra computadora.

Terminando

En este tutorial, aprendimos cómo convertir HTML en PDF usando Puppeteer. Aprendimos a crear una instancia del navegador Titiritero, abrir una página en esa instancia y configurar el contenido HTML de esa página. A continuación, aprendimos cómo convertir esa página HTML en un búfer de archivo PDF y luego, una vez almacenada en caché en una variable, cerrar la página de Titiritero y la instancia del navegador para ahorrar memoria.

Finalmente, aprendimos cómo tomar el búfer del archivo PDF que recibimos de Puppeteer y renderizarlo en el navegador usando Express.