Articles

Generieren von PDF aus HTML mit Node.js und Puppenspieler

Máté Boérs Bild's Picture

Máté Boér

Full-Stack-Entwickler bei RisingStack

In diesem Artikel werde ich zeigen, wie Sie mit Node ein PDF-Dokument aus einer stark gestalteten React-Seite generieren können.js, Puppenspieler, kopfloses Chrom & Docker.

Hintergrund: Vor einigen Monaten hat uns einer der Kunden von RisingStack gebeten, eine Funktion zu entwickeln, mit der der Benutzer eine React-Seite im PDF-Format anfordern kann. Diese Seite ist im Grunde ein Bericht / Ergebnis für Patienten mit Datenvisualisierung, der viele SVGs enthält. Darüber hinaus gab es einige spezielle Anfragen, um das Layout zu manipulieren und einige Neuordnungen der HTML-Elemente vorzunehmen. Daher sollte das PDF im Vergleich zur ursprünglichen React-Seite ein anderes Styling und andere Ergänzungen aufweisen.

Da die Zuordnung etwas komplexer war, als mit einfachen CSS-Regeln hätte gelöst werden können, untersuchten wir zunächst mögliche Implementierungen. Im Wesentlichen haben wir 3 Hauptlösungen gefunden. Dieser Blogbeitrag führt Sie durch diese Möglichkeiten und die endgültigen Implementierungen.

Ein persönlicher Kommentar, bevor wir loslegen: Es ist ziemlich mühsam, also schnall dich an!

Inhaltsverzeichnis:

  • Client-Seite oder Backend-Seite?
  • Option 1: Erstellen eines Screenshots aus dem DOM
  • Option 2: Verwenden Sie nur eine PDF-Bibliothek
  • Letzte Option 3: Puppenspieler, kopfloses Chrome mit Knoten.js
    • Stilmanipulation
    • Datei an den Client senden und speichern
  • Puppenspieler mit Docker verwenden
  • Option 3 +1: CSS-Druckregeln
  • Zusammenfassung

Clientseitig oder Serverseitig?

Es ist möglich, eine PDF-Datei sowohl clientseitig als auch serverseitig zu generieren. Es ist jedoch wahrscheinlich sinnvoller, das Backend damit umgehen zu lassen, da Sie nicht alle Ressourcen verbrauchen möchten, die der Browser des Benutzers bieten kann.

Trotzdem werde ich immer noch Lösungen für beide Methoden zeigen.

Option 1: Machen Sie einen Screenshot aus dem DOM

Auf den ersten Blick schien diese Lösung die einfachste zu sein, und es stellte sich heraus, dass sie wahr ist, aber sie hat ihre eigenen Einschränkungen. Wenn Sie keine besonderen Anforderungen haben, z. B. auswählbaren oder durchsuchbaren Text in der PDF-Datei, ist dies eine gute und einfache Möglichkeit, einen zu generieren.

Diese Methode ist schlicht und einfach: Erstellen Sie einen Screenshot von der Seite und fügen Sie ihn in eine PDF-Datei ein. Ziemlich einfach. Wir haben zwei Pakete für diesen Ansatz verwendet:

Html2canvas, um einen Screenshot aus dem DOM zu machen
jsPDF, eine Bibliothek zum Generieren von PDF

Beginnen wir mit dem Codieren.

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

Und das war’s!

Stellen Sie sicher, dass Sie sich die html2canvasonclone -Methode ansehen. Es kann sich als praktisch erweisen, wenn Sie schnell einen Schnappschuss machen und das DOM manipulieren müssen (z. B. die Druckschaltfläche ausblenden), bevor Sie das Bild aufnehmen. Ich kann ziemlich viele Anwendungsfälle für dieses Paket sehen. Leider war unsere keine, da wir die PDF-Erstellung auf der Backend-Seite übernehmen mussten.

Möglichkeit 2: Verwenden Sie nur eine PDF-Bibliothek

Es gibt mehrere Bibliotheken auf NPM für diesen Zweck, wie jsPDF (oben erwähnt) oder PDFKit. Das Problem dabei ist, dass ich die Seitenstruktur erneut erstellen müsste, wenn ich diese Bibliotheken verwenden möchte. Das schadet definitiv der Wartbarkeit, da ich alle nachfolgenden Änderungen sowohl auf die PDF-Vorlage als auch auf die Reaktionsseite hätte anwenden müssen.

Schauen Sie sich den Code unten an. Sie müssen das PDF-Dokument selbst von Hand erstellen. Jetzt könnten Sie das DOM durchlaufen und herausfinden, wie jedes Element in PDF-Elemente übersetzt wird, aber das ist eine mühsame Aufgabe. Es muss einen einfacheren Weg geben.

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

Dieses Snippet stammt aus den PDFKit-Dokumenten. Es kann jedoch nützlich sein, wenn Ihr Ziel sofort eine PDF-Datei ist und nicht die Konvertierung einer bereits vorhandenen (und sich ständig ändernden) HTML-Seite.

Letzte Option 3: Puppenspieler, kopflos, mit Knoten.js

Was ist Puppenspieler? In der Dokumentation heißt es:

Puppeteer ist eine Knotenbibliothek, die eine High-Level-API zur Steuerung von Chrome oder Chromium über das DevTools-Protokoll bereitstellt. Puppeteer läuft standardmäßig kopflos, kann aber so konfiguriert werden, dass Chrome oder Chromium vollständig (nicht kopflos) ausgeführt wird.

Es ist im Grunde ein Browser, den Sie von Node aus ausführen können.js. Wenn Sie die Dokumente lesen, ist das erste, was über Puppeteer gesagt wird, dass Sie damit Screenshots und PDFs von Seiten generieren können. Ausgezeichnet! Das haben wir gesucht.

Installieren wir Puppeteer mit npmi i puppeteer und implementieren unseren Anwendungsfall.

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

Dies ist eine einfache Funktion, die zu einer URL navigiert und eine PDF-Datei der Site generiert.

Zuerst starten wir den Browser (PDF-Generierung nur im Headless-Modus unterstützt), dann öffnen wir eine neue Seite, stellen das Ansichtsfenster ein und navigieren zur angegebenen URL.

Das Setzen der waitUntil: ‘networkidle0’ Option bedeutet, dass Puppeteer die Navigation als beendet betrachtet, wenn für mindestens 500 ms keine Netzwerkverbindungen vorhanden sind.)

Danach speichern wir das PDF in einer Variablen, schließen den Browser und geben das PDF zurück.

Hinweis: Die page.pdf -Methode empfängt ein options -Objekt, in dem Sie die Datei auch mit der Option „path“ auf der Festplatte speichern können. Wenn kein Pfad angegeben wird, wird die PDF-Datei nicht auf der Festplatte gespeichert. Später diskutiere ich, wie Sie damit umgehen können.)

Falls Sie sich zuerst anmelden müssen, um eine PDF-Datei von einer geschützten Seite zu generieren, müssen Sie zuerst zur Anmeldeseite navigieren, die Formularelemente auf ID oder Namen überprüfen, ausfüllen und dann das Formular absenden:

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

Speichern Sie Anmeldeinformationen immer in Umgebungsvariablen, codieren Sie sie nicht fest!

Stilmanipulation

Puppeteer hat auch eine Lösung für diese Stilmanipulation. Sie können Stil-Tags einfügen, bevor Sie die PDF-Datei generieren, und Puppeteer generiert eine Datei mit den geänderten Stilen.

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

Datei an den Client senden und speichern

Okay, jetzt haben Sie eine PDF-Datei im Backend generiert. Was jetzt zu tun?

Wie oben erwähnt, erhalten Sie einen Puffer, wenn Sie die Datei nicht auf der Festplatte speichern. Sie müssen nur diesen Puffer mit dem richtigen Inhaltstyp an das Front-End senden.

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

Jetzt können Sie einfach eine Anfrage an den Server senden, um das generierte PDF zu erhalten.

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

Sobald Sie die Anfrage gesendet haben, sollte der Puffer heruntergeladen werden. Der letzte Schritt besteht nun darin, den Puffer in eine PDF-Datei zu konvertieren.

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>

Das war’s! Wenn Sie auf die Schaltfläche Speichern klicken, wird das PDF vom Browser gespeichert.

Puppenspieler mit Docker verwenden

Ich denke, das ist der schwierigste Teil der Implementierung – also lass mich dir ein paar Stunden Googeln ersparen.

In der offiziellen Dokumentation heißt es, dass „es schwierig sein kann, kopfloses Chrome in Docker zum Laufen zu bringen“. Die offiziellen Dokumente enthalten einen Abschnitt zur Fehlerbehebung, in dem Sie zum Zeitpunkt des Schreibens alle erforderlichen Informationen zur Installation von Puppeteer mit Docker finden.

Wenn Sie Puppeteer auf dem Alpine-Image installieren, scrollen Sie ein wenig nach unten zu diesem Teil der Seite. Andernfalls könnten Sie die Tatsache beschönigen, dass Sie die neueste Puppeteer-Version nicht ausführen können, und Sie müssen auch die shm-Verwendung mit einem Flag deaktivieren:

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

Andernfalls könnte dem Puppeteer-Unterprozess der Speicher ausgehen, bevor er überhaupt richtig gestartet wird. Weitere Informationen dazu finden Sie im obigen Link zur Fehlerbehebung.

Option 3 + 1: CSS-Druckregeln

Man könnte meinen, dass die einfache Verwendung von CSS-Druckregeln aus Sicht der Entwickler einfach ist. Keine NPM-Module, nur reines CSS. Aber wie geht es ihnen, wenn es um Cross-Browser-Kompatibilität geht?

Wenn Sie CSS-Druckregeln auswählen, müssen Sie das Ergebnis in jedem Browser testen, um sicherzustellen, dass es das gleiche Layout bietet, und es ist nicht 100%, dass es funktioniert.

Das Einfügen eines Umbruchs nach einem bestimmten Element kann beispielsweise nicht als esoterischer Anwendungsfall angesehen werden, aber Sie werden überrascht sein, dass Sie Problemumgehungen verwenden müssen, damit dies in Firefox funktioniert.

Wenn Sie kein kampferprobter CSS-Magier mit viel Erfahrung in der Erstellung druckbarer Seiten sind, kann dies zeitaufwändig sein.

Druckregeln sind großartig, wenn Sie die Druckstylesheets einfach halten können.

Sehen wir uns ein Beispiel an.

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

Dieses CSS oben verbirgt die Druckschaltfläche und fügt nach jedem div mit der Klasse content. Einen Seitenumbruch ein Es gibt einen großartigen Artikel, der zusammenfasst, was Sie mit Druckregeln tun können und welche Schwierigkeiten sie haben, einschließlich der Browserkompatibilität.

Wenn Sie alles berücksichtigen, sind CSS-Druckregeln großartig und effektiv, wenn Sie eine PDF-Datei aus einer nicht so komplexen Seite erstellen möchten.

Zusammenfassung: PDF aus HTML mit Knoten.js und Puppenspieler

Lassen Sie uns also schnell die hier behandelten Optionen zum Generieren von PDF-Dateien aus HTML-Seiten durchgehen:

  • Screenshot aus dem DOM: Dies kann nützlich sein, wenn Sie Schnappschüsse von einer Seite erstellen müssen (z. B. um eine Miniaturansicht zu erstellen), reicht jedoch nicht aus, wenn Sie viele Daten verarbeiten müssen.
  • Verwenden Sie nur eine PDF-Bibliothek: Wenn Sie PDF-Dateien programmgesteuert von Grund auf neu erstellen müssen, ist dies eine perfekte Lösung. Andernfalls müssen Sie die HTML- und PDF-Vorlagen pflegen, was definitiv ein No-Go ist.
  • Puppenspieler: Obwohl es relativ schwierig war, es auf Docker zum Laufen zu bringen, lieferte es das beste Ergebnis für unseren Anwendungsfall und es war auch am einfachsten, den Code damit zu schreiben.
  • CSS-Druckregeln: Wenn Ihre Benutzer gut genug ausgebildet sind, um zu wissen, wie man in eine Datei druckt, und Ihre Seiten relativ einfach sind, kann dies die schmerzloseste Lösung sein. Wie Sie in unserem Fall gesehen haben, war es nicht so.

Viel Spaß beim Drucken!

Verwandte Themen

Knoten.js Tutorials für Anfänger / @RisingStack