在朋友圈,你可能會見過有不少帶着我的信息或者二維碼的絢麗的海報圖片,看起來很高大上的樣子。在很早以前,我有過了解的是,這些海報圖片都是由 UI 設計師,進行人肉設計出來的,很是考驗設計師的忍耐力。再到後來,隨着 web 技術的發展,出現了由前端來生成一張又一張的海報圖片,這樣 UI 設計師只須要設計一張模版圖片,剩餘的交給前端來完成便可,大大的減輕了 UI 小姐姐們苦力活(滑稽笑)。html
雖然,UI 小姐姐的活減輕了,可是前端(包含移動端)小哥哥的活就加劇了,還面臨着不少頭疼的問題,好比:生成的圖片清晰度不夠、手機兼容問題致使生成失敗、資源跨域問題致使生成失敗、web 端和移動端生成的圖片不同等等問題。前端
其實前端是經過 HTML5 新增長的 canvas API 來繪製的,可是 canvas 繪製會有不少的痛點:上手門檻比較高,須要掌握 canvas API;代碼可讀性比較差、調試複雜;代碼可複用低,每一個端須要從新編碼;無緩存、同一張相同圖片會屢次繪製,用戶體驗差;若是有遠程圖片,可能會引發跨域問題,致使繪製失敗等等問題。node
針對這些問題,社區出現了一個開源庫(html2canvas),經過編寫 HTML 頁面來生成圖片,有效解決了大部分問題,可是圖片跨域、緩存、代碼複用問題,仍是沒法解決。針對這些問題,社區提出了使用服務端來完成海報圖片渲染,就有可能完全解決這些問題。git
在小程序社區,有贊商城前端團隊成員提出基於 Puppeteer 來實現公共海報渲染服務,使用方只需傳入海報圖片的html,海報渲染服務繪製一張對應的圖片做爲返回結果,解決了canvas繪製的各類痛點問題。那麼,我今天就來體驗下快感吧。在這以前,咱們先來了解下!github
puppeteer 是一個Chrome官方出品的headless Chrome node庫。它提供了一系列的API, 能夠在無UI的狀況下調用Chrome的功能, 適用於爬蟲、自動化處理等各類場景等。web
根據官網上描述,Puppeteer幾乎能實現你能在瀏覽器上作的任何事情,好比:npm
一、生成頁面截圖和 PDF;canvas
二、自動化表單提交、UI 測試、鍵盤輸入等;小程序
三、建立一個最新的自動化測試環境。使用最新的 JavaScript ;跨域
四、和瀏覽器功能,能夠直接在最新版本的 Chrome 中運行測試;
五、捕獲站點的時間線跟蹤,以幫助診斷性能問題;
六、爬取 SPA 頁面並進行預渲染(即'Prerender');
七、開發我的微信接口,實現我的微信機器人(wechaty)。
本文的渲染服務將會使用它的截圖功能,來實現圖片生成。
接下來,就讓咱們簡單的看看如何代碼實現吧!
首先,須要先初始化一個 npm 項目,而且安裝響應的模塊,這裏就不一一說明了。
npm init
npm install puppeteer koa crypto --save
複製代碼
這裏,咱們安裝了三個模塊:puppeteer 咱們今天的關鍵性模塊、koa 快速搭建一個 web 服務、crypto 經過加密內容字符串來生成惟一標識符。
另外,個人 node 版本是 10.11 的,因此使用了不少新語法。 app.js
/* * @Descripttion: 入口文件 * @version: 1.0.0 * @Author: falost * @Date: 2019-08-27 10:54:32 * @LastEditors: falost * @LastEditTime: 2019-09-08 18:20:41 */
const SnapshotController = require('./libs/SnapshotController')
const Koa = require('koa')
const controller = new SnapshotController()
const app = new Koa()
app.use(async ctx => {
return await controller.postSnapshotJson(ctx)
})
app.listen(3000)
複製代碼
/libs/SnapshotController.js
/* * @Descripttion: 調取 puppenter 來生成接收到的html 數據生成圖片 * @version: 1.0.0 * @Author: falost * @Date: 2019-08-27 09:55:52 * @LastEditors: falost * @LastEditTime: 2019-09-08 18:20:56 */
const crypto = require('crypto');
const PuppenteerHelper = require('./PuppenteerHelper');
const oneDay = 24 * 60 * 60;
class SnapshotController {
/** * 截圖接口 * @param {Object} ctx 上下文 */
async postSnapshotJson(ctx) {
const result = await this.handleSnapshot()
ctx.body = {code: 10000, message: 'ok', result}
}
async handleSnapshot() {
const { ctx } = this
const { html } = ctx.request.body // html 是咱們將要生成的海報圖片的 HTML 實現代碼字符串
// 根據 html 作 sha256 的哈希做爲 Redis Key
const htmlRedisKey = crypto.createHash('sha256').update(html).digest('hex');
try {
// 首先看海報是否有繪製過的
let result = await this.findImageFromCache(htmlRedisKey);
// 獲取緩存失敗
if (!result) {
result = await this.generateSnapshot(htmlRedisKey);
}
return result;
} catch (error) {
ctx.status = 500;
return ctx.throw(500, error.message);
}
}
/** * 判斷kv中是否有緩存 * @param {String} htmlRedisKey kv存儲的key */
async findImageFromCache(htmlRedisKey) {
return false
}
/** * 生成截圖 * @param {String} htmlRedisKey kv存儲的key */
async generateSnapshot(htmlRedisKey) {
const { ctx } = this
const {
html,
width = 375,
height = 667,
quality = 80,
ratio = 2,
type: imageType = 'jpeg',
} = ctx.request.body;
if (!html) {
return 'html 不能爲空'
}
let imgBuffer;
try {
imgBuffer = await PuppenteerHelper.createImg({
html,
width,
height,
quality,
ratio,
imageType,
fileType: 'path',
htmlRedisKey
});
} catch (err) {
// logger
console.log(err)
}
let imgUrl;
try {
imgUrl = await this.uploadImage(imgBuffer);
// 將海報圖片路徑存在 Redis 裏
await ctx.kvdsClient.setex(htmlRedisKey, oneDay, imgUrl);
} catch (err) {
}
return {
img: imgUrl || ''
}
}
/** * 上傳圖片到 CDN 服務 * @param {Buffer} imgBuffer 圖片buffer */
async uploadImage(imgBuffer) {
// upload image to cdn and return cdn url
}
}
module.exports = SnapshotController
複製代碼
./libs/PuppenteerHelper.js
/* * @Descripttion: 建立生成圖片類 * @version: 1.0.0 * @Author: falost * @Date: 2019-08-27 11:43:41 * @LastEditors: falost * @LastEditTime: 2019-09-08 18:20:51 */
const puppeteer = require('puppeteer')
const { mkdirsSync, formatNumber } = require('../utils/utils')
class PuppenteerHelper {
async createImg(params) {
const browser = await puppeteer.launch({
headless: false, // 默認爲 true 不打開瀏覽器,設置 false 打開
})
const date = new Date()
const path = `static/upload/${date.getFullYear()}/${formatNumber(date.getMonth() + 1)}`
mkdirsSync(path)
// 經過建立瀏覽器標籤來打開
const page = await browser.newPage()
// 設置視窗大小
await page.setViewport({
width: params.width,
height: params.height,
deviceScaleFactor: params.ratio
})
// 設置須要截圖的html內容
await page.setContent(params.html)
await this.waitForNetworkIdle(page, 50)
let filePath
// 根據 type 返回不一樣的類型 一種圖片路徑、一種 base64
if (params.fileType === 'path') {
filePath = `${path}/${params.htmlRedisKey}.${params.imageType}`
await page.screenshot({
path: filePath,
fullPage: false,
omitBackground: true
})
} else {
filePath = await page.screenshot({
fullPage: false,
omitBackground: true,
encoding: 'base64'
})
}
browser.close()
return filePath
}
// 等待HTML 頁面資源加載完成
waitForNetworkIdle(page, timeout, maxInflightRequests = 0) {
page.on('request', onRequestStarted);
page.on('requestfinished', onRequestFinished);
page.on('requestfailed', onRequestFinished);
let inflight = 0;
let fulfill;
let promise = new Promise(x => fulfill = x);
let timeoutId = setTimeout(onTimeoutDone, timeout);
return promise;
function onTimeoutDone() {
page.removeListener('request', onRequestStarted);
page.removeListener('requestfinished', onRequestFinished);
page.removeListener('requestfailed', onRequestFinished);
fulfill();
}
function onRequestStarted() {
++inflight;
if (inflight > maxInflightRequests)
clearTimeout(timeoutId);
}
function onRequestFinished() {
if (inflight === 0)
return;
--inflight;
if (inflight === maxInflightRequests)
timeoutId = setTimeout(onTimeoutDone, timeout);
}
}
}
module.exports = new PuppenteerHelper()
複製代碼
../utils/utils.js
/* * @Descripttion: 工具類庫 * @version: * @Author: falost * @Date: 2019-08-27 14:10:16 * @LastEditors: falost * @LastEditTime: 2019-08-27 14:15:52 */
const fs = require('fs')
const path = require('path')
const mkdirsSync = (dirname) => {
if (fs.existsSync(dirname)) {
return true;
} else {
if (mkdirsSync(path.dirname(dirname))) {
fs.mkdirSync(dirname);
return true;
}
}
}
const formatNumber = function (n) {
n = n.toString()
return n[1] ? n : '0' + n
}
module.exports = {
mkdirsSync,
formatNumber
}
複製代碼
到這裏,簡單的代碼實現步驟,現已完成,接下來咱們看看最後生成的效果圖吧!
若是你想要用於生產環境,那麼你還須要作一些其餘個工做,這樣才能保證他能更好的產出。固然這裏面也有些坑,還須要咱們去完成填坑的。
git 倉庫地址:github.com/falost/node…
若是你想了解更多關於 puppeteer 的使用, 能夠查詢官方倉庫說明文檔:github.com/GoogleChrom…
很是感謝您的耐心閱讀!
文中若有不對之處,歡迎留言指教!
做者:falost
原文地址:https://falost.cc/article/5d74e54f8457894be1bc3b45複製代碼