淺析Node中怎麼利用Puppeteer庫生成海報(實現方案分享)

2022-01-18 22:00:14
怎麼利用Node生成海報?下面本篇文章給大家介紹一下使用+Puppeteer生成海報的方法,希望對大家有所幫助!

之前文章寫了一下前幾天因為使用了 html2canvas 碰到了很多相容性問題,差點提桶跑路。然後經過評論區大佬們指導,發現了一個操作簡單,複用性高的海報生成方案—— Node+Puppeteer生成海報

主要的設計思路為:存取生成海報的介面,介面通過Puppeteer去存取傳入的地址,將對應的元素截圖返回。

Puppeteer 生成海報相對於 Canvas 生成的優勢有哪些:

  • 沒有瀏覽器相容,平臺相容等問題。
  • 程式碼複用性高,h5、小程式、app的生成海報服務都可以使用。
  • 優化操作空間更大。因為改成了介面生成海報的形式,可以使用各種伺服器端的方式去優化響應速度,比如:加伺服器、加快取

puppeteer介紹

Puppeteer 是一個 庫,它提供了一個高階 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

更多node相關知識,請存取:!!

以上就是淺析Node中怎麼利用Puppeteer庫生成海報(實現方案分享)的詳細內容,更多請關注TW511.COM其它相關文章!