這篇文章主要介紹“Node中如何用Puppeteer庫(kù)生成海報(bào)”,在日常操作中,相信很多人在Node中如何用Puppeteer庫(kù)生成海報(bào)問題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”Node中如何用Puppeteer庫(kù)生成海報(bào)”的疑惑有所幫助!接下來,請(qǐng)跟著小編一起來學(xué)習(xí)吧!
創(chuàng)新互聯(lián)服務(wù)項(xiàng)目包括福綿網(wǎng)站建設(shè)、福綿網(wǎng)站制作、福綿網(wǎng)頁制作以及福綿網(wǎng)絡(luò)營(yíng)銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢(shì)、行業(yè)經(jīng)驗(yàn)、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機(jī)構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,福綿網(wǎng)站推廣取得了明顯的社會(huì)效益與經(jīng)濟(jì)效益。目前,我們服務(wù)的客戶以成都為中心已經(jīng)輻射到福綿省份的部分城市,未來相信會(huì)繼續(xù)擴(kuò)大服務(wù)區(qū)域并繼續(xù)獲得客戶的支持與信任!
之前文章寫了一下前幾天因?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ù)用性高,h6、小程序、app的生成海報(bào)服務(wù)都可以使用。
優(yōu)化操作空間更大。因?yàn)楦某闪私涌谏珊?bào)的形式,可以使用各種服務(wù)端的方式去優(yōu)化響應(yīng)速度,比如:加服務(wù)器、加緩存
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 來完成! 下面是一些示例:
生成頁面 PDF或者截圖。
抓取 SPA(單頁應(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ò)展。
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)簽頁 => 截圖 => 關(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)簽頁
page.goto(url[, options]):導(dǎo)航到某個(gè)頁面
page.setViewport(viewport):制定打開頁面的窗口
page.$(selector):元素選擇
elementHandle.screenshot([options]):截圖。其中encoding屬性可以指定返回值是base64或Buffer
browser.close():關(guān)閉瀏覽器及標(biāo)簽頁
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', //頁面“l(fā)oad” 事件觸發(fā) 'domcontentloaded', //頁面 “DOMcontentloaded” 事件觸發(fā) 'networkidle0', //在 500ms 內(nèi)沒有任何網(wǎng)絡(luò)連接 'networkidle2' //在 500ms 內(nèi)網(wǎng)絡(luò)連接個(gè)數(shù)不超過 2 個(gè) ] });
如果使用 networkidle0 的方案等待頁面完成,會(huì)發(fā)現(xiàn)接口的響應(yīng)時(shí)間會(huì)比較長(zhǎng), 因?yàn)?networkidle0 需要等待500ms,真實(shí)業(yè)務(wù)場(chǎng)景下很多情況下不需要等待,所以可以封裝一個(gè)延時(shí)器,可以自定義等待時(shí)間。比如我們的海報(bào)頁只是渲染一個(gè)背景圖跟一個(gè)二維碼圖片,頁面觸發(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);
如果這種方式不能滿足,需要頁面在某個(gè)時(shí)機(jī)通知puppeteer結(jié)束,還可以使用 page.waitForSelector(selector[, options]) 等待頁面某個(gè)指定的元素出現(xiàn)。比如:頁面執(zhí)行完某個(gè)操作時(shí),插入一個(gè) id="end" 的元素,puppereer 等待這個(gè)元素出現(xiàn)。
await page.waitForSelector("#end")
類似的方法共包括:
page.waitForXPath(xpath[, options]):等待 xPath 對(duì)應(yīng)的元素出現(xiàn)在頁面中。
page.waitForSelector(selector[, options]):等待指定的選擇器匹配的元素出現(xiàn)在頁面中,如果調(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)簽頁打開頁面,截圖完成后只關(guān)閉標(biāo)簽頁,保留瀏覽器。下一次請(qǐng)求過來時(shí)直接創(chuàng)建標(biāo)簽頁,達(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)的流程。
到此,關(guān)于“Node中如何用Puppeteer庫(kù)生成海報(bào)”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!