原文参考我的公众号文章 前端生成海报新姿态 Puppeteer
以往都是怎么生成海报的?
在开发中常常会遇到「生成海报长图」的需要,个别都是这么做的:
- 后端生成:引入会图库进行绘制
- 前端生成:用原生canvas进行绘制、用一些js库(html-to-canvas)
这么用过去的体验就是:无论是谁生成,都会遇到海报上各个元素的定位艰难、款式还原的艰难、动静内容和动静海报尺寸不好把控等问题。
因而,通过一波摸索,接触了puppeteer
这个「高级货!」,用完之后,几乎有种相见恨晚的感觉!
所以当初要想生成一个简单的海报或者长图的流程变成了这样:
- 编写海报承载web页面
- 调用puppeteer截图服务,对web页面进行截图,并返回图片或者图片地址给调用者
先看看官网是怎么介绍的?
Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协定管制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行,然而能够通过批改配置文件运行“有头”模式。
你能够在浏览器中手动执行的绝大多数操作都能够应用 Puppeteer 来实现! 上面是一些示例:
- 生成页面截图或PDF。
- 抓取 SPA(单页利用)并生成预渲染内容(即“SSR”(服务器端渲染))。
- 主动提交表单,进行 UI 测试,键盘输入等。
- 创立一个时时更新的自动化测试环境。 应用最新的 JavaScript 和浏览器性能间接在最新版本的Chrome中执行测试。
- 捕捉网站的 timeline trace,用来帮忙剖析性能问题。
- 测试浏览器扩大。
几乎是「不明觉厉」!
配角退场
这篇文章的配角就是 page.screenshot
,通过它,咱们能够实现对指标网页(其实也就是承载海报内容的网页)的截图,而后开一个node接口服务,造成高可用的业务接口,将截图后的图片或者图片地址返回给调用者。
间接参考 screenshot文档 就能疾速实现一个截图服务,上面间接上代码,而后再说一些期间遇到的小问题及解决思路。
编写截图外围 screenshotPuppeteer.js
const path = require("path");const puppeteer = require("puppeteer");function sleep(delay = 1000) { return new Promise((resolve) => { setTimeout(() => { resolve(1); }, delay); });}function loginfo(debug, info) { if (debug) { console.log(info); }}/** * 基于「puppeteer」的服务端截图工具 * @param {*} options {...} 具体见下 @param {*} options.debug: false 是否开启调试 @param {*} options.pageUrl: "" 要截图的web地址 @param {*} options.defaultViewport: { width: 390, height: 844, deviceScaleFactor: 3, isMobile: true, } 是否开启Viewport模拟器 @param {*} options.headless: "" 是否有浏览器界面 @param {*} options.fileName: "" 截图文件保留名称 @param {*} options.fileSavePath: "datasource/poster/" 默认服务器代码写死 @param {*} options.fileType: "jpeg" 截图保留格局 [jpeg, png, webm] @param {*} options.quality: 100 压缩率[0,100],fileType=jpeg时无效 @param {*} options.fullPage: true 是否全屏,边滚动动边截图 @param {*} options.clip: null 指定裁剪区域,fullPage为true时不可用,默认null,容许参数:{ x: 0, y: 0, width: 390, height: 844 } @param {Number} screenshotDelay: 500 页面load后,触发截图之前的【延迟时间】 @param {*} options.closePage: true 截图完敞开页面(默认true敞开) @param {*} options.closeBrowser: true 截图完敞开浏览器(默认true敞开) @param {*} options.cb: null, }; */async function screenshotCore(config = {}) { let options = { debug: false, headless: true, //默认无头 pageUrl: "", //要截图的web地址 defaultViewport: { width: 390, height: 667, deviceScaleFactor: 3, isMobile: true, }, //是否开启Viewport模拟器 fileName: "", //截图文件保留名称 fileSavePath: "datasource/poster/", //默认服务器代码写死 fileType: "jpeg", //截图保留格局 [jpeg, png, webm] quality: 100, //压缩率[0,100],fileType=jpeg时无效 fullPage: true, //是否全屏,边滚动动边截图 clip: null, //指定裁剪区域,fullPage为true时不可用,默认null,容许参数:{ x: 0, y: 0, width: 390, height: 844 } screenshotDelay: 500, //页面load后,触发截图之前的【延迟时间】 closePage: true, //截图完敞开页面(默认true敞开) closeBrowser: true, //截图完敞开浏览器(默认true敞开) cb: null, ...config, }; loginfo( options.debug, `screenshotCore options: ${JSON.stringify(options, " ", "\t")}` ); let webpagePath = options.pageUrl || null; let saveType = options.fileType || "jpeg"; let file = `${options.fileName}.${saveType}`; let relativePath = `${options.fileSavePath}${file}`; let savePath = path.join(__dirname, "..", relativePath); let saveQuality = options.quality || 100; let isFullPage = options.fullPage || true; let clipArea = options.clip || null; let browser = null; let page = null; const errHandler = (msg, err) => { return { error: 1, msg, errMsg: err.message || err.msg, err, }; }; const successHandler = (msg, data = {}) => { return { error: 0, msg, data, }; }; const closeAll = async() => { if (options.closePage) { await page.close(); } if (options.closeBrowser) { await browser.close(); } }; return new Promise(async(resolve, reject) => { try { // 启动浏览器 browser = await puppeteer.launch({ args: ["--no-sandbox", "--disable-setuid-sandbox"], //如果报“No usable sandbox!” // slowMo: 100, //加快浏览器执行速度,不便测试察看 headless: options.headless, //是否为无界面拜访浏览器 defaultViewport: options.defaultViewport || null, }); } catch (err) { closeAll(); return reject(errHandler("POSTER利用启动失败", err.msg || err)); } try { // 新建页面 page = await browser.newPage(); page.setDefaultNavigationTimeout(60000); //超时报错timeout工夫,默认30s,0示意无限度 } catch (err) { closeAll(); return reject(errHandler("puppeteer关上新页面失败", err)); } // page.on("load", async () => { // loginfo(options.debug, "Page loaded!"); // await sleep(options.screenshotDelay); // do screenshot ... // }); // let reqNum = 0; // page.on("request", async (req) => { // let method = req.method(); // let url = req.url(); // console.log("request:", ++reqNum); // }); // let repNum = 0; // page.on("response", async (rep) => { // let url = rep.url(); // let status = rep.status(); // console.log("response:", ++repNum); // }); try { // 关上指标页面 { waitUntil: "networkidle0"} 示意以后页面500ms内无http申请,再返回 await page.goto(webpagePath, { waitUntil: "networkidle0" }); } catch (err) { closeAll(); return reject(errHandler("puppeteer关上指标网页失败", err)); } try { // 开始截图 await sleep(options.screenshotDelay); await page.screenshot({ path: savePath, //在服务器上的存储地位 type: saveType, quality: saveQuality, fullPage: isFullPage, clip: clipArea, }); loginfo(options.debug, `截图文件存储在了: ${savePath}`); let retData = { file, type: saveType, quality: saveQuality, }; resolve(successHandler("海报生成胜利", retData)); options.cb && options.cb(page); } catch (err) { reject(errHandler("puppeteer截图失败", err)); } closeAll(); });}module.exports = { screenshotCore,};
调用示例
const { screenshotCore } = require("./screenshotPuppeteer");screenshotCore({ pageUrl: "www.baidu.com", fileName: `capture_${+new Date()}` }) .then((res) => { console.log("success:", res); }) .catch((err) => { console.log("failed:", err); });
总结一些遇到的问题
puppeteer.launch启动报错
比方在本地开发没问题,部署到Linux服务器之后,遇到No usable sandbox!
或 ...setuid...
,须要在启动配置中减少 args: ["--no-sandbox","--disable-setuid-sandbox"]
// 启动浏览器browser = await puppeteer.launch({ args: ["--no-sandbox", "--disable-setuid-sandbox"], // slowMo: 100, //加快浏览器执行速度,不便测试察看});
截图呈现中文乱码?
这是因为服务器上没有中文字体的缘故,只需在Linux服务器的 usr/local/share/fonts
目录下增加中文字体包即可。
如果要完满还原指标页面的字体款式,还须要装置对应的字体文件。
截图不够清晰?
能够设置 page.setViewport(viewport)
,其中viewport
的deviceScaleFactor
代表着定义设施缩放, (相似于 dpr)。 默认 1。咱们能够依据想要的成果设置为2或者3(这是依据iPhone的屏幕dpr来的),当然这个值越高,图片体积也就回越大,所以要做好衡量。个别最多设置为3。
截图内容空白或不残缺?
有时候发现,尽管曾经是在page
实例的page.on('load')
里才开始截图,然而仍然会呈现空白内容。这是因为大部分状况,咱们的海报页面并不是纯动态的,也会有接口申请,而后再渲染一些动静内容。因而,须要做到「在接口申请完结,且短暂提早后」再触发截图操作。
能够这么做:
// networkidle0示意以后页面500ms内无http申请,再返回await page.goto(webpagePath, { waitUntil: "networkidle0" });
针对某个dom进行截图
//对页面某个元素截图let element = await page.$x('#target_dom');await element.screenshot({ ...});