主要功能

通过 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中的文件名
    }
}

前端肥智
116 声望7 粉丝

前端工程师