如果您曾嘗試以程式化方式將現代網頁轉換為 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。這樣做,動態頁面就會乾淨地轉換。
此功能的完整、可複製貼上版本——加上固定標頭和多頁面處理——值得保存在某個程式碼片段中。您會比預期更常使用它。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.