實踐指南-網(wǎng)頁生成PDF
一、背景
開發(fā)工作中,需要實現(xiàn)網(wǎng)頁生成 PDF 的功能,生成的 PDF 需上傳至服務(wù)端,將 PDF 地址作為參數(shù)請求外部接口,這個轉(zhuǎn)換過程及轉(zhuǎn)換后的 PDF 不需要在前端展示給用戶。
二、技術(shù)選型該功能不需要在前端展示給用戶,為節(jié)省客戶端資源,選擇在服務(wù)端實現(xiàn)網(wǎng)頁生成 PDF 的功能。
1. Puppeteer—
Puppeteer[1] 是一個 Node 庫,它提供了高級 API 來通過 DevTools 協(xié)議控制 Chrome 或 Chromium。
在瀏覽器中手動執(zhí)行的大多數(shù)操作都可以使用 Puppeteer 完成,比如:
- 生成頁面的屏幕截圖和 PDF;
- 爬取
SPA并生成預(yù)渲染的內(nèi)容(即SSR); - 自動進(jìn)行表單提交,UI 測試,鍵盤輸入等;
- 創(chuàng)建最新的自動化測試環(huán)境。使用最新的
JavaScript和瀏覽器功能,直接在最新版本的Chrome中運行測試; - 捕獲時間線跟蹤網(wǎng)站,以幫助診斷性能問題;
- 測試
Chrome擴(kuò)展程序。
從上可見,Puppeteer 可以實現(xiàn)在Node 端生成頁面的 PDF 功能。
1. 安裝—
進(jìn)入項目,安裝 puppeteer 到本地。
$?npm?install?-g?cnpm?--registry=https://registry.npm.taobao.org
$?cnpm?i?puppeteer?--save
需注意的是,安裝 puppeteer 時,會下載與 API 一起使用的最新版本的 Chromium 瀏覽器,有以下方法可以修改默認(rèn)設(shè)置,不下載瀏覽器:
- 在環(huán)境變量[2]中設(shè)置
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD; - 用
puppeteer-core代替puppeteer。
puppeteer-core 是 puppeteer 的輕量級版本,默認(rèn)不下載瀏覽器,而是啟動現(xiàn)有的瀏覽器或者連接遠(yuǎn)程瀏覽器,使用 puppeteer-core 需注意本地有可連接的瀏覽器,且安裝的 puppeteer-core 版本與打算連接的瀏覽器兼容。連接本地瀏覽器方法如下:
const?browser?=?await?puppeteer.launch({?
??executablePath:?'/path/to/Chrome'?
});
本項目需要部署至服務(wù)端,沒有可連接的瀏覽器,因此選擇安裝的是 puppeteer。
2. 啟動瀏覽器—
const?browser?=?await?puppeteer.launch({
????headless:?true,
????args:?['--no-sandbox',?'--font-render-hinting=medium']
??})
headless 代表無頭模式,在后端啟動瀏覽器,前端不會有展示。
小建議:本地調(diào)試時,建議設(shè)置
headless: false,可以啟動完整版本的瀏覽器,直接在瀏覽器窗口查看內(nèi)容。
3. 打開新頁面—
生成瀏覽器后,在瀏覽器中打開新頁面。
const?page?=?await?browser.newPage()
4. 跳轉(zhuǎn)到指定頁面—
跳轉(zhuǎn)至要生成 PDF 的頁面。
await?page.goto(`${baseURL}/article/${id}`,?{
????timeout:?60000,
????waitUntil:?'networkidle2',?//?networkidle2?會一直等待,直到頁面加載后不存在?2?個以上的資源請求,這種狀態(tài)持續(xù)至少?500?ms
??})
timeout 是最長的加載時間,默認(rèn) 30s,網(wǎng)頁加載時間長的情況下,建議將 timeout 值改大,防止超時報錯。
waitUntil 表示頁面加載到什么程度可以開始生成 PDF 或其他操作了,當(dāng)網(wǎng)頁需加載的圖片資源較多時,建議設(shè)置為 networkidle2,有以下值可選:
- load:當(dāng)
load事件觸發(fā)時; - domcontentloaded:當(dāng)
DOMContentLoaded事件觸發(fā)時; - networkidle0:頁面加載后不存在 0 個以上的資源請求,這種狀態(tài)持續(xù)至少 500 ms;
- networkidle2:頁面加載后不存在 2 個以上的資源請求,這種狀態(tài)持續(xù)至少 500 ms。
5. 指定路徑,生成pdf—
上述指定的頁面加載完成后,將該頁面生成 PDF。
??const?ext?=?'.pdf'
??const?key?=?randomFilename(title,?ext)
??const?_path?=?path.resolve(config.uploadDir,?key)
??await?page.pdf({?path:?_path,?format:?'a4'?})
path 表示將 PDF 保存到的文件路徑,如果未提供路徑,PDF 將不會保存至磁盤。
小建議:不管 PDF 是不是需要保存到本地,建議在調(diào)試的時候都設(shè)置一個path,方便查看生成的 PDF 的樣式,檢查是否有問題。
format 表示 PDF 的紙張格式,a4 尺寸為 8.27 英寸 x 11.7 英寸,是傳統(tǒng)的打印尺寸。
注意:目前僅支持headless: true 無頭模式下生成 PDF
6. 關(guān)閉瀏覽器—
所有操作完成后,關(guān)閉瀏覽器,節(jié)約性能。
??await?browser.close()
四、難點1. 圖片懶加載—
由于需生成 PDF 的頁面是文章類型的頁面,包含大量圖片,且圖片引入了懶加載,導(dǎo)致生成的 PDF 會帶有很多懶加載兜底圖,效果如下圖:

解決方法是跳轉(zhuǎn)到頁面后,將頁面滾動到底部,所有圖片資源都會得到請求,waitUntil 設(shè)置為 networkidle2,圖片就能加載成功了。
await?autoScroll(page)?//?因為文章圖片引入了懶加載,所以需要把頁面滑動到最底部,保證所有圖片都加載出來
/**
?*?控制頁面自動滾動
?*?*/
function?autoScroll?(page)?{
??return?page.evaluate(()?=>?{
????return?new?Promise<void>(resolve?=>?{
??????let?totalHeight?=?0
??????const?distance?=?100
??????//?每200毫秒讓頁面下滑100像素的距離
??????const?timer?=?setInterval(()?=>?{
????????const?scrollHeight?=?document.body.scrollHeight
????????window.scrollBy(0,?distance)
????????totalHeight?+=?distance
????????if?(totalHeight?>=?scrollHeight)?{
??????????clearInterval(timer)
??????????resolve()
????????}
??????},?200)
????})
??})
}
這里用到了 page.evaluate() 方法,用來控制頁面操作,比如使用內(nèi)置的 DOM 選擇器、使用 window 方法等等。
2. CSS 打印樣式—
根據(jù)官網(wǎng)[3]說明,page.pdf() 生成 PDF 文件的樣式是通過 print css media 指定的,因此可以通過 css 來修改生成的 PDF 的樣式,以本文需求為例,生成的 PDF 需要隱藏頭部、底部,以及其他和文章主體無關(guān)的部分,代碼如下:
@media?print?{
??.other_info,
??.authors,
??.textDetail_comment,
??.detail_recTitle,
??.detail_rec,
??.SuspensePanel?{
????display:?none?!important;
??}
??.Footer,
??.HeaderSuctionTop?{
????display:?none;
??}
}
3. 登錄態(tài)—
由于存在一部分文章不對外部用戶公開,需要鑒權(quán)用戶身份,符合要求的用戶才能看到文章內(nèi)容,因此跳轉(zhuǎn)到指定文章頁后,需要在生成的瀏覽器窗口中注入登錄態(tài),符合條件的登錄用戶才能看到這部分文章的內(nèi)容。
采用注入 cookie 的方式來獲取登錄態(tài),使用 page.evaluate() 設(shè)置 cookie,代碼如下:
async?function?simulateLogin?(page,?cookies,?domain)?{
??return?await?page.evaluate((sig,?sess,?domain)?=>?{
????let?date?=?new?Date()
????date?=?new?Date(date.setDate(date.getDate()?+?1))
????let?expires?=?''
????expires?=?`;?expires=${date.toUTCString()}`
????document.cookie?=?`koa:sess.sig=${sig}${expires};?domain=${domain};?path=/`
????document.cookie?=?`koa:sess=${sess}=${expires};?domain=${domain};?path=/`?//?=是這個cookie的value
????document.cookie?=?`is_login=true${expires};?domain=${domain};?path=/`
??},?cookies['koa:sess.sig'],?cookies['koa:sess'],?domain)
}
await?simulateLogin(page,?cookies,?config.domain.split('//')[1])
小建議:
Puppeteer也有自帶的api實現(xiàn)cookie注入,如page.setCookie({name: name, value: value}),但是我用這個方式注入沒能獲取到登錄態(tài),沒有找到具體原因,建議還是直接用我上面這個方法來注入cookie,注意除name和value外,expires、domain、path也需要配置。
4. Docker 部署 Puppeteer—
根據(jù)上文操作,本地已經(jīng)可以成功將頁面生成 PDF 了,本地體驗沒問題后,需要部署到服務(wù)端給到測試、上線。
沒有修改 Dockerfile 時,部署后發(fā)現(xiàn)了如下錯誤:
官網(wǎng)有給 Docker 配置說明[4]可以參考,最終實踐可用的 ubuntu 系統(tǒng)的 Dockerfile 如下:
#?...省略...
#?安裝?puppeteer?依賴
RUN?apt-get?update?&&?\
????apt-get?install?-y?libgbm-dev?&&?\
????apt-get?install?gconf-service?libasound2?libatk1.0-0?libatk-bridge2.0-0?libc6?libcairo2?libcups2?libdbus-1-3?libexpat1?libfontconfig1?libgcc1?libgconf-2-4?libgdk-pixbuf2.0-0?libglib2.0-0?libgtk-3-0?libnspr4?libpango-1.0-0?libpangocairo-1.0-0?libstdc++6?libx11-6?libx11-xcb1?libxcb1?libxcomposite1?libxcursor1?libxdamage1?libxext6?libxfixes3?libxi6?libxrandr2?libxrender1?libxss1?libxtst6?ca-certificates?fonts-liberation?libappindicator1?libnss3?lsb-release?xdg-utils?wget?build-essential?libcairo2-dev?libpango1.0-dev?libjpeg-dev?libgif-dev?librsvg2-dev?-y?&&?\
????apt-get?install?-y?fonts-ipafont-gothic?fonts-wqy-zenhei?fonts-thai-tlwg?fonts-kacst?fonts-freefont-ttf?--no-install-recommends
#?...省略...
只需要重點關(guān)注 安裝 puppeteer 依賴 部分即可。
五、總結(jié)注意:在 v1.18.1 之前,Puppeteer 至少需要 Node v6.4.0。從 v1.18.1 到 v2.1.0 的版本都依賴于 Node 8.9.0+。從 v3.0.0 開始,Puppeteer 開始依賴于 Node 10.18.1+。配置 Dockerfile 時也需要注意服務(wù)端的 node 版本。
本文講述了實現(xiàn)在 Node 端將網(wǎng)頁生成 PDF 文件的完整過程,總結(jié)為以下 3 點:
- 技術(shù)選型,根據(jù)需求場景選擇合適的手段實現(xiàn)功能;
- 閱讀官方文檔[5],快速過一遍文檔才能少遇到些坑;
- 破解難點,使用一個未使用的工具,會遇到?jīng)]有解決過的難題,遇招拆招吧 ^ ^。
參照 Demo 源碼[6] 可快速上手上述功能,希望本文能對你有所幫助,感謝閱讀??
· 往期精彩 ·
參考資料
[1]Puppeteer: https://pptr.dev/
[2]環(huán)境變量: https://github.com/puppeteer/puppeteer/blob/v8.0.0/docs/api.md#environment-variables
[3]官網(wǎng): https://pptr.dev/#?product=Puppeteer&version=v8.0.0&show=api-pagepdfoptions
[4]Docker 配置說明: https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-puppeteer-in-docker
[5]官方文檔: https://pptr.dev/
[6]Demo 源碼: https://github.com/jiaozitang/puppeteerPdfDemo
往期推薦


如何實現(xiàn)比 setTimeout 快 80 倍的定時器?

萬字長文!總結(jié)Vue 性能優(yōu)化方式及原理

最后
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點個「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點在看,都是耍流氓 -_-)
歡迎加我微信「huab119」拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...
關(guān)注公眾號「前端勸退師」,持續(xù)為你推送精選好文,也可以加我為好友,隨時聊騷。
