Articles

Generando PDF a partir de HTML con Nodo.js y Titiritero

La imagen de Máté Boér's Picture

Máté Boér

Desarrollador de pila completa en RisingStack

En este artículo voy a mostrar cómo se puede generar un documento PDF a partir de una página de React con mucho estilo utilizando el Nodo.js, Puppeteer, Chrome sin cabeza & Acoplador.

Fondo: Hace unos meses, uno de los clientes de RisingStack nos pidió que desarrolláramos una función donde el usuario pudiera solicitar una página de React en formato PDF. Esa página es básicamente un informe / resultado para pacientes con visualización de datos, que contiene una gran cantidad de SVG. Además, hubo algunas peticiones especiales para manipular el diseño y hacer algunos reordenamientos de los elementos HTML. Por lo tanto, el PDF debe tener un estilo y adiciones diferentes en comparación con la página original de React.

Como la tarea era un poco más compleja de lo que se podría haber resuelto con reglas CSS simples, primero exploramos posibles implementaciones. Básicamente, encontramos 3 soluciones principales. Este blogpost te guiará a través de estas posibilidades y las implementaciones finales.

Un comentario personal antes de empezar: es una molestia, ¡así que abróchate el cinturón!

Tabla de Contenidos

  • lado del Cliente o Backend lado?
  • Opción 1: Hacer una captura de pantalla desde el DOM
  • Opción 2: Usar solo una biblioteca PDF
  • Opción final 3: Titiritero, Chrome sin cabeza con Nodo.js
    • Manipulación de estilo
    • Enviar archivo al cliente y guardarlo
  • Usando Puppeteer con Docker
  • Opción 3 + 1: Reglas de impresión CSS
  • Resumen

Lado del cliente o del servidor?

Es posible generar un archivo PDF tanto en el lado del cliente como en el lado del servidor. Sin embargo, probablemente tenga más sentido dejar que el backend lo maneje, ya que no desea usar todos los recursos que el navegador del usuario puede ofrecer.

Aún así, seguiré mostrando soluciones para ambos métodos.

Opción 1: Hacer una captura de pantalla desde el DOM

A primera vista, esta solución parecía ser la más simple, y resultó ser cierta, pero tiene sus propias limitaciones. Si no tiene necesidades especiales, como texto seleccionable o de búsqueda en el PDF, es una forma buena y sencilla de generar uno.

Este método es sencillo y sencillo: crea una captura de pantalla de la página y ponla en un archivo PDF. Bastante sencillo. Utilizamos dos paquetes para este enfoque:

Html2canvas, para hacer una captura de pantalla del DOM
jsPDF, una biblioteca para generar PDF

Comencemos a codificar.

npm install html2canvas jspdf

import html2canvas from 'html2canvas'import jsPdf from 'jspdf' function printPDF () { const domElement = document.getElementById('your-id') html2canvas(domElement, { onclone: (document) => { document.getElementById('print-button').style.visibility = 'hidden'}}) .then((canvas) => { const img = canvas.toDataURL('image/png') const pdf = new jsPdf() pdf.addImage(imgData, 'JPEG', 0, 0, width, height) pdf.save('your-filename.pdf')})

Y eso es todo!

Asegúrese de echar un vistazo al métodohtml2canvasonclone. Puede resultar útil cuando necesita tomar rápidamente una instantánea y manipular el DOM (por ejemplo, ocultar el botón de impresión) antes de tomar la foto. Puedo ver muchos casos de uso para este paquete. Desafortunadamente, el nuestro no era uno, ya que necesitábamos manejar la creación de PDF en el lado de fondo.

Opción 2: Use solo una biblioteca PDF

Hay varias bibliotecas en NPM para este propósito, como jsPDF (mencionado anteriormente) o PDFKit. El problema con ellos es que tendría que recrear la estructura de la página de nuevo si quisiera usar estas bibliotecas. Eso definitivamente perjudica la capacidad de mantenimiento, ya que habría necesitado aplicar todos los cambios posteriores tanto a la plantilla PDF como a la página React.

Echa un vistazo al siguiente código. Debe crear el documento PDF usted mismo a mano. Ahora puede recorrer el DOM y descubrir cómo traducir cada elemento a PDF, pero ese es un trabajo tedioso. Debe haber una manera más fácil.

doc = new PDFDocumentdoc.pipe fs.createWriteStream('output.pdf')doc.font('fonts/PalatinoBold.ttf') .fontSize(25) .text('Some text with an embedded font!', 100, 100) doc.image('path/to/image.png', { fit: , align: 'center', valign: 'center'}); doc.addPage() .fontSize(25) .text('Here is some vector graphics...', 100, 100) doc.end()

Este fragmento es de los documentos de PDFKit. Sin embargo, puede ser útil si su destino es un archivo PDF de inmediato y no la conversión de una página HTML ya existente (y en constante cambio).

Última opción 3: Titiritero, Cromo sin cabeza con Nodo.js

¿Qué es Titiritero? La documentación dice:

Puppeteer es una biblioteca de nodos que proporciona una API de alto nivel para controlar Chrome o Chromium sobre el Protocolo DevTools. Puppeteer se ejecuta sin cabeza de forma predeterminada, pero se puede configurar para ejecutar Cromo o Cromo completo (no sin cabeza).

Es básicamente un navegador que puedes ejecutar desde Node.js. Si lees los documentos, lo primero que dice sobre Puppeteer es que puedes usarlo para generar capturas de pantalla y archivos PDF de páginas. ¡Excelente! Eso es lo que estábamos buscando.

Instalemos Puppeteer con npmi i puppeteer, e implementemos nuestro caso de uso.

const puppeteer = require('puppeteer') async function printPDF() { const browser = await puppeteer.launch({ headless: true }); const page = await browser.newPage(); await page.goto('https://blog.risingstack.com', {waitUntil: 'networkidle0'}); const pdf = await page.pdf({ format: 'A4' }); await browser.close(); return pdf})

Esta es una función simple que navega a una URL y genera un archivo PDF del sitio.

Primero, iniciamos el navegador (solo se admite la generación de PDF en modo sin cabeza), luego abrimos una nueva página, configuramos la ventana gráfica y navegamos a la URL proporcionada.

Configurar la opción waitUntil: ‘networkidle0’ significa que Puppeteer considera que la navegación ha finalizado cuando no hay conexiones de red durante al menos 500 ms. (Consulte los documentos de la API para obtener más información.)

Después de eso, guardamos el PDF en una variable, cerramos el navegador y devolvemos el PDF.

Nota: El método page.pdfrecibe un objeto options, donde también puede guardar el archivo en el disco con la opción ‘ruta’. Si no se proporciona la ruta, el PDF no se guardará en el disco, obtendrá un búfer en su lugar. Más tarde, discuto cómo puedes manejarlo.)

En caso de que necesite iniciar sesión primero para generar un PDF desde una página protegida, primero debe navegar a la página de inicio de sesión, inspeccionar los elementos del formulario en busca de ID o nombre, rellenarlos y luego enviar el formulario:

await page.type('#email', process.env.PDF_USER)await page.type('#password', process.env.PDF_PASSWORD)await page.click('#submit')

Almacene siempre las credenciales de inicio de sesión en variables de entorno, ¡no las codifique!

Manipulación de estilos

Puppeteer también tiene una solución para esta manipulación de estilos. Puede insertar etiquetas de estilo antes de generar el PDF, y Puppeteer generará un archivo con los estilos modificados.

await page.addStyleTag({ content: '.nav { display: none} .navbar { border: 0px} #print-button {display: none}' })

Enviar archivo al cliente y guardarlo

Bien, ahora ha generado un archivo PDF en el backend. ¿Qué hacer ahora?

Como mencioné anteriormente, si no guarda el archivo en el disco, obtendrá un búfer. Solo necesita enviar ese búfer con el tipo de contenido adecuado al front-end.

printPDF.then(pdf => {res.set({ 'Content-Type': 'application/pdf', 'Content-Length': pdf.length })res.send(pdf)

Ahora simplemente puede enviar una solicitud al servidor para obtener el PDF generado.

function getPDF() { return axios.get(`${API_URL}/your-pdf-endpoint`, { responseType: 'arraybuffer', headers: { 'Accept': 'application/pdf' } })

Una vez que haya enviado la solicitud, el búfer debería comenzar a descargarse. Ahora el último paso es convertir el búfer en un archivo PDF.

savePDF = () => { this.openModal(‘Loading…’) // open modal return getPDF() // API call .then((response) => { const blob = new Blob(, {type: 'application/pdf'}) const link = document.createElement('a') link.href = window.URL.createObjectURL(blob) link.download = `your-file-name.pdf` link.click() this.closeModal() // close modal }) .catch(err => /** error handling **/) }

<button onClick={this.savePDF}>Save as PDF</button>

Eso fue todo! Si hace clic en el botón guardar, el navegador guardará el PDF.

Usar Puppeteer con Docker

Creo que esta es la parte más complicada de la implementación, así que déjame ahorrarte un par de horas de búsqueda en Google.

La documentación oficial indica que «poner en marcha Chrome sin cabeza en Docker puede ser complicado». Los documentos oficiales tienen una sección de solución de problemas, donde en el momento de escribir este artículo puede encontrar toda la información necesaria sobre la instalación de puppeteer con Docker.

Si instala Puppeteer en la imagen Alpina, asegúrese de desplazarse un poco hacia abajo hasta esta parte de la página. De lo contrario, podría pasar por alto el hecho de que no puede ejecutar la última versión de Puppeteer y también necesita deshabilitar el uso de shm, utilizando una bandera:

const browser = await puppeteer.launch({ headless: true, args: });

De lo contrario, el subproceso de Puppeteer podría quedarse sin memoria incluso antes de que se inicie correctamente. Más información sobre eso en el enlace de solución de problemas de arriba.

Opción 3 + 1: Reglas de impresión CSS

Uno podría pensar que simplemente usar reglas de impresión CSS es fácil desde el punto de vista de los desarrolladores. Sin módulos NPM, solo CSS puro. Pero, ¿cómo les va cuando se trata de compatibilidad entre navegadores?

Al elegir reglas de impresión CSS, debe probar el resultado en cada navegador para asegurarse de que proporciona el mismo diseño, y no es 100% lo que lo hace.

Por ejemplo, insertar un descanso después de un elemento dado no se puede considerar un caso de uso esotérico, sin embargo, puede que te sorprenda que necesites usar soluciones alternativas para que funcione en Firefox.

A menos que seas un mago CSS curtido en la batalla con mucha experiencia en la creación de páginas imprimibles, esto puede llevar mucho tiempo.

Las reglas de impresión son excelentes si puede mantener las hojas de estilo de impresión simples.

Veamos un ejemplo.

@media print { .print-button { display: none; } .content div { break-after: always; }}

Este CSS anterior oculta el botón imprimir e inserta un salto de página después de cada div con la clase content. Hay un gran artículo que resume lo que puede hacer con las reglas de impresión y cuáles son las dificultades con ellas, incluida la compatibilidad con navegadores.

Teniendo todo en cuenta, las reglas de impresión CSS son excelentes y efectivas si desea crear un PDF a partir de una página no tan compleja.

Resumen: PDF desde HTML con nodo.js y Puppeteer

Así que repasemos rápidamente las opciones que cubrimos aquí para generar archivos PDF a partir de páginas HTML:

  • Captura de pantalla del DOM: Esto puede ser útil cuando necesita crear instantáneas de una página (por ejemplo, para crear una miniatura), pero se queda corto cuando tiene muchos datos que manejar.
  • Use solo una biblioteca PDF: Si necesita crear archivos PDF de forma programática desde cero, esta es una solución perfecta. De lo contrario, debe mantener las plantillas HTML y PDF, lo que definitivamente es imposible.
  • Titiritero: A pesar de ser relativamente difícil de conseguir que funcione en Docker, proporcionó el mejor resultado para nuestro caso de uso, y también fue el más fácil de escribir el código con.
  • Reglas de impresión CSS: Si sus usuarios están lo suficientemente educados para saber cómo imprimir en un archivo y sus páginas son relativamente simples, puede ser la solución más indolora. Como viste en nuestro caso, no lo fue.

¡Feliz impresión!

temas Relacionados

Nodo.Tutoriales de js para principiantes / @RisingStack