Converting a JavaScript-Rendered Web Page to PDF 的封面圖片

PetrDev

如果您曾嘗試以程式化方式將現代網頁轉換為 PDF,您可能曾遇到難題:檔案輸出空白、內容不完整,或停在讀取動畫上。網頁在瀏覽器中看起來完美,問題出在哪裡?

答案在於時機。大多數 PDF 處理方式都是在 JavaScript 渲染內容「之前」就擷取 HTML。在伺服器渲染的頁面中這樣沒問題——標記已經存在。但在 React/Vue/Angular 應用程式中,伺服器傳送的幾乎是空的殼,瀏覽器之後才建立 DOM。如果過早擷取,就會儲存這個殼。

以下是正確的做法。

為什麼天真的方法會失敗

wkhtmltopdf 是 Google 的經典解答。它快速且存在已久,但使用的是古老的 WebKit 版本,實際上沒有現代 JavaScript 支援。對於靜態頁面沒問題,但對於任何用戶端渲染的內容,它會擷取空白狀態。

瀏覽器的 window.print() / Ctrl+P 有效是因為它「就是」真正的瀏覽器——但這是手動操作、單頁面,且無法在規模上乾淨地自動化。

使用 HTTP 用戶端直接請求原始 HTML(axios/fetch 然後傳送至 PDF 函式庫)與 wkhtmltopdf 有相同的致命缺陷:沒有 JS 執行,沒有渲染後的內容。

您真正需要的是一個真正的瀏覽器引擎,能執行頁面的 JavaScript、等待其完成,然後「再」列印。這就是 Puppeteer。

Puppeteer 方法

Puppeteer 驅動無頭 Chromium。它執行頁面的方式與正常的 Chrome 分頁完全相同,因此您擷取的內容與畫面上呈現的相同。

const puppeteer = require('puppeteer');

async function pageToPdf(url, outPath) {
  const browser = await puppeteer.launch({
    args: ['--no-sandbox', '--disable-setuid-sandbox'], // needed in most containers
  });
  const page = await browser.newPage();

  await page.goto(url, { waitUntil: 'networkidle0', timeout: 60000 });

  await page.pdf({
    path: outPath,
    format: 'A4',
    printBackground: true, // otherwise CSS backgrounds/colors are dropped
    margin: { top: '20px', bottom: '20px', left: '16px', right: '16px' },
  });

  await browser.close();
}

pageToPdf('https://example.com', 'out.pdf');

Enter fullscreen mode Exit fullscreen mode

這裡有兩個選項比表面看起來更重要:

  • waitUntil: 'networkidle0' 告訴 Puppeteer 只有在 500 毫秒內沒有網路連線時,才認為導覽已完成。對於大多數 SPA 而言,這是資料已載入且 DOM 已建立的訊號。(networkidle2 允許最多 2 個連線——適用於具有長輪詢或分析信標的頁面,這些連線永遠不會完全靜止。)
  • printBackground: true —— 如果忘記這一點,每個背景顏色、漸層和圖片都會消失,因為 Chrome 的列印路徑預設會移除它們。

仍然會咬您的陷阱

取得非空白的 PDF 是第一步。取得「正確」的 PDF 才是問題浮現的地方。

延遲載入的內容

在捲動時才載入的圖片和區段不會出現在 PDF 中,因為 Puppeteer 從未捲動。在列印之前透過自動捲動強制載入它們:

await page.evaluate(async () => {
  await new Promise((resolve) => {
    let y = 0;
    const step = 300;
    const timer = setInterval(() => {
      window.scrollBy(0, step);
      y += step;
      if (y >= document.body.scrollHeight) {
        clearInterval(timer);
        window.scrollTo(0, 0);
        resolve();
      }
    }, 100);
  });
});

Enter fullscreen mode Exit fullscreen mode

Web 字型尚未載入

文字會以備用字型渲染,因為 Web 字型在列印時尚未完成載入。明確等待它:

await page.evaluateHandle('document.fonts.ready');

Enter fullscreen mode Exit fullscreen mode

列印 CSS 覆蓋您的版面配置

如果網站提供 @media print 樣式表,Chrome 會套用它。有時這是您想要的;有時它會毀掉版面配置。使用以下方式強制畫面渲染:

await page.emulateMediaType('screen');

Enter fullscreen mode Exit fullscreen mode

固定標頭重複或覆蓋內容

黏性/固定元素可能在每個 PDF 頁面上重複出現,或與主體重疊。可靠的解決方法是在列印之前將其清除——透過 page.addStyleTag() 將其位置設為 static(或隱藏它們)。

當自行託管 Puppeteer 不值得時

當您需要在自己控制的管道中產生 PDF 時,Puppeteer 是正確的工具。但它有真正的營運負擔:無頭 Chromium 很重(200MB+),需要在容器中安裝正確的系統函式庫,如果不小心管理瀏覽器實例就會洩漏記憶體,而且除非您將頁面集區化並限制並行度,否則在並行處理下會當機。對於一個附屬功能——「讓使用者匯出此頁面」——這是需要照顧的龐大基礎設施。

如果您只需要輸出而不需要管道,託管服務可以在幕後執行相同的無頭渲染後擷取,而無需營運負擔。我經營 site2pdf.online 正是為了這個目的——它在真正的 Chrome 中渲染每個頁面(因此 JS 內容會通過),它還可以追蹤網站的內部連結,以一次傳遞的方式擷取多個頁面,匯出為 PDF、PNG 或 ZIP。當替代方案是建立並維護自己的 Chromium 機群時,這很方便。

經驗法則:如果 PDF 產生是您產品的核心,請自行託管 Puppeteer 並擁有它。如果這是邊緣的便利功能,請將其卸載。

總結

空白 PDF 的問題幾乎總是時機問題:擷取發生在 JavaScript 渲染之前。使用真正的瀏覽器引擎,等待網路(和字型)穩定,捲動以觸發延遲載入的內容,並啟用 printBackground。這樣做,動態頁面就會乾淨地轉換。

此功能的完整、可複製貼上版本——加上固定標頭和多頁面處理——值得保存在某個程式碼片段中。您會比預期更常使用它。