怎么利用Node生成海報?下面本篇文章給大家介紹一下使用Node+Puppeteer生成海報的方法,希望對大家有所幫助!
之前文章寫了一下前幾天因為使用了 html2canvas 碰到了很多兼容性問題,差點提桶跑路。然后經過評論區大佬們指導,發現了一個操作簡單,復用性高的海報生成方案—— Node+Puppeteer生成海報 。
主要的設計思路為:訪問生成海報的接口,接口通過Puppeteer去訪問傳入的地址,將對應的元素截圖返回。
Puppeteer 生成海報相對于 Canvas 生成的優勢有哪些:
- 沒有瀏覽器兼容,平臺兼容等問題。
- 代碼復用性高,h5、小程序、app的生成海報服務都可以使用。
- 優化操作空間更大。因為改成了接口生成海報的形式,可以使用各種服務端的方式去優化響應速度,比如:加服務器、加緩存
puppeteer介紹
Puppeteer 是一個 Nodejs 庫,它提供了一個高級 API 來通過 DevTools 協議控制 Chromium 或 Chrome。Puppeteer 默認以 headless 模式運行即“無頭”模式,但是可以通過修改配置 headless:false 運行“有頭”模式。 在瀏覽器中手動執行的絕大多數操作都可以使用 Puppeteer 來完成! 下面是一些示例:
- 生成頁面 PDF或者截圖。
- 抓取 SPA(單頁應用)并生成預渲染內容(即“SSR”(服務器端渲染))。
- 自動提交表單,進行 UI 測試,鍵盤輸入等。
- 創建一個時時更新的自動化測試環境。 使用最新的 JavaScript 和瀏覽器功能直接在最新版本的Chrome中執行測試。
- 捕獲網站的 timeline trace,用來幫助分析性能問題。
- 測試瀏覽器擴展。
方案實現
1. 寫一個簡單的接口
Express 是一個簡潔而靈活的 node.js Web應用框架。使用express寫一個簡單的node服務,定義一個接口,接收截圖所需的配置項傳遞給puppeteer。
const express = require('express') const createError = require("http-errors") const app = express() // 中間件--json化入參 app.use(express.json()) app.post('/api/getShareImg', (req, res) => { // 業務邏輯 }) // 錯誤攔截 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) }) // 啟動服務監聽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. 創建一個截圖模塊
打開一個瀏覽器 => 打開一個標簽頁 => 截圖 => 關閉瀏覽器
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]):啟動一個瀏覽器
- browser.newPage():創建一個標簽頁
- page.goto(url[, options]):導航到某個頁面
- page.setViewport(viewport):制定打開頁面的窗口
- page.$(selector):元素選擇
- elementHandle.screenshot([options]):截圖。其中encoding屬性可以指定返回值是base64或Buffer
- browser.close():關閉瀏覽器及標簽頁
3. 優化
1. 請求時間優化
page.goto(url[, options]) 方法的配置項 waitUntil 表示什么狀態下算執行完畢, 默認是load事件觸發時。事件包括:
await page.goto(url, { waitUntil: [ 'load', //頁面“load” 事件觸發 'domcontentloaded', //頁面 “DOMcontentloaded” 事件觸發 'networkidle0', //在 500ms 內沒有任何網絡連接 'networkidle2' //在 500ms 內網絡連接個數不超過 2 個 ] });
如果使用 networkidle0 的方案等待頁面完成,會發現接口的響應時間會比較長, 因為 networkidle0 需要等待500ms,真實業務場景下很多情況下不需要等待,所以可以封裝一個延時器,可以自定義等待時間。比如我們的海報頁只是渲染一個背景圖跟一個二維碼圖片,頁面觸發 load 時已經加載完成了,不需要等待時間,可以傳入0跳過等待時間。
const waitTime = (n) => new Promise((r) => setTimeout(r, n)); //省略部分代碼 await page.goto(opt.url); await waitTime(opt.waitTime || 0);
如果這種方式不能滿足,需要頁面在某個時機通知puppeteer結束,還可以使用 page.waitForSelector(selector[, options]) 等待頁面某個指定的元素出現。比如:頁面執行完某個操作時,插入一個 id="end" 的元素,puppereer 等待這個元素出現。
await page.waitForSelector("#end")
類似的方法共包括:
- page.waitForXPath(xpath[, options]):等待 xPath 對應的元素出現在頁面中。
- page.waitForSelector(selector[, options]):等待指定的選擇器匹配的元素出現在頁面中,如果調用此方法時已經有匹配的元素,那么此方法立即返回。
- page.waitForResponse(urlOrPredicate[, options]):等待指定的響應結束。
- page.waitForRequest(urlOrPredicate[, options]):等待指定的響應出現。
- page.waitForFunction(pageFunction[, options[, …args]]):等待某個方法執行。
- page.waitFor(selectorOrFunctionOrTimeout[, options[, …args]]):此方法相當于上面幾個方法的選擇器,根據第一個參數的不同結果不同,比如:傳入一個string類型,會判斷是不是xpath或者selector,此時相當于waitForXPath或waitForSelector。
2. 啟動項優化
Chromium啟動時還會開啟很多不需要的功能,可以通過參數禁用某些啟動項。
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. 復用瀏覽器
因為每次接口被調用都啟動了一個瀏覽器,截圖之后關閉了這個瀏覽器,造成了資源的浪費,并且啟動瀏覽器也需要耗費時間。并且同時啟動的瀏覽器過多,程序還會拋出異常。所以使用了連接池:啟動多個瀏覽器,在其中一個瀏覽器下創建標簽頁打開頁面,截圖完成后只關閉標簽頁,保留瀏覽器。下一次請求過來時直接創建標簽頁,達到復用瀏覽器的目的。當瀏覽器使用次數達到一定數目或者一段時間內沒有被使用時就關閉這個瀏覽器。 有大佬已經對generic-pool這個連接池進行了處理,我就直接拿來用了。
const initPuppeteerPool = () => { if (global.pp) global.pp.drain().then(() => global.pp.clear()) const opt = { max: 4,//最多產生多少個puppeteer實例 。 min: 1,//保證池中最少有多少個puppeteer實例存活 testOnBorrow: true,// 在將實例提供給用戶之前,池應該驗證這些實例。 autostart: false,//是不是需要在池初始化時初始化實例 idleTimeoutMillis: 1000 * 60 * 60,//如果一個實例60分鐘都沒訪問就關掉他 evictionRunIntervalMillis: 1000 * 60 * 3,//每3分鐘檢查一次實例的訪問狀態 maxUses: 2048,//自定義的屬性:每一個 實例 最大可重用次數。 validator: () => Promise.resolve(true) } const factory = { create: () => puppeteer.launch({ //啟動參數參考第二條 }).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) // 重寫了原有池的消費實例的方法。添加一個實例使用次數的增加 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 => { // 不管業務方使用實例成功與后都表示一下實例消費完成 pool.release(resource) return result }, err => { pool.release(resource) throw err } ) } return pool; } global.pp = initPuppeteerPool()
4. 優化接口防止圖片重復生成
用同一組參數重復調用時每次都會開啟一個瀏覽器進程去截圖,可以使用緩存機制優化重復的請求。可以通過傳入唯一的key作為標識位(比如用戶id+活動id),將圖片base64存入redis或者寫入內存中。當接口被請求時先查看緩存里是否已經生成過,如果生成過就直接從緩存取。否則就走生成海報的流程。
結尾
這個方案目前已經開始在項目里試運行了,這對于我一個前端開發來說簡直太友好了,再也不用在小程序里一步一步去繪制canvas,不用考慮資源跨域,也不用考慮微信瀏覽器、各種自帶瀏覽器的兼容問題。省下了時間可以讓我寫這篇文章。其次,我比較擔心的還是性能問題,因為只有在分享的動作才會觸發,并發較小,目前使用還未暴露出性能的問題,有了解的大佬們可以指導我一下可以進一步優化或者預防的點。
代碼
完整代碼查看:github
https://github.com/yuwuwu/markdown-code/tree/master/puppeteer%E6%88%AA%E5%9B%BE