Articles

Generowanie PDF z HTML z węzłem.js i Puppeteer

Zdjęcie Máté Boér's Picture

Máté Boér

Full-Stack Developer at RisingStack

w tym artykule pokażę, jak można wygenerować dokument PDF z mocno wystylizowanej strony Reactowej za pomocą węzła.js, Puppeteer, Headless Chrome & Docker.

Informacje ogólne: Kilka miesięcy temu jeden z klientów RisingStack poprosił nas o stworzenie funkcji, dzięki której użytkownik będzie mógł zażądać Reactowej strony w formacie PDF. Ta strona jest w zasadzie raportem / wynikiem dla pacjentów z wizualizacją danych, zawierającym wiele SVG. Ponadto pojawiły się Specjalne prośby o manipulowanie układem i dokonanie pewnych zmian w elementach HTML. Tak więc plik PDF powinien mieć inną stylizację i dodatki w porównaniu z oryginalną stroną Reacta.

ponieważ zadanie było nieco bardziej złożone niż to, co można było rozwiązać prostymi regułami CSS, najpierw zbadaliśmy możliwe implementacje. Zasadniczo znaleźliśmy 3 główne rozwiązania. Ten blogpost przeprowadzi Cię przez te możliwości i ostateczne wdrożenia.

osobisty komentarz zanim zaczniemy: to dość kłopotliwe, więc zapnijcie pasy!

spis treści:

  • strona klienta czy Backend?
  • opcja 1: wykonanie zrzutu ekranu z DOM
  • opcja 2: Użyj tylko biblioteki PDF
  • ostatnia opcja 3: Puppeteer, Bezgłowy Chrome z węzłem.js
    • manipulacja stylem
    • Wyślij plik do klienta i zapisz go
  • używając Puppeteer z Dockerem
  • opcja 3 +1: CSS print rules
  • podsumowanie

po stronie klienta czy serwera?

możliwe jest wygenerowanie pliku PDF zarówno po stronie klienta, jak i po stronie serwera. Jednak prawdopodobnie bardziej sensowne jest, aby zaplecze to obsłużyło, ponieważ nie chcesz wykorzystywać wszystkich zasobów, które może zaoferować przeglądarka użytkownika.

mimo to pokażę rozwiązania dla obu metod.

Wariant 1: Zrób zrzut ekranu z DOM

na pierwszy rzut oka to rozwiązanie wydawało się najprostsze i okazało się prawdziwe, ale ma swoje ograniczenia. Jeśli nie masz specjalnych potrzeb, takich jak wybierany lub przeszukiwany tekst w pliku PDF, jest to dobry i prosty sposób na wygenerowanie go.

ta metoda jest prosta i prosta: Utwórz zrzut ekranu ze strony i umieść go w pliku PDF. Całkiem proste. Użyliśmy dwóch pakietów do tego podejścia:

Html2canvas, aby zrobić zrzut ekranu z DOM
jsPdf, biblioteki do generowania PDF

zacznijmy kodować.

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

i tyle!

zapoznaj się z metodąhtml2canvasonclone. Może okazać się przydatny, gdy trzeba szybko zrobić migawkę i manipulować DOM (np. ukryć przycisk drukowania) przed zrobieniem zdjęcia. Widzę sporo przypadków użycia tego pakietu. Niestety, nasz nie był jednym z nich,ponieważ musieliśmy zająć się tworzeniem plików PDF po stronie zaplecza.

Wariant 2: Użyj tylko biblioteki PDF

istnieje kilka bibliotek na NPM do tego celu, jak jspdf (wspomniany powyżej) lub PDFKit. Problem z nimi, że musiałbym ponownie odtworzyć strukturę strony, gdybym chciał korzystać z tych bibliotek. To zdecydowanie szkodzi utrzymaniu, ponieważ musiałbym zastosować wszystkie późniejsze zmiany zarówno do szablonu PDF, jak i strony Reactowej.

spójrz na poniższy kod. Musisz samodzielnie utworzyć dokument PDF. Teraz możesz przemierzać DOM i dowiedzieć się, jak przetłumaczyć każdy element na PDF, ale to jest żmudna praca. Musi być łatwiejszy sposób.

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

Ten fragment pochodzi z dokumentów PDFKit. Jednak może to być przydatne, jeśli twoim celem jest plik PDF od razu, a nie konwersja już istniejącej (i ciągle zmieniającej się) strony HTML.

ostatnia opcja 3: Puppeteer, Bezgłowy chrom z węzłem.js

Co to jest Puppeteer? Dokumentacja mówi:

uppeteer jest biblioteką węzłów, która zapewnia wysoki poziom API do sterowania Chrome lub Chromium poprzez protokół DevTools. Puppeteer domyślnie działa bezgłowy, ale może być skonfigurowany tak, aby działał w pełnym (bezgłowym) Chrome lub Chromium.

jest to zasadniczo przeglądarka, którą można uruchomić z węzła.js. Jeśli czytasz dokumenty, pierwszą rzeczą, o której mówi Puppeteer, jest to, że możesz go używać do generowania zrzutów ekranu i plików PDF stron”. Doskonale! Tego szukaliśmy.

zainstalujmy Puppeteer z npmi i puppeteer I zaimplementujmy nasz przypadek użycia.

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

jest to prosta funkcja, która przechodzi do adresu URL i generuje plik PDF witryny.

najpierw uruchamiamy przeglądarkę (Generowanie PDF obsługiwane tylko w trybie bezgłowym), następnie otwieramy nową stronę, ustawiamy viewport i przechodzimy do podanego adresu URL.

ustawienie opcjiwaitUntil: ‘networkidle0’ oznacza, że Puppeteer uważa nawigację za zakończoną, gdy nie ma połączeń sieciowych przez co najmniej 500 ms. (sprawdź dokumenty API, aby uzyskać więcej informacji.)

Po tym zapisujemy plik PDF do zmiennej, zamykamy przeglądarkę i zwracamy plik PDF.

Uwaga: Metodapage.pdfotrzymuje obiektoptions, w którym można zapisać plik na dysk również z opcją 'path’. Jeśli ścieżka nie jest podana, plik PDF nie zostanie zapisany na dysku, zamiast tego otrzymasz bufor. Później omówię, jak sobie z tym poradzisz.)

w przypadku, gdy musisz się najpierw zalogować, aby wygenerować plik PDF z chronionej strony, najpierw musisz przejść do strony logowania, sprawdzić elementy formularza pod kątem identyfikatora lub nazwy, wypełnić je, a następnie przesłać formularz:

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

zawsze przechowuj dane logowania w zmiennych środowiskowych,nie koduj ich na twardo!

manipulacja stylem

uppeteer ma również rozwiązanie dla tej manipulacji stylem. Możesz wstawić znaczniki stylów przed wygenerowaniem pliku PDF, a Puppeteer wygeneruje plik ze zmodyfikowanymi stylami.

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

Wyślij plik do klienta i zapisz go

ok, teraz wygenerowałeś plik PDF na backendzie. Co teraz zrobić?

jak wspomniałem powyżej, jeśli nie zapiszesz pliku na dysk, otrzymasz bufor. Wystarczy wysłać ten bufor z odpowiednim typem zawartości do interfejsu.

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

teraz możesz po prostu wysłać zapytanie na serwer, aby uzyskać wygenerowany plik PDF.

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

Po wysłaniu żądania bufor powinien rozpocząć pobieranie. Ostatnim krokiem jest przekonwertowanie bufora na plik 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>

To było to! Jeśli klikniesz przycisk Zapisz, plik PDF zostanie zapisany przez przeglądarkę.

używając Puppeteer z Dockerem

myślę, że jest to najtrudniejsza część implementacji – więc oszczędzę Ci kilku godzin googlowania.

oficjalna dokumentacja mówi, że”uruchomienie Chrome bez głowy w Dockerze może być trudne”. Oficjalne dokumenty mają sekcję rozwiązywania problemów, gdzie w momencie pisania można znaleźć wszystkie niezbędne informacje na temat instalacji puppeteer z Dockerem.

Jeśli zainstalujesz Puppeteer na obrazku Alpine, upewnij się, że przewiń trochę w dół do tej części strony. W przeciwnym razie możesz pominąć fakt, że nie możesz uruchomić najnowszej wersji Puppeteer i musisz również wyłączyć użycie shm, używając flagi:

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

w przeciwnym razie proces podrzędny puppeteer może zabraknąć pamięci, zanim zostanie poprawnie uruchomiony. Więcej informacji na ten temat można znaleźć w powyższym linku do rozwiązywania problemów.

Opcja 3 + 1: CSS Print Rules

można by pomyśleć, że samo użycie CSS print rules jest łatwe z punktu widzenia programistów. Brak modułów NPM, tylko czysty CSS. Ale jak sobie radzą, jeśli chodzi o kompatybilność między przeglądarkami?

wybierając reguły drukowania CSS, musisz przetestować wynik w każdej przeglądarce, aby upewnić się, że zapewnia ten sam układ, a nie jest to na 100%.

na przykład wstawienie przerwy po danym elemencie nie może być uważane za ezoteryczny przypadek użycia, ale możesz być zaskoczony, że musisz użyć obejść, aby to działało w Firefoksie.

o ile nie jesteś zaprawionym w bojach magiem CSS z dużym doświadczeniem w tworzeniu stron do druku, może to być czasochłonne.

Zasady drukowania są świetne, jeśli możesz zachować proste arkusze stylów drukowania.

zobaczmy przykład.

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

Ten CSS powyżej ukrywa przycisk drukowania i wstawia podział strony po każdymdiv z klasącontent. jest świetny artykuł, który podsumowuje, co można zrobić z regułami drukowania i jakie są z nimi trudności, w tym kompatybilność przeglądarki.

biorąc wszystko pod uwagę, reguły drukowania CSS są świetne i skuteczne, jeśli chcesz zrobić plik PDF z niezbyt złożonej strony.

podsumowanie: PDF z HTML z węzłem.js i Puppeteer

więc szybko przejdźmy przez opcje, które omówiliśmy tutaj do generowania plików PDF ze stron HTML:

  • zrzut ekranu z drzewa DOM: może to być przydatne, gdy musisz utworzyć migawki ze strony (na przykład, aby utworzyć miniaturę), ale nie działa, gdy masz dużo danych do obsługi.
  • używaj tylko biblioteki PDF: Jeśli chcesz programowo tworzyć pliki PDF od podstaw, jest to idealne rozwiązanie. W przeciwnym razie musisz utrzymywać szablony HTML i PDF, co zdecydowanie nie jest możliwe.
  • Puppeteer: Pomimo tego, że było to stosunkowo trudne do uruchomienia na Dockerze, zapewniło to najlepszy wynik dla naszego przypadku użycia, a także było najłatwiejsze do napisania kodu.
  • reguły drukowania CSS: jeśli Twoi użytkownicy są wystarczająco wykształceni, aby wiedzieć, jak drukować do pliku, a Twoje strony są stosunkowo proste, może to być najbardziej bezbolesne rozwiązanie. Jak widzieliście w naszym przypadku, nie było.

miłego drukowania!

Tematy pokrewne

.js Tutorials for Beginners / @ RisingStack