Articles

genereren van PDF van HTML met Node.js and Puppeteer

Máté Boér ' s Picture's Picture

Máté Boér

Full-Stack Developer at RisingStack

In dit artikel ga ik laten zien hoe u een PDF-document kunt genereren van een sterk gestileerde React page met behulp van Node.js, Puppeteer, headless Chrome & Docker.

Achtergrond: Een paar maanden geleden vroeg een van de klanten van RisingStack ons om een functie te ontwikkelen waar de gebruiker een React-pagina in PDF-formaat zou kunnen aanvragen. Die pagina is eigenlijk een rapport / resultaat voor patiënten met datavisualisatie, met veel SVG ‘ s. Verder waren er een aantal speciale verzoeken om de lay-out te manipuleren en een aantal herschikkingen van de HTML-elementen te maken. Dus de PDF moet verschillende styling en toevoegingen hebben in vergelijking met de originele React pagina.

omdat de opdracht iets complexer was dan wat met eenvoudige CSS-regels had kunnen worden opgelost, hebben we eerst mogelijke implementaties onderzocht. In wezen vonden we 3 hoofdoplossingen. Deze blogpost zal u begeleiden over deze mogelijkheden en de uiteindelijke implementaties.

een persoonlijke opmerking voordat we beginnen: het is nogal een gedoe, dus riemen vast!

inhoudsopgave:

  • Client-of Backend-zijde?
  • Optie 1: Een Screenshot maken van de DOM
  • Optie 2: Gebruik alleen een PDF-bibliotheek
  • laatste optie 3: Puppeteer, Chrome zonder kop met Node.js
    • Stijlmanipulatie
    • bestand naar de client sturen en opslaan
  • Met Puppeteer met Docker
  • optie 3 + 1: CSS afdrukregels
  • samenvatting

Client-of serverzijde?

Het is mogelijk om een PDF-bestand aan te maken, zowel aan de client-kant als aan de server-kant. Het is echter waarschijnlijk zinvoller om de backend het te laten afhandelen, omdat je niet alle middelen wilt gebruiken die de browser van de gebruiker kan bieden.

toch zal ik oplossingen voor beide methoden tonen.

Optie 1: Maak een Screenshot van de DOM

op het eerste gezicht leek deze oplossing de eenvoudigste, en het bleek waar te zijn, maar het heeft zijn eigen beperkingen. Als u geen speciale behoeften, zoals selecteerbare of doorzoekbare tekst in de PDF, het is een goede en eenvoudige manier om een te genereren.

Deze methode is eenvoudig: maak een screenshot van de pagina, en zet het in een PDF-bestand. Vrij eenvoudig. We gebruikten twee pakketten voor deze aanpak:

Html2canvas, om een screenshot te maken van de DOM
jsPdf, een bibliotheek om PDF

te genereren laten we beginnen met coderen.

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

en dat is het!

kijk naar de methode html2canvasonclone. Het kan handig blijken te zijn wanneer u snel een snapshot moet maken en de DOM moet manipuleren (bijvoorbeeld de printknop verbergen) voordat u de foto neemt. Ik zie heel wat use cases voor dit pakket. Helaas, de Onze was niet een, als we nodig hadden om de PDF-creatie aan de backend kant te behandelen.

Optie 2: Gebruik alleen een PDF-bibliotheek

Er zijn verschillende bibliotheken op NPM voor dit doel, zoals jsPDF (hierboven vermeld) of PDFKit. Het probleem met hen dat ik de paginastructuur opnieuw zou moeten maken als ik deze bibliotheken zou willen gebruiken. Dat doet zeker pijn onderhoudbaarheid, als ik nodig zou hebben om alle volgende wijzigingen toe te passen op zowel de PDF-sjabloon en de React pagina.

bekijk de onderstaande code. U moet het PDF-document zelf met de hand maken. Nu kunt u de DOM doorkruisen en uitzoeken hoe u elk element kunt vertalen naar PDF-elementen, maar dat is een vervelende taak. Er moet een makkelijkere manier zijn.

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

dit fragment komt uit de pdfkit docs. Echter, het kan handig zijn als uw doel is een PDF-bestand meteen en niet de conversie van een reeds bestaande (en steeds veranderende) HTML-pagina.

laatste optie 3: Puppeteer, Headless Chrome met Node.js

Wat is poppenspeler? De documentatie zegt:

Puppeteer is een Node bibliotheek die een API op hoog niveau biedt om Chrome of Chromium over het DevTools Protocol te besturen. Puppeteer draait standaard headless, maar kan worden geconfigureerd om volledige (niet-headless) Chrome of Chromium draaien.

Het is in principe een browser die u kunt uitvoeren vanaf Node.js. Als je de documenten leest, is het eerste wat het zegt over Puppeteer dat je het kunt gebruiken om screenshots en PDF’s van pagina’ s te genereren. Uitstekend! Daar waren we naar op zoek.

laten we Puppeteer installeren met npmi i puppeteer, en onze use case implementeren.

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

Dit is een eenvoudige functie die naar een URL navigeert en een PDF-bestand van de site genereert.

eerst starten we de browser (PDF-generatie wordt alleen ondersteund in headless-modus), dan openen we een nieuwe pagina, stellen de viewport in en navigeren naar de opgegeven URL.

Het instellen van de waitUntil: ‘networkidle0’ optie betekent dat Puppeteer van mening is dat navigatie is voltooid wanneer er geen netwerkverbindingen zijn voor minstens 500 ms. (Check API docs voor meer informatie.)

daarna slaan we de PDF op in een variabele, sluiten we de browser en retourneren de PDF.

opmerking: De methodepage.pdfontvangt een options object, waar u het bestand ook op schijf kunt opslaan met de optie ‘pad’. Als het pad niet is opgegeven, wordt de PDF niet opgeslagen op de schijf, in plaats daarvan krijgt u een buffer. Later bespreek ik hoe je het aankunt.)

in het geval dat u eerst moet inloggen om een PDF te genereren van een beveiligde pagina, moet u eerst naar de login pagina navigeren, de formulierelementen inspecteren op ID of naam, deze invullen en vervolgens het formulier verzenden:

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

sla aanmeldingsreferenties altijd op in omgevingsvariabelen, niet hardcoderen!

Stijlmanipulatie

Puppeteer heeft ook een oplossing voor deze stijlmanipulatie. U kunt stijltags invoegen voordat u de PDF genereert, en Puppeteer zal een bestand met de gewijzigde stijlen genereren.

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

stuur bestand naar de client en sla het op

Ok, nu hebt u een PDF-bestand aangemaakt op de backend. Wat nu te doen?

zoals ik hierboven al zei, als je het bestand niet op schijf opslaat, krijg je een buffer. Je hoeft alleen die buffer met de juiste inhoudstype naar de front-end te sturen.

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

nu kunt u eenvoudig een verzoek naar de server sturen om de gegenereerde PDF te krijgen.

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

zodra u het verzoek hebt verzonden, moet de buffer beginnen met downloaden. Nu is de laatste stap om de buffer om te zetten in een PDF-bestand.

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>

dat was het! Als u op de knop Opslaan klikt, wordt de PDF opgeslagen door de browser.

Puppeteer gebruiken met Docker

Ik denk dat dit het lastigste deel van de implementatie is – dus laat me je een paar uur googlen besparen.

de officiële documentatie stelt dat “Headless Chrome opstarten en draaien in Docker lastig kan zijn”. De officiële documenten hebben een probleemoplossing sectie, waar je op het moment van schrijven alle benodigde informatie kunt vinden over het installeren van puppeteer met Docker.

als je Puppeteer installeert op de alpine afbeelding, zorg er dan voor dat je een beetje naar beneden scrolt naar dit deel van de pagina. Anders zou je het feit kunnen verdoezelen dat je de laatste Puppeteer versie niet kunt draaien en je moet ook shm gebruik uitschakelen met een vlag:

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

anders zou het Puppeteer sub proces zonder geheugen kunnen zitten voordat het zelfs maar goed gestart wordt. Meer info hierover op de link probleemoplossing hierboven.

optie 3 + 1: CSS-Afdrukregels

men zou kunnen denken dat het eenvoudig gebruiken van CSS-afdrukregels vanuit een ontwikkelaarsstandpunt eenvoudig is. Geen NPM modules, gewoon pure CSS. Maar hoe doen ze het als het gaat om cross-browser compatibiliteit?

wanneer u CSS-afdrukregels kiest, moet u het resultaat in elke browser testen om er zeker van te zijn dat het dezelfde lay-out heeft, en het is niet 100% dat het doet.

bijvoorbeeld, het invoegen van een pauze na een bepaald element kan niet worden beschouwd als een esoterische use case, maar het kan je verbazen dat je tijdelijke oplossingen moet gebruiken om dat te laten werken in Firefox.

tenzij u een geharde CSS-goochelaar bent met veel ervaring in het maken van afdrukbare pagina ‘ s, kan dit tijdrovend zijn.

Afdrukregels zijn geweldig als u de afdrukstylesheets eenvoudig kunt houden.

laat een voorbeeld zien.

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

Deze CSS hierboven verbergt de afdrukknop, en voegt een pagina-einde in na elke div met de klasse content. er is een geweldig artikel dat een samenvatting geeft van wat u kunt doen met afdrukregels, en wat de problemen zijn met deze regels, inclusief browsercompatibiliteit.

rekening houdend met alles, zijn CSS-afdrukregels geweldig en effectief als u een PDF wilt maken van een niet zo complexe pagina.

samenvatting: PDF van HTML met Node.js en Puppeteer

dus laten we snel gaan door de opties die we hier behandeld voor het genereren van PDF-bestanden van HTML-pagina ‘ s:

  • Screenshot van de DOM: dit kan handig zijn wanneer u snapshots van een pagina moet maken (bijvoorbeeld om een thumbnail te maken), maar schiet tekort wanneer u veel gegevens te verwerken hebt.
  • gebruik alleen een PDF-bibliotheek: als u PDF-bestanden programmatisch vanuit het niets moet maken, is dit een perfecte oplossing. Anders, je nodig hebt om de HTML en PDF templates die is zeker een no-go te behouden.
  • poppenspeler: Ondanks dat het relatief moeilijk was om het aan het werk te krijgen op Docker, bood het het beste resultaat voor onze use case, en het was ook het makkelijkst om de code mee te schrijven.
  • CSS-afdrukregels: als uw gebruikers voldoende zijn opgeleid om te weten hoe ze naar een bestand moeten afdrukken en uw pagina ‘ s relatief eenvoudig zijn, kan dit de meest pijnloze oplossing zijn. Zoals je in ons geval zag, was het niet.

Gelukkig afdrukken!

gerelateerde onderwerpen

knooppunt.js Tutorials voor Beginners / @RisingStack