怎么利用Node生成海報(bào)?下面本篇文章給大家介紹一下使用Node+Puppeteer生成海報(bào)的方法,希望對(duì)大家有所幫助!
之前文章寫了一下前幾天因?yàn)槭褂昧?html2canvas 碰到了很多兼容性問題,差點(diǎn)提桶跑路。然后經(jīng)過評(píng)論區(qū)大佬們指導(dǎo),發(fā)現(xiàn)了一個(gè)操作簡(jiǎn)單,復(fù)用性高的海報(bào)生成方案—— Node+Puppeteer生成海報(bào) 。
主要的設(shè)計(jì)思路為:訪問生成海報(bào)的接口,接口通過Puppeteer去訪問傳入的地址,將對(duì)應(yīng)的元素截圖返回。
Puppeteer 生成海報(bào)相對(duì)于 Canvas 生成的優(yōu)勢(shì)有哪些:
- 沒有瀏覽器兼容,平臺(tái)兼容等問題。
- 代碼復(fù)用性高,h5、小程序、app的生成海報(bào)服務(wù)都可以使用。
- 優(yōu)化操作空間更大。因?yàn)楦某闪私涌谏珊?bào)的形式,可以使用各種服務(wù)端的方式去優(yōu)化響應(yīng)速度,比如:加服務(wù)器、加緩存
puppeteer介紹
Puppeteer 是一個(gè) Nodejs 庫(kù),它提供了一個(gè)高級(jí) API 來通過 DevTools 協(xié)議控制 Chromium 或 Chrome。Puppeteer 默認(rèn)以 headless 模式運(yùn)行即“無頭”模式,但是可以通過修改配置 headless:false 運(yùn)行“有頭”模式。 在瀏覽器中手動(dòng)執(zhí)行的絕大多數(shù)操作都可以使用 Puppeteer 來完成! 下面是一些示例:
- 生成頁(yè)面 PDF或者截圖。
- 抓取 SPA(單頁(yè)應(yīng)用)并生成預(yù)渲染內(nèi)容(即“SSR”(服務(wù)器端渲染))。
- 自動(dòng)提交表單,進(jìn)行 UI 測(cè)試,鍵盤輸入等。
- 創(chuàng)建一個(gè)時(shí)時(shí)更新的自動(dòng)化測(cè)試環(huán)境。 使用最新的 JavaScript 和瀏覽器功能直接在最新版本的Chrome中執(zhí)行測(cè)試。
- 捕獲網(wǎng)站的 timeline trace,用來幫助分析性能問題。
- 測(cè)試瀏覽器擴(kuò)展。
方案實(shí)現(xiàn)
1. 寫一個(gè)簡(jiǎn)單的接口
Express 是一個(gè)簡(jiǎn)潔而靈活的 node.js Web應(yīng)用框架。使用express寫一個(gè)簡(jiǎn)單的node服務(wù),定義一個(gè)接口,接收截圖所需的配置項(xiàng)傳遞給puppeteer。
const express = require('express') const createError = require("http-errors") const app = express() // 中間件--json化入?yún)?app.use(express.json()) app.post('/api/getShareImg', (req, res) => { // 業(yè)務(wù)邏輯 }) // 錯(cuò)誤攔截 app.use(function(req, res, next) { next(createError(404)); }); app.use(function(err, req, res, next) { let result = { code: 0, msg: err.message, err: err.stack } res.status(err.status || 500).json(result) }) // 啟動(dòng)服務(wù)監(jiān)聽7000端口 const server = app.listen(7000, '0.0.0.0', () => { const host = server.address().address; const port = server.address().port; console.log('app start listening at http://%s:%s', host, port); });
2. 創(chuàng)建一個(gè)截圖模塊
打開一個(gè)瀏覽器 => 打開一個(gè)標(biāo)簽頁(yè) => 截圖 => 關(guān)閉瀏覽器
const puppeteer = require("puppeteer"); module.exports = async (opt) => { try { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(opt.url, { waitUntil: ['networkidle0'] }); await page.setViewport({ width: opt.width, height: opt.height, }); const ele = await page.$(opt.ele); const base64 = await ele.screenshot({ fullPage: false, omitBackground: true, encoding: 'base64' }); await browser.close(); return 'data:image/png;base64,'+ base64 } catch (error) { throw error } };
- puppeteer.launch([options]):?jiǎn)?dòng)一個(gè)瀏覽器
- browser.newPage():創(chuàng)建一個(gè)標(biāo)簽頁(yè)
- page.goto(url[, options]):導(dǎo)航到某個(gè)頁(yè)面
- page.setViewport(viewport):制定打開頁(yè)面的窗口
- page.$(selector):元素選擇
- elementHandle.screenshot([options]):截圖。其中encoding屬性可以指定返回值是base64或Buffer
- browser.close():關(guān)閉瀏覽器及標(biāo)簽頁(yè)
3. 優(yōu)化
1. 請(qǐng)求時(shí)間優(yōu)化
page.goto(url[, options]) 方法的配置項(xiàng) waitUntil 表示什么狀態(tài)下算執(zhí)行完畢, 默認(rèn)是load事件觸發(fā)時(shí)。事件包括:
await page.goto(url, { waitUntil: [ 'load', //頁(yè)面“l(fā)oad” 事件觸發(fā) 'domcontentloaded', //頁(yè)面 “DOMcontentloaded” 事件觸發(fā) 'networkidle0', //在 500ms 內(nèi)沒有任何網(wǎng)絡(luò)連接 'networkidle2' //在 500ms 內(nèi)網(wǎng)絡(luò)連接個(gè)數(shù)不超過 2 個(gè) ] });
如果使用 networkidle0 的方案等待頁(yè)面完成,會(huì)發(fā)現(xiàn)接口的響應(yīng)時(shí)間會(huì)比較長(zhǎng), 因?yàn)?networkidle0 需要等待500ms,真實(shí)業(yè)務(wù)場(chǎng)景下很多情況下不需要等待,所以可以封裝一個(gè)延時(shí)器,可以自定義等待時(shí)間。比如我們的海報(bào)頁(yè)只是渲染一個(gè)背景圖跟一個(gè)二維碼圖片,頁(yè)面觸發(fā) load 時(shí)已經(jīng)加載完成了,不需要等待時(shí)間,可以傳入0跳過等待時(shí)間。
const waitTime = (n) => new Promise((r) => setTimeout(r, n)); //省略部分代碼 await page.goto(opt.url); await waitTime(opt.waitTime || 0);
如果這種方式不能滿足,需要頁(yè)面在某個(gè)時(shí)機(jī)通知puppeteer結(jié)束,還可以使用 page.waitForSelector(selector[, options]) 等待頁(yè)面某個(gè)指定的元素出現(xiàn)。比如:頁(yè)面執(zhí)行完某個(gè)操作時(shí),插入一個(gè) id="end" 的元素,puppereer 等待這個(gè)元素出現(xiàn)。
await page.waitForSelector("#end")
類似的方法共包括:
- page.waitForXPath(xpath[, options]):等待 xPath 對(duì)應(yīng)的元素出現(xiàn)在頁(yè)面中。
- page.waitForSelector(selector[, options]):等待指定的選擇器匹配的元素出現(xiàn)在頁(yè)面中,如果調(diào)用此方法時(shí)已經(jīng)有匹配的元素,那么此方法立即返回。
- page.waitForResponse(urlOrPredicate[, options]):等待指定的響應(yīng)結(jié)束。
- page.waitForRequest(urlOrPredicate[, options]):等待指定的響應(yīng)出現(xiàn)。
- page.waitForFunction(pageFunction[, options[, …args]]):等待某個(gè)方法執(zhí)行。
- page.waitFor(selectorOrFunctionOrTimeout[, options[, …args]]):此方法相當(dāng)于上面幾個(gè)方法的選擇器,根據(jù)第一個(gè)參數(shù)的不同結(jié)果不同,比如:傳入一個(gè)string類型,會(huì)判斷是不是xpath或者selector,此時(shí)相當(dāng)于waitForXPath或waitForSelector。
2. 啟動(dòng)項(xiàng)優(yōu)化
Chromium啟動(dòng)時(shí)還會(huì)開啟很多不需要的功能,可以通過參數(shù)禁用某些啟動(dòng)項(xiàng)。
const browser = await puppeteer.launch({ headless: true, slowMo: 0, args: [ '--no-zygote', '--no-sandbox', '--disable-gpu', '--no-first-run', '--single-process', '--disable-extensions', "--disable-xss-auditor", '--disable-dev-shm-usage', '--disable-popup-blocking', '--disable-setuid-sandbox', '--disable-accelerated-2d-canvas', '--enable-features=NetworkService', ] });
3. 復(fù)用瀏覽器
因?yàn)槊看谓涌诒徽{(diào)用都啟動(dòng)了一個(gè)瀏覽器,截圖之后關(guān)閉了這個(gè)瀏覽器,造成了資源的浪費(fèi),并且啟動(dòng)瀏覽器也需要耗費(fèi)時(shí)間。并且同時(shí)啟動(dòng)的瀏覽器過多,程序還會(huì)拋出異常。所以使用了連接池:?jiǎn)?dòng)多個(gè)瀏覽器,在其中一個(gè)瀏覽器下創(chuàng)建標(biāo)簽頁(yè)打開頁(yè)面,截圖完成后只關(guān)閉標(biāo)簽頁(yè),保留瀏覽器。下一次請(qǐng)求過來時(shí)直接創(chuàng)建標(biāo)簽頁(yè),達(dá)到復(fù)用瀏覽器的目的。當(dāng)瀏覽器使用次數(shù)達(dá)到一定數(shù)目或者一段時(shí)間內(nèi)沒有被使用時(shí)就關(guān)閉這個(gè)瀏覽器。 有大佬已經(jīng)對(duì)generic-pool這個(gè)連接池進(jìn)行了處理,我就直接拿來用了。
const initPuppeteerPool = () => { if (global.pp) global.pp.drain().then(() => global.pp.clear()) const opt = { max: 4,//最多產(chǎn)生多少個(gè)puppeteer實(shí)例 。 min: 1,//保證池中最少有多少個(gè)puppeteer實(shí)例存活 testOnBorrow: true,// 在將實(shí)例提供給用戶之前,池應(yīng)該驗(yàn)證這些實(shí)例。 autostart: false,//是不是需要在池初始化時(shí)初始化實(shí)例 idleTimeoutMillis: 1000 * 60 * 60,//如果一個(gè)實(shí)例60分鐘都沒訪問就關(guān)掉他 evictionRunIntervalMillis: 1000 * 60 * 3,//每3分鐘檢查一次實(shí)例的訪問狀態(tài) maxUses: 2048,//自定義的屬性:每一個(gè) 實(shí)例 最大可重用次數(shù)。 validator: () => Promise.resolve(true) } const factory = { create: () => puppeteer.launch({ //啟動(dòng)參數(shù)參考第二條 }).then(instance => { instance.useCount = 0; return instance; }), destroy: instance => { instance.close() }, validate: instance => { return opt.validator(instance).then(valid => Promise.resolve(valid && (opt.maxUses <= 0 || instance.useCount < opt.maxUses))); } }; const pool = genericPool.createPool(factory, opt) const genericAcquire = pool.acquire.bind(pool) // 重寫了原有池的消費(fèi)實(shí)例的方法。添加一個(gè)實(shí)例使用次數(shù)的增加 pool.acquire = () => genericAcquire().then(instance => { instance.useCount += 1 return instance }) pool.use = fn => { let resource return pool .acquire() .then(r => { resource = r return resource }) .then(fn) .then( result => { // 不管業(yè)務(wù)方使用實(shí)例成功與后都表示一下實(shí)例消費(fèi)完成 pool.release(resource) return result }, err => { pool.release(resource) throw err } ) } return pool; } global.pp = initPuppeteerPool()
4. 優(yōu)化接口防止圖片重復(fù)生成
用同一組參數(shù)重復(fù)調(diào)用時(shí)每次都會(huì)開啟一個(gè)瀏覽器進(jìn)程去截圖,可以使用緩存機(jī)制優(yōu)化重復(fù)的請(qǐng)求??梢酝ㄟ^傳入唯一的key作為標(biāo)識(shí)位(比如用戶id+活動(dòng)id),將圖片base64存入redis或者寫入內(nèi)存中。當(dāng)接口被請(qǐng)求時(shí)先查看緩存里是否已經(jīng)生成過,如果生成過就直接從緩存取。否則就走生成海報(bào)的流程。
結(jié)尾
這個(gè)方案目前已經(jīng)開始在項(xiàng)目里試運(yùn)行了,這對(duì)于我一個(gè)前端開發(fā)來說簡(jiǎn)直太友好了,再也不用在小程序里一步一步去繪制canvas,不用考慮資源跨域,也不用考慮微信瀏覽器、各種自帶瀏覽器的兼容問題。省下了時(shí)間可以讓我寫這篇文章。其次,我比較擔(dān)心的還是性能問題,因?yàn)橹挥性诜窒淼膭?dòng)作才會(huì)觸發(fā),并發(fā)較小,目前使用還未暴露出性能的問題,有了解的大佬們可以指導(dǎo)我一下可以進(jìn)一步優(yōu)化或者預(yù)防的點(diǎn)。
代碼
完整代碼查看:github
https://github.com/yuwuwu/markdown-code/tree/master/puppeteer%E6%88%AA%E5%9B%BE