由先前的文章《 为什么 page.waitForResponse 只适合"短时间内"的场景? 》可知,page.waitForResponse
主要用于等待那些能迅速返回的响应。那么,对于需要长时间等待或者捕获多个响应的场景,我们应该如何处理呢?
答案是:我们可以借助 page.on('response', fn) 事件监听器来实现
。为了同时确保代码的健壮性和清晰的职责分离,最好的方法是将其封装成一个通用的异步函数,专门用于处理这类复杂的等待任务。
所以,接下来,我们先来看看封装的 监听单个响应、监听多个响应
通用函数及调用方式,之后我们再来分析封装的优势:
typescript
import puppeteer, { Page, HTTPResponse } from 'puppeteer-core';
// 监听单个响应
// 在外部定义 predicate 函数,可以使代码更加模块化、易于阅读和维护。
async function commonWaitForSingleResponsePromise(
page: Page,
options: { timeout: number; predicate: (response: HTTPResponse) => boolean },
): Promise<HTTPResponse> {
return new Promise((resolve, reject) => {
// 设置超时机制
const timeoutId = setTimeout(() => {
page.off('response', responseHandler); // 在超时后移除监听器
reject(new Error(`Timeout of ${options.timeout}ms exceeded.`));
}, options.timeout); // 等待超时时间
// 定义响应处理器
const responseHandler = (response: HTTPResponse) => {
if (options.predicate(response)) {
clearTimeout(timeoutId); // 清除超时定时器
page.off('response', responseHandler);
resolve(response);
}
};
// 设置监听器
page.on('response', responseHandler);
});
}
// 监听多个响应
async function commonWaitForMultiResponsePromise(
page: Page,
options: { timeout: number; predicate: (response: HTTPResponse) => boolean; matchedResponseCount: number },
): Promise<HTTPResponse[]> {
return new Promise((resolve, reject) => {
const _matchedResponses: HTTPResponse[] = [];
// 设置超时机制
const timeoutId = setTimeout(() => {
page.off('response', responseHandler); // 在超时后移除监听器
reject(
new Error(
`Timeout of ${options.timeout}ms exceeded. Collected ${_matchedResponses.length} of ${options.matchedResponseCount} responses.`,
),
);
}, options.timeout); // 等待超时时间
// 定义响应处理器
const responseHandler = (response: HTTPResponse) => {
if (options.predicate(response)) {
_matchedResponses.push(response);
if (_matchedResponses.length === options.matchedResponseCount) {
clearTimeout(timeoutId); // 清除超时定时器
page.off('response', responseHandler);
resolve(_matchedResponses);
}
}
};
// 设置监听器
page.on('response', responseHandler);
});
}
javascript
// 调用方
async function main() {
const _browser = await puppeteer.launch({
userDataDir: 'xxx',
});
const _page = await _browser.newPage();
const _predicate = (response: HTTPResponse): boolean => {
// 这个判断函数中,可以使用 response.url()、response.request().method()、response.request().postData() 等数据进行判断
// 比如 response.url().includes('your-target-api') 、 response.request().method() === 'POST'
// 尽量使用特别详细的指证方案
return true;
};
try {
// 监听多个响应时请使用 commonWaitForMultiResponsePromise
// 使用 Promise.all() 将等待 Promise 和页面导航操作结合在一起。这是解决 Puppeteer 竞态条件的标准模式,能确保监听器在触发请求之前就已经准备就绪,从而保证了代码的可靠性。
// 为什么要使用 Promise.all() ?因为我们的使用场景是先设置监听、再跳转页面、再同步等待结果返回,如果不使用promise.all(),则可能会出现问题:虽然设置监听的代码执行了,但是还没设置上的时候页面就执行了跳转,导致监听失效,从而导致等待超时。
// 而使用 Promise.all() 则可以确保在跳转页面之前,监听已经设置好,从而避免了这个问题。
const [_response] = await Promise.all([
commonWaitForSingleResponsePromise(_page, {
timeout: 180000,
predicate: _predicate,
}),
_page.goto('https://www.baidu.com', { waitUntil: 'domcontentloaded' }),
]);
if (_response && _response.ok()) {
// 获取到response之后,再从中获取数据 & 处理
const _result = await _response.json();
}
} catch (e) {
console.error(e); // 捕获超时错误
}
}
main();
封装函数的优势
除了确保代码的健壮性和开发的高效性,我们封装的这个通用函数还拥有以下核心优势:
-
完善的超时管理 :我们使用
setTimeout
设定了明确的超时时间,这能有效防止因响应过慢或永远不来而导致的资源长期占用问题。在等待结束或超时后,相关的资源会被及时释放; -
可靠的资源清理 :无论是任务成功完成还是超时失败,我们都通过
page.off()
手动移除了事件监听器。这一关键步骤杜绝了内存泄漏的隐患,保证了脚本的持续稳定运行; -
清晰的 Promise 生命周期 :这个
Promise
只有在找到目标响应或达到超时限制时才会结束。这种模式确保了异步操作的状态清晰可控;
调用方注意事项
虽然封装函数已经足够便捷和健壮,但是,在使用时,请特别注意以下两点,以发挥其最大效用:
-
解决竞态条件 :使用
Promise.all()
将等待函数和页面操作(如page.goto
)同时执行。这是 Puppeteer 中解决竞态条件的标准模式,能确保我们的监听器在网络请求触发之前就已经准备就绪,从而可靠地捕获到目标响应; -
精准的 predicate 函数 :为了避免意外匹配到非目标响应,请在
predicate
函数中提供尽可能详细的判断条件。例如,除了检查 URL,还可以同时验证请求方法、请求体数据等,以确保只找到你真正想要的响应。
写在最后
首先,感谢你读到此处。
其次,还是想再提醒一下:当前文章针对多个响应获取场景比较单一,以及,处理错误也比较武断,在实际使用时请务必根据业务场景进行实际封装。