主要功能
通過 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中的文件名
}
}