Articles

Generazione di PDF da HTML con Nodo.js e Burattinaio

Máté Boér Immagine's Picture

Máté Boér

Full-Stack Developer presso RisingStack

In questo articolo ho intenzione di mostrare come è possibile generare un documento PDF da una pesantemente in stile Reagire pagina utilizzando il Nodo.js, Burattinaio, Chrome senza testa & Finestra mobile.

Sfondo: Alcuni mesi fa uno dei clienti di RisingStack ci ha chiesto di sviluppare una funzionalità in cui l’utente potesse richiedere una pagina React in formato PDF. Quella pagina è fondamentalmente un rapporto / risultato per i pazienti con visualizzazione dei dati, contenente molti SVG. Inoltre, ci sono state alcune richieste speciali per manipolare il layout e fare alcuni riarrangiamenti degli elementi HTML. Quindi il PDF dovrebbe avere uno stile e aggiunte diversi rispetto alla pagina React originale.

Poiché l’assegnazione era un po ‘ più complessa di quella che avrebbe potuto essere risolta con semplici regole CSS, abbiamo prima esplorato possibili implementazioni. Essenzialmente abbiamo trovato 3 soluzioni principali. Questo blogpost ti guiderà attraverso queste possibilità e le implementazioni finali.

Un commento personale prima di iniziare: è piuttosto una seccatura, quindi allacciate le cinture!

Sommario:

  • Lato client o lato backend?
  • Opzione 1: Fare uno screenshot dal DOM
  • Opzione 2: Utilizzare solo una libreria PDF
  • Opzione finale 3: Burattinaio, Chrome senza testa con Nodo.js
    • Manipolazione di stile
    • Invia file al client e salvalo
  • Usando Puppeteer con Docker
  • Opzione 3 +1: Regole di stampa CSS
  • Riepilogo

Lato client o lato server?

È possibile generare un file PDF sia sul lato client che sul lato server. Tuttavia, probabilmente ha più senso lasciare che il backend lo gestisca, poiché non si desidera utilizzare tutte le risorse che il browser dell’utente può offrire.

Anche così, mostrerò ancora soluzioni per entrambi i metodi.

Opzione 1: Crea uno Screenshot dal DOM

A prima vista, questa soluzione sembrava essere la più semplice, e si è rivelata vera, ma ha i suoi limiti. Se non hai esigenze particolari, come il testo selezionabile o ricercabile nel PDF, è un modo buono e semplice per generarne uno.

Questo metodo è semplice e semplice: crea uno screenshot dalla pagina e inseriscilo in un file PDF. Piuttosto semplice. Abbiamo usato due pacchetti per questo approccio:

Html2canvas, per fare uno screenshot dal DOM
jsPdf, una libreria per generare PDF

Iniziamo la codifica.

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')})

E il gioco è fatto!

Assicurati di dare un’occhiata al metodohtml2canvasonclone. Può rivelarsi utile quando è necessario scattare rapidamente un’istantanea e manipolare il DOM (ad esempio nascondere il pulsante di stampa) prima di scattare la foto. Posso vedere un sacco di casi d’uso per questo pacchetto. Sfortunatamente, il nostro non era uno, in quanto avevamo bisogno di gestire la creazione di PDF sul lato back-end.

Opzione 2: Usa solo una libreria PDF

Ci sono diverse librerie là fuori su NPM per questo scopo, come jsPDF (menzionato sopra) o PDFKit. Il problema con loro che dovrei ricreare di nuovo la struttura della pagina se volessi usare queste librerie. Ciò danneggia sicuramente la manutenibilità, poiché avrei dovuto applicare tutte le modifiche successive sia al modello PDF che alla pagina React.

Dai un’occhiata al codice qui sotto. È necessario creare il documento PDF manualmente. Ora potresti attraversare il DOM e capire come tradurre ogni elemento in PDF, ma questo è un lavoro noioso. Ci deve essere un modo più semplice.

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()

Questo frammento proviene dai documenti PDFKit. Tuttavia, può essere utile se il tuo obiettivo è un file PDF immediatamente e non la conversione di una pagina HTML già esistente (e in continua evoluzione).

Opzione finale 3: burattinaio, Chrome senza testa con nodo.js

Che cos’è il Burattinaio? La documentazione dice:

Puppeteer è una libreria di nodi che fornisce un’API di alto livello per controllare Chrome o Chromium sul protocollo DevTools. Puppeteer viene eseguito senza testa per impostazione predefinita, ma può essere configurato per l’esecuzione completa (non senza testa) Chrome o Chromium.

È fondamentalmente un browser che puoi eseguire dal nodo.js. Se leggete i documenti, la prima cosa che dice su Puppeteer è che è possibile utilizzarlo per generare screenshot e PDF di pagine’. Eccellente! E ‘ quello che stavamo cercando.

Installiamo Puppeteer con npmi i puppeteer e implementiamo il nostro caso d’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})

Questa è una semplice funzione che naviga verso un URL e genera un file PDF del sito.

Per prima cosa, lanciamo il browser (generazione PDF supportata solo in modalità headless), quindi apriamo una nuova pagina, impostiamo la finestra e navighiamo verso l’URL fornito.

L’impostazione dell’opzionewaitUntil: ‘networkidle0’ significa che Puppeteer considera terminata la navigazione quando non ci sono connessioni di rete per almeno 500 ms. (Controllare i documenti API per ulteriori informazioni.)

Dopo di che, salviamo il PDF in una variabile, chiudiamo il browser e restituiamo il PDF.

Nota: Il metodopage.pdfriceve un oggettooptions, in cui è possibile salvare il file su disco anche con l’opzione ‘path’. Se il percorso non viene fornito, il PDF non verrà salvato sul disco, si otterrà invece un buffer. Più tardi, parlerò di come puoi gestirlo.)

Nel caso in cui sia necessario accedere prima per generare un PDF da una pagina protetta, per prima cosa è necessario passare alla pagina di accesso, ispezionare gli elementi del modulo per ID o nome, compilarli, quindi inviare il modulo:

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

Memorizza sempre le credenziali di accesso nelle variabili di ambiente, non codificarle!

Manipolazione di stile

Puppeteer ha una soluzione anche per questa manipolazione di stile. È possibile inserire tag di stile prima di generare il PDF, e Puppeteer genererà un file con gli stili modificati.

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

Invia file al client e salvalo

Ok, ora hai generato un file PDF sul backend. Cosa fare ora?

Come ho detto sopra, se non si salva il file su disco, si otterrà un buffer. Hai solo bisogno di inviare quel buffer con il tipo di contenuto corretto al front-end.

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

Ora puoi semplicemente inviare una richiesta al server, per ottenere il PDF generato.

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

Una volta inviata la richiesta, il buffer dovrebbe iniziare il download. Ora l’ultimo passo è convertire il buffer in un file 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>

Che era! Se si fa clic sul pulsante Salva, il PDF verrà salvato dal browser.

Usando Puppeteer con Docker

Penso che questa sia la parte più complicata dell’implementazione, quindi lascia che ti risparmi un paio d’ore di Googling.

La documentazione ufficiale afferma che “ottenere Chrome headless installato e funzionante in Docker può essere complicato”. I documenti ufficiali hanno una sezione di risoluzione dei problemi, dove al momento della scrittura è possibile trovare tutte le informazioni necessarie sull’installazione di puppeteer con Docker.

Se installi Puppeteer sull’immagine Alpine, assicurati di scorrere un po ‘ verso il basso fino a questa parte della pagina. Altrimenti, potresti ignorare il fatto che non puoi eseguire l’ultima versione di Puppeteer e devi anche disabilitare l’utilizzo di shm, usando un flag:

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

Altrimenti, il sottoprocesso Puppeteer potrebbe esaurire la memoria prima ancora che venga avviato correttamente. Maggiori informazioni su questo sul link di risoluzione dei problemi qui sopra.

Opzione 3 + 1: Regole di stampa CSS

Si potrebbe pensare che semplicemente usare le regole di stampa CSS sia facile dal punto di vista degli sviluppatori. Nessun modulo NPM, solo puro CSS. Ma come se la passano quando si tratta di compatibilità cross-browser?

Quando si scelgono le regole di stampa CSS, è necessario testare il risultato in ogni browser per assicurarsi che fornisca lo stesso layout e non è al 100% che lo faccia.

Ad esempio, l’inserimento di un’interruzione dopo un dato elemento non può essere considerato un caso d’uso esoterico, tuttavia potresti essere sorpreso dal fatto che sia necessario utilizzare soluzioni alternative per farlo funzionare in Firefox.

A meno che tu non sia un mago CSS temprato con molta esperienza nella creazione di pagine stampabili, questo può richiedere molto tempo.

Le regole di stampa sono ottime se riesci a mantenere i fogli di stile di stampa semplici.

Vediamo un esempio.

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

Questo CSS sopra nasconde il pulsante di stampa, e inserisce un’interruzione di pagina dopo ogni div con la classe content. C’è un ottimo articolo che riassume ciò che si può fare con la stampa di regole, e quali sono le difficoltà con loro, tra cui la compatibilità con i browser.

Tenendo conto di tutto, le regole di stampa CSS sono ottime ed efficaci se vuoi creare un PDF da una pagina non così complessa.

Sommario: PDF da HTML con Nodo.js e Puppeteer

Quindi passiamo rapidamente attraverso le opzioni che abbiamo trattato qui per la generazione di file PDF da pagine HTML:

  • Screenshot dal DOM: Questo può essere utile quando è necessario creare istantanee da una pagina (ad esempio per creare una miniatura), ma non è sufficiente quando si hanno molti dati da gestire.
  • Usa solo una libreria PDF: se hai bisogno di creare file PDF a livello di codice da zero, questa è una soluzione perfetta. In caso contrario, è necessario mantenere i modelli HTML e PDF che è sicuramente un no-go.
  • Burattinaio: Nonostante sia relativamente difficile farlo funzionare su Docker, ha fornito il miglior risultato per il nostro caso d’uso, ed è stato anche il più facile da scrivere il codice con.
  • Regole di stampa CSS: se i tuoi utenti sono abbastanza istruiti da sapere come stampare su un file e le tue pagine sono relativamente semplici, può essere la soluzione più indolore. Come hai visto nel nostro caso, non lo era.

Stampa felice!

Argomenti correlati

Nodo.js Tutorial per principianti / @ RisingStack