主要功能
通过 puppeteer 打开无头浏览器,对目标页面进行截图存储本地制定目录,通过把本地文件转为文件流的方式上传华为云,上传成功过后返回预览文件的 key;
上传华为云通过 ObsClient、putObject 方式实现 [华为云 OBS Nodejs SDK](https://support.huaweicloud.com/sdk-nodejs-devg-obs/obs_29_0403.html)
依赖包版本介绍
- Nodejs:v16.3.0
- Eggjs:^3
- puppeteer:^21.2.1
- esdk-obs-nodejs:^3.24.3
核心代码&逻辑说明
截图核心代码
- 截图的图片高度根据页面给定的dom元素的高度来计算(在page.setViewport中height体现);我这里默认给了个“content”的class,接口入参支持传element的class;
- 把截图的页面图片存储到本地的指定目录,
const puppeteer = require('puppeteer');
const { getParamsValueFromUrl } = require('../utils/tools');
/**
* @description 执行截图逻辑 --> 生成截图 --> 存到根目录 --> 关闭浏览器
* @param {*} page
* @param {*} baseDir
* @param {*} element 要获取的目标元素,默认'.element'
* @return {*}
*/
const asyncGeneratePng = (page, baseDir, element) => {
return new Promise((resolve, reject) => {
page.once('load', async () => {
try {
// 获取元素的高度,如果元素不存在则返回0
const elementHeight = await page.evaluate((element) => {
const targetElement = document.querySelector(`.${element}`);
return targetElement ? targetElement.clientHeight : 0;
}, element);
console.log('Element height:', elementHeight);
if (elementHeight > 0) {
await page.setViewport({
width: 1920,
height: elementHeight,
});
await page.waitForTimeout(2000); // 等待2秒
const screenshotOptions = {
path: `${baseDir}/screen-shot.png`,
fullPage: true,
type: 'png',
waitUntil: 'domcontentloaded',
};
// 获取当前页面URL中screen的值
const currentUrl = page.url();
const pageType = getParamsValueFromUrl(currentUrl);
if (pageType === 'pdf') {
const tableDataRendered = await page.evaluate(() => {
return document.querySelectorAll('table tr').length > 0;
});
if (tableDataRendered) {
await page.screenshot(screenshotOptions);
}
} else {
await page.screenshot(screenshotOptions);
}
} else {
console.log('未获取到元素高度,截图失败');
}
resolve();
} catch (error) {
reject(error);
}
});
});
};
/**
* 截屏的核心代码
* @param {*} url 要截图的网站地址
* @param {*} name 截图之后保存的文件名
* @param baseDir
* @param element
*/
const snapShot = async (url, baseDir, element) => {
const conf = {
headless: true, // 设置为无头模式
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--ignore-certificate-errors',
// '--proxy-server=${newProxyUrl}'
],
dumpio: true,
slowMo: 100, // 设置浏览器每一步之间的时间间隔,单位毫秒
};
// 启动无头浏览器
const browser = await puppeteer.launch(conf);
// 创建新页面
const page = await browser.newPage();
try {
// 跳转目标页面
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
// 执行截图逻辑
await asyncGeneratePng(page, baseDir, element);
} catch (error) {
console.error(error);
} finally {
await browser.close(); // 关闭浏览器
}
};
module.exports = snapShot;
截图Controller代码
'use strict';
const BaseController = require('./base');
// const getObsData = require('./obs');
const uploadFile = require('./upload');
/**
* @description 生成页面截图的文件流 --> 调用upload Controller
* @class ShotController
* @augments {Controller}
*/
class ShotController extends BaseController {
constructor(that) {
super(that);
// this.getObsDataController = new getObsData(that);
this.uploadFileController = new uploadFile(that);
}
/**
* 调用截图Controller --> 调用上传OBSController --> 删除本地生成的文件
*
* @return 上传至obs的文件key
*/
async generateSnapShotBufferUpload() {
const { url, appname, element } = this.ctx.request.body;
try {
await this.service.screenShot.screenShot({
url, // 目标页面url
appname, // 存储到obs的目录
element: element || 'content', // 要截取页面的目标元素, 默认是content
});
// 调用上传文件Controller
const uploadResponse = await this.uploadFileController.uploadFileToOBS(appname);
if (uploadResponse) {
const response = {
fileKey: uploadResponse,
};
this.success(response);
// 成功返回后,删除本地生成的截图文件
this.deleteLocalFile(this);
}
} catch (error) {
this.faild(error);
}
}
}
module.exports = ShotController;
使用到的工具函数
getParamsValueFromUrl: (url, value = 'screenshot') => {
const urlParams = new URLSearchParams(url);
return urlParams.get(value);
}
上传华为云核心代码
- 使用strem的形式通过obs.putObject的方法进行上传;通过fs.createReadStream将本地的png文件转为obs可识别可读流
const BaseController = require('./base');
const ObsClient = require('esdk-obs-nodejs');
const fs = require('fs');
const sendToWormhole = require('stream-wormhole');
const { randomString } = require('../utils/tools');
/**
* @description 文件上传 --> 上传华为云obs
* @class UploadFileController
* @augments {BaseController}
*/
class UploadFileController extends BaseController {
async uploadFileToOBS(appname) {
const { ctx } = this;
const filePath = `${ctx.app.config.baseDir}/screen-shot.png`;
// 检查本地文件是否生成
const result = await this.waitForFile(filePath);
// 防止本地文件生成失败,服务死循环
if (result) {
// 创建可读流
const stream = await fs.createReadStream(filePath);
const fileName = `${randomString(30)}.png`;
try {
console.log('this.app.config,.,.,.,.,.,.', this.app.config);
const { obsData } = this.app.config;
// 获取obs配置
const obs = new ObsClient({
access_key_id: obsData.ak,
secret_access_key: obsData.sk,
server: obsData.endPoint,
});
// 调用OBS插件提供的方法将图片上传到OBS
const fileKey = `${fileName}`;
const result = await obs.putObject({
Bucket: obsData.bucketName,
Key: fileKey,
Body: stream,
ACL: 'public-read', // 设置上传对象的ACL权限为公共读
});
console.log('result,.,.,.,.', result);
if (result.CommonMsg.Status !== 200) {
this.faild(result);
return;
}
// 返回图片的在obs的存储key值
return fileKey;
} catch (error) {
// 必须将上传的文件流消费掉,要不然浏览器响应会卡死
await sendToWormhole(stream);
this.faild(error);
}
} else {
this.faild('文件不存在');
}
}
}
module.exports = UploadFileController;
使用到的工具函数和baseController
// 生成随机数用于文件名
randomString: (len) => {
len = len || 32;
const chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
const maxPos = chars.length;
let pwd = '';
for (let i = 0; i < len; i++) {
pwd += chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
}
const Controller = require('egg').Controller;
const fs = require('fs');
const path = require('path');
/**
* @description 公共controller,定义返回结构
* @class BaseController
* @augments {Controller}
*/
class BaseController extends Controller {
constructor(ctx) {
super(ctx);
this.count = 0;
}
success(data = null, message = 'success', code = 200) {
const { ctx } = this;
ctx.status = 200;
ctx.body = {
code,
message,
data,
};
}
faild(data = null, message = 'faild', code = 400) {
const { ctx } = this;
ctx.status = 400;
ctx.body = {
code,
message,
data,
};
}
/**
* 等待文件存在
* @param filePath 文件路径
* @return Promise<void>
*/
async waitForFile(filePath) {
let result = false;
try {
// 检查文件是否存在
await new Promise((resolve, reject) => {
// eslint-disable-next-line node/prefer-promises/fs
fs.stat(filePath, (err, stats) => {
if (err) {
reject(err);
} else {
resolve(stats);
result = true;
}
});
});
} catch (error) {
if (error.code === 'ENOENT') {
this.count++;
// 文件不存在, 等待 100ms 后重试
await new Promise((resolve) => setTimeout(resolve, 100));
// 防止本地文件生成失败,服务死循环
if (this.count > 50) {
return false;
}
return await this.waitForFile(filePath);
}
throw error;
}
return result;
}
/**
* 异步删除文件
* @return 删除结果
*/
async deleteLocalFile() {
const { ctx } = this;
const filePath = path.join(ctx.app.config.baseDir, '/', 'screen-shot.png');
// eslint-disable-next-line node/prefer-promises/fs
fs.unlink(filePath, (err) => {
if (err) {
this.faild('删除文件失败');
} else {
this.success('删除文件成功');
}
});
}
}
module.exports = BaseController;
接口返回实例
{
"code": 200,
"message": "success",
"data": {
"fileKey": "5pfdrt7wyRQDapwndJ4k7WieJZNnj3.png" // fileKey:obs中的文件名
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。