Puppeteer page.on('response',fn)的最佳实践之等待响应

由先前的文章《 为什么 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();

封装函数的优势

除了确保代码的健壮性和开发的高效性,我们封装的这个通用函数还拥有以下核心优势:

  1. 完善的超时管理 :我们使用 setTimeout 设定了明确的超时时间,这能有效防止因响应过慢或永远不来而导致的资源长期占用问题。在等待结束或超时后,相关的资源会被及时释放;

  2. 可靠的资源清理 :无论是任务成功完成还是超时失败,我们都通过 page.off() 手动移除了事件监听器。这一关键步骤杜绝了内存泄漏的隐患,保证了脚本的持续稳定运行;

  3. 清晰的 Promise 生命周期 :这个 Promise 只有在找到目标响应或达到超时限制时才会结束。这种模式确保了异步操作的状态清晰可控;

调用方注意事项

虽然封装函数已经足够便捷和健壮,但是,在使用时,请特别注意以下两点,以发挥其最大效用:

  1. 解决竞态条件 :使用 Promise.all() 将等待函数和页面操作(如 page.goto)同时执行。这是 Puppeteer 中解决竞态条件的标准模式,能确保我们的监听器在网络请求触发之前就已经准备就绪,从而可靠地捕获到目标响应;

  2. 精准的 predicate 函数 :为了避免意外匹配到非目标响应,请在 predicate 函数中提供尽可能详细的判断条件。例如,除了检查 URL,还可以同时验证请求方法、请求体数据等,以确保只找到你真正想要的响应。

写在最后

首先,感谢你读到此处。

其次,还是想再提醒一下:当前文章针对多个响应获取场景比较单一,以及,处理错误也比较武断,在实际使用时请务必根据业务场景进行实际封装。

相关推荐
跟橙姐学代码2 小时前
给Python项目加个“隔离间”,从此告别依赖纠缠!
前端·python·ipython
Cache技术分享2 小时前
202. Java 异常 - throw 语句的使用
前端·后端
_AaronWong2 小时前
Electron全局搜索框实战:快捷键调起+实时高亮+多窗口支持
前端·搜索引擎·electron
笔尖的记忆2 小时前
渲染引擎详解
前端
大明二代2 小时前
为 Angular Material 应用添加完美深色模式支持
前端
Mintopia2 小时前
🚪 当 Next.js 中间件穿上保安制服:请求拦截与权限控制的底层奇幻之旅
前端·后端·next.js
Mintopia2 小时前
🚗💨 “八缸” 的咆哮:V8 引擎漫游记
前端·javascript·v8
源去_云走2 小时前
npm 包构建与发布
前端·npm·node.js
Sport3 小时前
面试官:聊聊 Webpack5 的优化方向
前端·面试