原文参考我的公众号文章 前端生成海报新姿态 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({
...
});
发表回复