• puppeteer实现网页截图


    上周接到接到一个需求,将某个页面整体截图,并定时发送邮件。

    这里我将其拆解成三个大步骤:

    1、实现页面整体截图

    2、发送邮件

    3、定时发送

    本文主要记录实现截图过程中遇到的一些问题和整体复盘。

    一、实现过程中遇到的问题

    1、页面中存在竖向滚动,如何截全屏?

    涉及知识点: puppeteer 模拟滚动

    实现过程中参考了 Puppeteer前端检测最佳实践 - 知乎 这篇回答中的答案。

    前期尝试 通过配置 fullPage 为 true, 结果发现没有成功。

      await page.screenshot({path: 'pics/demo1.png', fullPage: true});
    
    • 1

    后来发现,设置的视口高度要和全屏高度一致。 而想要得到全屏高度,则需要找到页面中的滚动元素,得到其滚动高度。

    在参考文中,使用了计算的方式找到滚动元素,而在我们项目中,滚动元素是固定的,因此,直接计算滚动元素高度即可。

    /** 获取无头浏览器打开页面的 某dom 高度 */
    export const calHeight = async (page: any, dom: string) => {
      const scrollHeight = await page.$eval(dom, el => el.scrollHeight);
      return scrollHeight;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在获得滚动高度后,还有仍需注意的一点。这里只是滚动高度,全局是大于滚动高度的。

    在这里插入图片描述

    如上图所示,puppeteer的视口是从页面左上角开始计算的,而一般情况下,页面的滚动都是内容区滚动,上面置顶。

    因此,需要将viewPort的height 计算为 滚动部分高度+ 固定部分高度。截图可以选择只截滚动内容。只需要设置clip 中的 x 和 y 即可。

    这部分的核心代码如下:

    // 引入puppeteer设置
    const puppeteer = require('puppeteer');
    // 启动浏览器
    const browser = await puppeteer.launch();
    //创建新页面
    const page = await browser.newPage();
    // 设置cookies
    cookies && await page.setCookie(...cookies);
    // 跳转url
      const response = await page.goto(url, {
        waitUntil: 'networkidle0',
        timeout: timeout || 120000
      });
    // 设置视口高度
    await page.setViewport({ width, height: height + y + 20 });
    // 设置截图位置
    await page.screenshot({ path: shotPath, clip: { x, y, height: height, width: width }, });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    2、页面中局部存在横向滚动,如何实现截全屏?

    解决了竖向的滚动问题,但是,在页面中,是仍然存在局部滚动。

    请添加图片描述
    如上图所示,因页面中存在表格,表格内是存在局部滚动的。这种情况下,我在控制台找到了可以获得实际表格宽度的元素。而每个表格中都有这个元素,就可以在找到所有表格宽度后,取最大值。这样就可以截取整屏宽度啦。

    /** 获取无头浏览器打开页面的 某dom 宽度 */
    export const calWidth = async (page: any, dom: string) => {
      let maxWidth = 0;
      const width = await page.$$eval(dom, el => el.map(e => e.clientWidth));
      width.forEach(w => maxWidth = w > maxWidth ? w : maxWidth);
      return maxWidth;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3、页面中局部存在竖向滚动,如何实现截全屏?

    局部竖向滚动指的表格的竖向滚动,这是因为开发表格组件过程中,固定了表格组件的高度。

    因此,在渲染的时候,只需要更改表格高度即可。 于是,问题就变成了,

    如何通过url传递参数,改变页面渲染?

    1、取到url中的参数

    2、改变表格高度

    项目中使用的vue框架,vue 获取url 参数的方式是:

      /** 事件ID */
      get urlTableHeight () {
        const height = this.$route.query.tableHeight as string;
        return height ? parseInt(height) : '';
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    而改变表格高度通过组件传参完成。

    4、截图的时候,如何保证页面中所有数据已经加载完成?

    因为页面存在加载时长和懒加载等问题,必须保证,页面中所有数据均已加载完成才能进行截图。

    通过两步来实现:

    ① 跳转url的时候,添加 waitUntil:‘networkkidle0’ 的参数。networkkidle0 的意思是 在 500ms 内没有网络连接时就算成功(全部的request结束),才认为导航结束。

    ② 由于我们的这个数据实在太多了,可能在限定时间内依然没有加载完,因此,又使用了pendingXHR 来进行双重保障。

    最终代码为:

    /** 获取具体的页面响应 */
    export const getPageResponse = async (url: string, _page?: any, timeout?: number, waitXhrAll = true, cookies?: Array<{ name: string, value: string, domain: string }>) => {
      let pendingXHR;
      const page = _page || await getBrowserPage(cookies);
      if (!page) return null;
    
      if (waitXhrAll) {
        pendingXHR = new PendingXHR(page);
      }
    
      const response = await page.goto(url, {
        waitUntil: 'networkidle0',
        timeout: timeout || 120000
      });
    
      if (response.status() !== 200) {
        logger(`跳转到${url}失败`, 'error');
        return null;
      }
    
      // 等当前的xhr请求都返回
      waitXhrAll && await pendingXHR.waitForAllXhrFinished();
    
      return response;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    二、整体复盘

    1、代码结构

    在review过程中,组长告诉我,虽然完成了截图功能的开发,但是整体代码没有复用性,排查问题也比较困难。

    整个截图过程是主要包括如下几个事情的:

    ① 创建页面

    ② 获取页面内容

    ③ 截图

    而最开始,我是把这些代码全部写在了一个函数中,虽然后期的确是复用这个大函数完成,但是这些代码全部融在一个函数中,即难阅读,也难以排查问题。

    最好的办法是将其进行函数拆分。

    函数拆分过程中,一个技巧就是,要注意函数的命名,入参,出参。

    通过函数命名就能知道,这个函数做了什么。 通过入参的命名知道需要传递什么参数,通过出参明确函数返回什么。

    2、异常处理

    还有一个问题就是,在开发过程中,不注意异常处理。

    前端代码中,往往都是业务代码,并且排查可以通过界面排查,但是后端代码,感觉需要详细的异常处理,不然很难排查问题。

    那么,问题就来了,什么时候需要抛出异常呢?

    需要抛出异常的三种情况:

    ①不可控因素 ②不可完全预测异常

    ①② 常出现在调用第三方库的时候

    ③ 希望带上异常调用栈

    错误日志记录

    常常在业务代码使用,譬如在定义post接口的返回的时候,返回为 {data: any, error: Error | null})。

    在明确下层调用者的情况下,返回明确的错误,能降低理解成本。

    在不明确调用者的情况下,譬如多数框架,均是向上抛出异常。


    明确的返回,是错误;异常处理的返回结果;
    异常,就是未知的

    三、参考链接

    1、puppeteer api文档:https://zhaoqize.github.io/puppeteer-api-zh_CN/#?product=Puppeteer&version=v14.4.1&show=api-overview​​​​​​​

    2、获取元素宽高:css宽高

    3、 url传参:Vue 路由组件传参的 8 种方式 - SegmentFault 思否

    4、滚动问题:Puppeteer前端检测最佳实践 - 知乎

  • 相关阅读:
    【公众号文章备份】从零开始学或许是一个谎言
    基于stm32单片机的电压报警系统Proteus仿真
    pytorch加载的cifar10数据集,到底有没有经过归一化
    PAT 1048 Find Coins
    2023年 MOOC《计算机网络》—— 第四章CSMA/CD作业答案解析(手写版)
    SAM资料
    【Spring Cloud实战】消费者直接调用提供者(案例)
    【C++入门到精通】C++入门 —— 红黑树(自平衡二叉搜索树)
    Jmoon极萌诠释“大”科技故事,让“极速变美”成为可能
    Sophus安装
  • 原文地址:https://blog.csdn.net/qq_34539486/article/details/125561752