背景
实现前端页面自动生成截图功能,如订阅页面数据场景,以页面截图方式推送
项目初始化
js
npm init
puppeteer安装
js
// 未安装pm2需先全局安装pm2
npm install pm2 -g
// 依赖express启动服务 compression压缩 puppeteer完成截图
// puppeteer 会自动安装chrome无头浏览器,版本不同,安装逻辑不同
npm install express compression puppeteer
项目启动配置
根目录新建ecosystem.config.js
js
module.exports = {
apps: [
{
name: 'project-name',
script: './app.js', // 启动执行文件
max_memory_restart: '200M', // 内存超800M重启,因为无头浏览器占用内存较大,防止同机器上其他应用崩溃,设置重启
watch: false,
error_file: './pm2-error.log', // 错误日志收集
out_file: './pm2-out.log', // 访问日志收集
log_date_format: 'YYYY-MM-DD HH:mm:ss',
exec_mode: "cluster", // 运行模式,可选 cluster, fork
env: {
PORT: 3000,
NODE_ENV: 'development',
},
env_production: {
PORT: 3000,
NODE_ENV: 'production',
},
}
]
};
修改package.json启动脚本
json
"scripts": {
"serve": "pm2 start ecosystem.config.js --env development",
"serve:prod": "pm2 start ecosystem.config.js --env production",
"stop": "pm2 stop all",
"delete": "pm2 delete all"
},
核心代码
根目录新建app.js
js
const express = require('express');
const puppeteer = require('puppeteer');
const compression = require('compression');
const app = express();
const port = process.env.PORT || 3000;
// 启用 Gzip 压缩
app.use(compression());
// 最大并发数
const MAX_CONCURRENT = 5;
// 当前活动任务数
let activeCount = 0;
// 请求队列
const requestQueue = [];
// Puppeteer 页面处理逻辑
const handlePageScreenshot = async () => {
const targetURL = process.env.WEB_URL // 目标url,可固定配置,也可根据请求动态改变;
let browser, page;
try {
// 启动浏览器
browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
// 创建页面
page = await browser.newPage();
// 调整视口规范
await page.setViewport({
width: 1280,
height: 720,
deviceScaleFactor: 2
});
// 导航到目标 URL,并等待页面加载完成,包括页面所有api请求,networkidle0是最严格的条件,确保页面所有异步请求全部完成
// timeout: 设置超时,0 禁用超时
await page.goto(targetURL, { waitUntil: 'networkidle0', timeout: 0 });
console.log('成功导航到目标URL')
// waitForFunction 可自定义等待条件
await page.waitForFunction(() => {}, { timeout: 0 });
console.log('所有api请求完成')
// 可选等待 某特定DOM 元素加载完成
await page.waitForSelector('#selector', { timeout: 0 });
console.log('dom加载完成')
// Puppeteer 核心方法,可修改目标元素样式等所有js逻辑,并返回到node环境中,比如存在滚动条会导致页面截图不完整,此时需要人工介入调整元素样式
await page.evaluate(({ selector, needCountWidthSelector }) => {
// js处理逻辑
}, { selector, needCountWidthSelector });
// 获取处理完之后的目标dom
const element = await page.$(selector);
if (!element) {
throw new Error('目标元素未找到');
}
// 目标盒子
const boundingBox = await element.boundingBox();
// 如果页面存在动画,需延时等待动画完成,延迟时间根据实际情况而定
await delay(1000);
// 截图保存为 Base64
const screenshotBase64 = await page.screenshot({
encoding: 'base64',
clip: {
x: boundingBox.x,
y: boundingBox.y,
width: boundingBox.width,
height: boundingBox.height,
},
});
return screenshotBase64;
} catch (error) {
console.error('截图处理失败:', error);
throw error; // 将错误抛出以供上层捕获
} finally {
// 截图完成,关闭浏览器,释放内存
if (page) await page.close();
if (browser) await browser.close();
}
};
// 队列任务处理函数,防止启动浏览器数量太多,内存撑爆
const processTask = async (task) => {
const screenshotBase64 = await handlePageScreenshot({ projectId, reportId });
res.send(screenshotBase64);
console.log('截图成功并返回');
} catch (error) {
console.error('任务处理出错:', error);
res.status(500).send('服务器内部错误');
} finally {
activeCount--; // 减少活动任务计数
processNext(); // 处理队列中的下一个任务
}
};
// 处理队列中的下一个任务
const processNext = () => {
console.log(`开始处理队列: 当前队列长度 ${requestQueue.length}, 活动任务数 ${activeCount}`);
while (activeCount < MAX_CONCURRENT && requestQueue.length > 0) {
const nextTask = requestQueue.shift(); // 从队列中取出任务
activeCount++;
processTask(nextTask); // 执行任务
}
};
// API 路由
app.get('/getPage', (req, res) => {
// 将任务加入队列
requestQueue.push({ req, res });
processNext(); // 尝试处理队列中的任务
});
// 启动服务
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
注意事项
- puppeteer 版本,及浏览器安装方式一级版本适配问题
- 服务器需安装字体包,识别中文,避免截图乱码