原文参考我的公众号文章 前端生成海报新姿态 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),其中viewportdeviceScaleFactor代表着定义设施缩放, (相似于 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({    ...});