如何在页面中判断图片已加载完成
主动调用注入函数进行截图的场景,通常都是我们自己的业务页面,这时我们可以在页面中对资源加载情况进行判断,来决定截图的时机。
起初我的想法是把图片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
等待函数来解决问题。
所以我们需要加一个自动滚动的方法,来模拟真实的页面浏览触发页面的资源懒加载。
实现的核心是利用 Puppeteer
的 evaluate
函数,改方法可以在目标页面上下文中执行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 毫秒后开始截图(这里滚动只是触发了资源加载,如果不等待一下资源有可能没加载完)
最终结果如下,大部分页面的情况应该都差不多:
正常截图 | 加入自动滚动 |
---|---|
页面打开处理方式
1. 每个页面由单独的浏览器实例打开
前面都是使用该方式,所以每次执行完毕之后都会手动关闭浏览器以释放内存,可以设置一个超时处理模块来销毁浏览器实例:
const forceTimeOut = 60000 // 超时时间限制
const regulators = setTimeout(() => {
browser && browser.close()
console.log('强制释放浏览器')
}, forceTimeOut)
// 关闭浏览器
await browser.close()
clearTimeout(regulators)
这种方式的好处是能保证单次任务执行的稳定性,每次执行完毕都销毁浏览器,没有常驻内存。缺点是无法充分利用到浏览器缓存。存在多个并行任务时内存消耗会更大,需要用队列控制一下,降低了任务并行上限。
2. 在Tab标签打开页面
// 启动浏览器,启动后不再销毁该实例
const browser = await puppeteer.launch({
........
})
// 创建复数个新页面
const pageA = await browser.newPage()
const pageB = await browser.newPage()
.....
await pageB.goto(url, { waitUntil: 'domcontentloaded' })
// 关闭标签页
await pageA.close()
这种方式可以利用浏览器缓存,让引用相同资源的页面在加载时更快,一个标签页占用的内存肯定比一个浏览器实例要少,也就提高了并行任务上限。缺点是有常驻内存,时间长了免不了会出现内存泄漏等问题,所以定时重启实例释放内存还是有必要的。需要控制一个浏览器实例最大打开的标签页数量,避免开太多标签页导致卡顿。
「隐身」独立浏览器会话
除了使用 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)
}
}