在朋友圈,你可能会见过有不少带着我的信息或者二维码的绚丽的海报图片,看起来很高大上的样子。在很早以前,我有过了解的是,这些海报图片都是由 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复制代码