5月28日 07:08

Puppeteer 中有哪些等待机制?如何正确使用它们来处理异步操作?

核心答案

Puppeteer 提供了 8 种等待机制,按场景分为四类:

  • 导航等待waitForNavigation() — 等待页面跳转完成
  • 元素等待waitForSelector()waitForXPath() — 等待 DOM 元素出现
  • 网络等待waitForResponse()waitForRequest() — 等待网络请求或响应
  • 自定义等待waitForFunction()waitForFrame()waitForTimeout()(已废弃)

选择原则:导航操作用 waitForNavigation,元素操作用 waitForSelector,API 调试用 waitForResponse,复杂条件用 waitForFunction。永远不要用 waitForTimeout 做硬等待,它已被废弃。

追问:waitForNavigation 和 click 为什么要用 Promise.all 包裹?

因为 click 触发的导航是异步的,如果先 click 再 await waitForNavigation,导航可能在 click 返回前就已经完成了,导致 waitForNavigation 永远等不到。用 Promise.all 让两者同时开始监听,才能确保不丢失导航事件。

导航等待:waitForNavigation

page.waitForNavigation() 等待页面发生导航并完成加载,典型场景是点击链接、提交表单。

javascript
// 正确写法:用 Promise.all 并行等待 await Promise.all([ page.waitForNavigation(), page.click('#submit-button') ]);

waitUntil 参数决定"加载完成"的标准,四个选项适用场景不同:

选项触发条件适用场景
loadwindow.onload 触发静态页面
domcontentloadedDOM 解析完毕只需 DOM 不等资源
networkidle0500ms 内无网络请求SPA 应用,等全部接口返回
networkidle2500ms 内 ≤2 个网络请求有长连接或轮询的页面

networkidle2 是实际项目中最常用的选项。很多页面有 WebSocket 长连接或统计上报,用 networkidle0 会永远等不到空闲,networkidle2 允许最多 2 个连接,正好覆盖这种情况。

超时时间通过 timeout 参数设置,默认 30 秒。如果页面加载慢,可以调大:

javascript
await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 60000 });

元素等待:waitForSelector 与 waitForXPath

waitForSelector

page.waitForSelector(selector) 等待匹配选择器的元素出现在 DOM 中。这是最常用的等待方法,大多数场景下用它就够了。

javascript
// 等待元素出现 await page.waitForSelector('.result-item'); // 等待元素可见(不仅存在于 DOM,还要有尺寸) await page.waitForSelector('.modal', { visible: true }); // 等待元素隐藏或消失 await page.waitForSelector('.loading-spinner', { hidden: true });

visiblehidden 的区别需要留意:不用这两个选项时,只要元素在 DOM 中就算满足条件,哪怕 display: none。加了 visible: true 才要求元素实际可见(有非零尺寸)。

waitForXPath

page.waitForXPath(xpath) 是 XPath 版本的元素等待,在需要按文本内容或复杂层级关系定位时有用:

javascript
// 按文本内容定位 await page.waitForXPath('//button[contains(text(), "提交")]'); // 复杂层级关系 await page.waitForXPath('//div[@class="form"]/following-sibling::button');

实际项目中 CSS 选择器能覆盖 90% 的场景,waitForXPath 主要用于文本匹配这类选择器不好写的情况。

网络等待:waitForResponse 与 waitForRequest

waitForResponse

page.waitForResponse() 等待特定的网络响应返回,在调试接口或等待异步数据加载时非常实用。

javascript
// 等待特定 URL 的响应 const response = await page.waitForResponse( 'https://api.example.com/data' ); // 用谓词函数做更精确的匹配 const response = await page.waitForResponse( res => res.url().includes('/api/users') && res.status() === 200 ); const data = await response.json();

一个常见的使用模式是:触发操作的同时等待对应的接口响应,确保数据已经返回:

javascript
const [response] = await Promise.all([ page.waitForResponse(res => res.url().includes('/api/search')), page.type('#search-input', 'puppeteer') ]);

waitForRequest

page.waitForRequest() 等待特定的网络请求发出。和 waitForResponse 的区别是:一个等请求发出,一个等响应回来。

javascript
// 验证点击按钮后是否发出了正确的请求 const request = await page.waitForRequest( req => req.url().includes('/api/track') && req.method() === 'POST' );

waitForRequest 在验证请求参数、检查埋点是否正确上报时比较常用。

自定义等待:waitForFunction

page.waitForFunction(pageFunction, ...args) 是最灵活的等待方式,可以等待任意 JavaScript 表达式为真。

javascript
// 等待列表加载超过 5 项 await page.waitForFunction( () => document.querySelectorAll('.item').length > 5 ); // 带参数 await page.waitForFunction( (count) => document.querySelectorAll('.item').length >= count, {}, 10 ); // 等待某个全局变量赋值 await page.waitForFunction( () => window.__APP_READY__ === true ); // 等待 SPA 路由切换 await page.waitForFunction( () => window.location.pathname === '/dashboard' );

waitForSelectorwaitForResponse 都无法满足需求时(比如等待元素数量变化、等待某个 JS 变量、等待 URL 变化),就用 waitForFunction

waitForFunction 的第二个参数可以传入轮询策略:

javascript
await page.waitForFunction( () => document.querySelector('.price')?.textContent !== '', { polling: 'mutation' } // DOM 变化时检查,比定时轮询高效 );

polling 支持 'raf'(每帧检查)、'mutation'(DOM 变化时检查)、数字(毫秒间隔)。DOM 相关等待用 mutation 最合理。

waitForFrame:等待 iframe 加载

page.waitForFrame() 等待指定的 iframe 加载完成,处理嵌入页面时使用:

javascript
const frame = await page.waitForFrame('iframe-name'); const button = await frame.waitForSelector('.btn'); await button.click();

多 iframe 场景下,操作前一定要先拿到正确的 frame 对象,再通过 frame 调用 waitForSelector,而不是直接用 page 调用,否则会找不到元素。

waitForTimeout:已废弃的硬等待

page.waitForTimeout(milliseconds) 等待固定时间,已被官方废弃。如果确实需要延时,用原生方式替代:

javascript
// 已废弃 await page.waitForTimeout(1000); // 替代方案 await new Promise(resolve => setTimeout(resolve, 1000));

硬等待的问题在于:时间设短了不够等,设长了浪费时间,而且无法适应网络波动。应该尽量用条件等待替代,只有在完全没有条件可判断的极端场景下才考虑延时。

常见坑与解决方案

坑 1:waitForNavigation 和 click 的竞态

这是 Puppeteer 新手最常见的 bug。先 click 再 waitForNavigation,导航可能在 click 返回前就完成了:

javascript
// 错误写法:可能永远等不到导航 await page.click('#link'); await page.waitForNavigation(); // 正确写法:并行等待 await Promise.all([ page.waitForNavigation(), page.click('#link') ]);

坑 2:元素在 DOM 中但不可见

waitForSelector 默认只检查元素是否在 DOM 中,不关心是否可见。如果页面有 display: nonevisibility: hidden 的元素,不加 visible: true 可能拿到不可操作的元素:

javascript
// 可能拿到隐藏元素 await page.waitForSelector('.dropdown-menu'); // 确保元素可见 await page.waitForSelector('.dropdown-menu', { visible: true });

坑 3:SPA 页面导航不会触发 waitForNavigation

SPA 内部的路由切换(比如 React Router 或 Vue Router)不会触发浏览器级别的导航事件,waitForNavigation 不会触发。这种场景要用 waitForFunction 等待 URL 变化或特定元素出现:

javascript
// SPA 路由切换不能用 waitForNavigation await Promise.all([ page.waitForFunction(() => window.location.pathname === '/profile'), page.click('.nav-profile') ]);

坑 4:networkidle0 在有长连接的页面上永远等不到

如果页面有 WebSocket 或 SSE 连接,网络请求永远不会归零,networkidle0 会超时。改用 networkidle2

javascript
// 有长连接的页面 await page.goto(url, { waitUntil: 'networkidle2' });

超时处理

所有等待方法都支持 timeout 参数,默认 30 秒。可以在页面级别设置默认超时:

javascript
page.setDefaultTimeout(10000); // 全局默认 10 秒 // 也可以在单次调用中覆盖 await page.waitForSelector('.element', { timeout: 5000 });

超时会抛出 TimeoutError,用 try/catch 捕获后可以做降级处理:

javascript
try { await page.waitForSelector('.optional-banner', { timeout: 3000 }); // banner 出现了,关闭它 await page.click('.banner-close'); } catch (error) { // banner 没出现,继续执行 }

这种"可选元素"的等待模式在实际项目中很常用:元素可能出现也可能不出现,出现了就处理,没出现也不影响主流程。

方法选择速查

场景方法示例
点击后页面跳转waitForNavigationawait Promise.all([page.waitForNavigation(), page.click('#link')])
等待动态元素出现waitForSelectorawait page.waitForSelector('.item', { visible: true })
等待接口返回数据waitForResponseawait page.waitForResponse(res => res.url().includes('/api/data'))
等待复杂条件满足waitForFunctionawait page.waitForFunction(() => document.querySelectorAll('.item').length > 5)
等待 iframe 加载waitForFrameawait page.waitForFrame('iframe-name')
SPA 路由切换waitForFunctionawait page.waitForFunction(() => location.pathname === '/new')
按文本定位元素waitForXPathawait page.waitForXPath('//button[contains(text(),"确认")]')
验证请求发出waitForRequestawait page.waitForRequest(req => req.url().includes('/track'))

Puppeteer 的等待机制核心思路就是用条件等待替代硬编码延时。选对方法、处理好事物的并行和竞态关系,脚本才能既稳定又高效。遇到问题先判断是导航、元素、网络还是自定义条件,然后对号入座选方法,大部分不稳定用例都能解决。

标签:Puppeteer