Puppeteer + Nodejs 通用全屏网页截图方案(三)页面处理

简介: 全屏网页截图程序页面处理技巧

如何在页面中判断图片已加载完成

主动调用注入函数进行截图的场景,通常都是我们自己的业务页面,这时我们可以在页面中对资源加载情况进行判断,来决定截图的时机。

起初我的想法是把图片url链接传进一个处理函数,函数中用Image对象加载src,当实例对象触发onload时就接着下一个,直到全部处理完毕那么也就算资源加载完成,但是很快我发现这样处理并不对,因为这个函数实际是异步的,很可能先加载完了资源但是该资源在页面中却并未加载完成,所以正确的方式应该是获取到页面当中真实的资源DOM节点,传入这个函数中处理,而且onload回调也不一定正确,经测试最稳定的方式是轮询complete属性来确定是否真的加载完成。

// Preload.ts 参考代码
export default class PreLoad {
  private i: number
  private arr: any[]
  constructor(arr: string[]) {
    this.i = 0
    this.arr = arr
  }
  public doms() {
    return new Promise((resolve: Function) => {
      const work = () => {
        if (this.i < this.arr.length) {
          this.arr[this.i].complete && this.i++ // 核心是轮询img节点的complete属性
          setTimeout(() => {
            work()
          }, 100)
        } else {
          resolve()
        }
      }
      work()
    })
  }
}

假设业务页面当中,内容是通过接口请求到前端渲染,为每个图片的div容器我都加上了img__box这个class样式,即<div class="img__box"> <img /> </div>这种形式,那么我可以这么处理:

const imgsData = []
const cNodes = document.querySelectorAll('.img__box')
        for (const el of cNodes) {
            imgsData.push(el.firstChild)
        }
        
const preload = new Preload(imgsData)
await preload.doms() // 实例化上面的Preload函数,开始轮询资源

console.log('--> 加载完成,可以开始截图')
try {
     window.loadFinishToInject('done') // 触发`Puppeteer`的注入方法
} catch (err) {}

懒加载页面处理方法

有时我们会遇到截取页面资源是懒加载的情况,像生成第三方网页文章时会非常不稳定,而且也不能通过单纯的sleep等待函数来解决问题。

所以我们需要加一个自动滚动的方法,来模拟真实的页面浏览触发页面的资源懒加载。

实现的核心是利用 Puppeteerevaluate 函数,改方法可以在目标页面上下文中执行JS代码,简单的触底判断:比较两次滚动后的scrollTop是否一致,如果一致就是页面不再往下滚了,即判断为触底。

// 创建自动滚动函数
async function autoScroll() {
  await page.evaluate(async () => {
    await new Promise((resolve, reject) => {
      try {
        const maxScroll = Number.MAX_SAFE_INTEGER
        let lastScroll = 0
        const interval = setInterval(() => {
          window.scrollBy(0, 100)
          const scrollTop = document.documentElement.scrollTop || window.scrollY
          if (scrollTop === maxScroll || scrollTop === lastScroll) { // 判断触底,或超出js最大安全长度
            clearInterval(interval)
            resolve()
          } else {
            lastScroll = scrollTop
          }
        }, 100) // 100毫秒执行间隔
      } catch (err) {
        console.log(err)
        reject(err)
      }
    })
  })
}

在截图前加入该函数:

page.on('load', async () => {
        await autoScroll() // 自动截图时先模拟滚动
        await sleep(wait) // 前面实现的等待方法
        // 开始截图
        await page.screenshot({ path, fullPage: true })
        // 关闭浏览器
        await browser.close()
})

这样当页面加载完成后,就会触发自动滚动,每100毫秒向下滚100像素,直到触底为止,跳出Promise,配合前面我们实现的wait参数,等待 x 毫秒后开始截图(这里滚动只是触发了资源加载,如果不等待一下资源有可能没加载完)

最终结果如下,大部分页面的情况应该都差不多:

正常截图 加入自动滚动
image.png image.png

页面打开处理方式

1. 每个页面由单独的浏览器实例打开

前面都是使用该方式,所以每次执行完毕之后都会手动关闭浏览器以释放内存,可以设置一个超时处理模块来销毁浏览器实例:

const forceTimeOut = 60000 // 超时时间限制

const regulators = setTimeout(() => {
      browser && browser.close()
      console.log('强制释放浏览器')
    }, forceTimeOut)
    
// 关闭浏览器
await browser.close()
clearTimeout(regulators)
这种方式的好处是能保证单次任务执行的稳定性,每次执行完毕都销毁浏览器,没有常驻内存。

缺点是无法充分利用到浏览器缓存。存在多个并行任务时内存消耗会更大,需要用队列控制一下,降低了任务并行上限。

2. 在Tab标签打开页面

2022-06-13 10.53.14.gif

    // 启动浏览器,启动后不再销毁该实例
    const browser = await puppeteer.launch({
      ........
    })
    // 创建复数个新页面
    const pageA = await browser.newPage()
    const pageB = await browser.newPage()
    ..... 
    await pageB.goto(url, { waitUntil: 'domcontentloaded' })
    // 关闭标签页
    await pageA.close()
这种方式可以利用浏览器缓存,让引用相同资源的页面在加载时更快,一个标签页占用的内存肯定比一个浏览器实例要少,也就提高了并行任务上限。

缺点是有常驻内存,时间长了免不了会出现内存泄漏等问题,所以定时重启实例释放内存还是有必要的。需要控制一个浏览器实例最大打开的标签页数量,避免开太多标签页导致卡顿。

「隐身」独立浏览器会话

2022-06-13 10.57.04.gif

除了使用 puppeteer.launch 创建浏览器实例以外,还可以用另一种方式,在同个浏览器实例下创建多个「隐身」独立浏览器会话,效果其实和创建多个实例差不多,不能共享缓存等信息(有点像打开新的窗口),但是销毁 browser 的时候会把所有上下文都一并关闭,这种方式官方的说法是: 「隐身」浏览器上下文不会将任何浏览数据写入磁盘,感觉在业务上没啥作用,不过还是记录一下:

    // 启动浏览器,启动后不再销毁该实例
    const browser = await puppeteer.launch({
      ........
    })
    // 创建浏览器上下文
    const newContextA = await browser.createIncognitoBrowserContext()
    const newContextB = await browser.createIncognitoBrowserContext()
    // 在不同的窗口中打开新标签页
    const pageA = await newContextA.newPage()
    const pageB = await newContextA.newPage()
    const page1 = await newContextB.newPage()
    const page2 = await newContextB.newPage()

生成缩略图

截图生成的图片是未经处理的,在原始分辨率下可能会比较大,在实际业务中不适合直接展示,如果图片上传至OSS服务中可以方便地获得各种尺寸缩略图,否则可以使用 images 这个库在每次截图后顺便生成一下缩略图。

const images = require('images')
const size = 300 // 等比缩放到300像素宽
const quality = 90 // 压缩质量:1-100,质量越小图片占用空间越小

const filePath = process.env.NODE_ENV === 'development' ? process.cwd() + `/static/` : '/cache/'
// 保存截图的路径及文件名
const path = filePath + `screenshot_${new Date().getTime()}.png`
// 保存缩略图的文件名,路径及名称和截图一样,只是把后缀改了
const thumbPath = path.replace('.png', '.jpg')

// 创建浏览器
// ...........
// console.log('-> 开始截图')
      await page.screenshot({ path, fullPage: true })
      
// 生成缩略图
      compress()

function compress() {
      // 压缩图片
        thumbPath &&
          images(path).size(+size || 300).save(thumbPath, { quality: +quality || 70 })
      } catch (err) {
        console.log(err)
      }
}
相关文章
|
11月前
npm install 报错 npm ERR! puppeteer@1.20.0 install: `node install.js`
npm install 报错 npm ERR! puppeteer@1.20.0 install: `node install.js`
265 0
|
12月前
|
数据采集 Web App开发 资源调度
如何使用Puppeteer在Node JS服务器上实现动态网页抓取
Puppeteer的核心功能是提供了一个Browser类,它可以启动一个Chrome或Chromium浏览器实例,并返回一个Browser对象。Browser对象可以创建多个Page对象,每个Page对象对应一个浏览器标签页,可以用来加载和操作网页。Page对象提供了一系列的方法,可以模拟用户的各种行为,如输入、点击、滚动、截图、PDF等。Page对象还可以监听网页上的事件,如请求、响应、错误、加载等。通过这些方法和事件,可以实现对动态网页的抓取。
335 0
如何使用Puppeteer在Node JS服务器上实现动态网页抓取
|
存储 缓存 JavaScript
【Node.js 】开发中遇到的多进程‘keylog‘ 事件以及TLS/SSL的解决学习方案实战
【Node.js 】开发中遇到的多进程‘keylog‘ 事件以及TLS/SSL的解决学习方案实战
【Node.js 】开发中遇到的多进程‘keylog‘ 事件以及TLS/SSL的解决学习方案实战
|
数据采集 运维 资源调度
|
数据采集 编解码 移动开发
Puppeteer + Nodejs 通用全屏网页截图方案(二)常用参数实现
学习如何对网页截图程序设计基本参数功能。
|
数据采集 JavaScript
Puppeteer + Nodejs 通用全屏网页截图方案(一)基本功能
学习一个网页截图程序的实现基本功能。
|
存储 SQL JavaScript
解秘 Node.js 单线程实现高并发请求原理,以及串联同步执行并发请求的方案
最近在做一个支持多进程请求的 Node 服务,要支持多并发请求,而且请求要按先后顺序串联同步执行返回结果。 对,这需求就是这么奇琶,业务场景也是那么奇琶。 需求是完成了,为了对 Node.js 高并发请求原理有更深一些的理解,特意写一篇文章来巩固一下相关的知识点。
851 0
解秘 Node.js 单线程实现高并发请求原理,以及串联同步执行并发请求的方案
|
JavaScript 内存技术
史上最详细nodejs版本管理器nvm的安装与使用(附注意事项和优化方案)
使用场景 在Node版本快速更新迭代的今天,新老项目使用的node版本号可能已经不相同了,node版本更新越来越快,项目越做越多,node切换版本号的需求越来越迫切,传统卸载一个版本在安装另一个版本的方式太过于麻烦,这也是nvm能够流行的原因。
2031 0
|
3天前
|
缓存 JavaScript 安全
2022年最新最详细的安装Node.js以及cnpm(详细图解过程、绝对成功)
这篇文章提供了2022年最新最详细的Node.js和cnpm安装教程,包括步骤图解、全局配置路径、cnpm安装命令、nrm的安装与使用,以及如何管理npm源和测试速度。
2022年最新最详细的安装Node.js以及cnpm(详细图解过程、绝对成功)
|
3天前
Mac 安装 Node Error: Could not symlink include/node/common.gypi
Mac 安装 Node Error: Could not symlink include/node/common.gypi
9 3