Puppeteer Frame.$$eval 方法完全指南:在页面框架中批量查询并对多元素执行函数求值
关联文档:
docs/api/puppeteer.frame.__eval.md(原始版本化文档对应version-25.8.0的 API 页面)
方法签名与类型定义
Frame.$$eval() 是 Puppeteer 中一个核心的 DOM 批量求值方法,它在给定的页面框架(Frame)中运行一个函数,并将与该框架中给定选择器匹配的所有元素数组作为该函数的第一个参数传入。
根据官方 API 文档(docs/api/puppeteer.frame.__eval.md),其方法签名为:
class Frame {
$$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<Array<NodeFor<Selector>>, Params> =
EvaluateFuncWith<Array<NodeFor<Selector>>, Params>,
>(
selector: Selector,
pageFunction: string | Func,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
}
关键要点:
- 方法名中的
$$表示“查询所有匹配元素”(区别于$仅查询第一个匹配元素),在 Puppeteer 中这是一个约定俗成的命名规范。 $$eval返回 Promise:如果给定的pageFunction返回一个 Promise,则此方法会等待该 Promise 解析完毕后再返回。最终返回Promise<Awaited<ReturnType<Func>>>。- 泛型参数由 TypeScript 推断:
Selector限定为字符串类型,Params为额外的参数数组,Func为在数组元素上执行的函数。
参数详解
方法接收三个参数,其中前两个是必填项,第三个为可选的任意数量附加参数。
1. selector(必填)
| 参数 | 类型 | 说明 |
|---|---|---|
| selector | Selector(string) |
用于在页面中查询的选择器 |
该选择器可以是:
- CSS 选择器:可以直接原样传入,例如
'div'、'.item'、'#list > li'等。 - Puppeteer 特有的选择器语法(非 CSS 选择器):
text/...:按文本内容查询(text selectors)aria/...:按无障碍角色的 name 属性查询(ARIA selectors)xpath/...:按 XPath 表达式查询(XPath selectors)- 也可以跨 shadow DOM 根节点组合查询。
- 带前缀的显式类型语法:若想强制指定选择器的解析类型,可以通过前缀来声明,例如
::-p-text(...)或::-p-aria(...)等。
2. pageFunction(必填)
| 参数 | 类型 | 说明 |
|---|---|---|
| pageFunction | string | Func |
要在框架上下文中执行的函数 |
- 该函数在框架(frame)的执行上下文中求值。
- 一个由匹配给定选择器的所有元素组成的数组,会作为函数的第一个参数传入。
- 函数内可以使用浏览器端的 DOM API,例如对每个元素读取
textContent、className、value、attributes等属性,或调用el.click()、el.getBoundingClientRect()等方法。 - 当传入一个函数时,该函数会被序列化后发送到浏览器端执行,因此闭包外部变量不会自动可见,需要通过第三个参数显式传递(如果需要)。
3. ...args(可选)
| 参数 | 类型 | 说明 |
|---|---|---|
| args | Params(unknown[]) |
传递给 pageFunction 的附加参数 |
当 pageFunction 需要用到 Node.js 环境中的变量、需要传递字符串参数或进行条件分支时,可将其作为 args 传入,并在函数内通过形参接收。
返回值
| 返回类型 | 说明 |
|---|---|
Promise<Awaited<ReturnType<Func>>> |
一个指向函数结果的 Promise |
该结果是 pageFunction 在浏览器端执行完成后的返回值。若函数返回了原始值(如数字、字符串、布尔值)会被序列化返回;若返回 undefined、函数、Symbol 等无法序列化的值,则会得到 undefined。
基本用法示例
官方文档给出了最简洁的入门示例:
const divsCounts = await frame.$$eval('div', divs => divs.length);
这段代码的含义是:在当前 frame 中查询出所有 div 元素,把它们的数组传给回调,返回数组长度(即该框架内 div 元素的总数)。
再看几个更接近实际业务场景的用法。
提取某个框架中所有列表项的文本:
const items = await frame.$$eval('.product-title', items =>
items.map(item => item.textContent)
);
// items 为字符串数组
计算元素属性的统计信息(带附加参数):
const count = await frame.$$eval(
'li',
(listItems, className) =>
listItems.filter(item => item.classList.contains(className)).length,
'active' // 这是第三个参数 args,会传给 pageFunction
);
从 Node.js 端注入参数控制逻辑:
const text = 'Hello Puppeteer';
const count = await frame.$$eval(
'p',
(paragraphs, target) =>
paragraphs.filter(p => p.textContent?.includes(target)).length,
text // Node 端字符串变量
);
与其他 API 的区别与联系
在 Puppeteer 的文档体系(docs/api/ 目录)中,与 $$eval 功能密切相关的还有几个方法,使用时很容易混淆,需要注意区分:
| 方法 | 作用域 | 匹配元素 | 典型场景 |
|---|---|---|---|
frame.$() |
单个元素 | 返回第一个匹配的 ElementHandle |
需要后续进行交互(点击、输入等)时 |
frame.$$() |
全部元素 | 返回所有匹配的 ElementHandle 数组 |
需要对每个元素做多次交互时 |
frame.$eval()(对应文档 puppeteer.frame._eval.md) |
单个元素 | 对第一个匹配元素执行函数求值 | 只需读取一个元素的值或属性 |
frame.$$eval()(本文档) |
全部元素 | 对所有匹配元素组成的数组执行函数求值 | 批量读取/统计/汇总多个元素 |
二者的典型对应示例为:
// $eval:对第一个匹配元素求值,读取搜索框的值
const searchValue = await frame.$eval('#search', el => el.value);
// $$eval:对所有匹配元素求值,统计 div 数量
const divsCounts = await frame.$$eval('div', divs => divs.length);
与 Page 层 API 的关系:Page.$$eval()(对应文档 docs/api/puppeteer.page.__eval.md)作用于整个页面的主框架(main frame)。从源码 packages/puppeteer-core/src/api/Page.ts 第 1584-1596 行可以看出,Page.$$eval() 的底层实现实际上是把调用转交给了主框架:
async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<Array<NodeFor<Selector>>, Params> = ...,
>(selector: Selector, pageFunction: Func | string, ...args: Params) {
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
return await this.mainFrame().$$eval(selector, pageFunction, ...args);
}
因此,frame.$$eval() 是更底层、更通用的 API。当页面中存在**多个 frame(例如 iframe 嵌套页面)**时,若要针对某个具体子框架内的元素进行批量求值,就必须使用 Frame.$$eval(),而 Page.$$eval() 只能作用于主框架。
源码级实现解析
Frame 类的实现(packages/puppeteer-core/src/api/Frame.ts)
Frame.$$eval() 方法在源码中的实现(第 708-723 行)如下:
@throwIfDetached
async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<Array<NodeFor<Selector>>, Params> =
EvaluateFuncWith<Array<NodeFor<Selector>>, Params>,
>(
selector: Selector,
pageFunction: string | Func,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
// eslint-disable-next-line @puppeteer/use-using -- This is cached.
const document = await this.#document();
return await document.$$eval(selector, pageFunction, ...args);
}
该实现有几个值得注意的内部机制:
@throwIfDetached装饰器:如果该 frame 已从页面中分离(detached,例如对应 iframe 被移除),则调用此方法会直接抛出错误,避免在无效的上下文中执行。withSourcePuppeteerURLIfNone:为调试器提供便利——当pageFunction是内联字符串时,会为该函数标记一个带方法名的虚拟 URL,从而在 DevTools 或错误堆栈中能更好地定位到调用源头(例如pptr:evaluate)。this.#document():方法先获取当前 frame 对应的文档(Document)句柄,然后委托给ElementHandle<Document>的$$eval。文档句柄是有缓存的(因此 eslint 忽略 use-using 规则),避免每次调用都重新取文档。从Frame.ts第 432 行可以找到#document()的定义,它会返回一个指向该 frame 内document节点的ElementHandle。
元素层级的实现(packages/puppeteer-core/src/api/ElementHandle.ts)
真正执行“批量查询 + 求值”的是 ElementHandle.$$eval()(第 563-579 行附近),其实现逻辑是:
async $$eval<...>(selector: Selector, pageFunction: Func | string, ...args: Params) {
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
const results = await this.$$(selector); // ① 在 DOM 中查出所有匹配元素
using elements = await this.evaluateHandle( // ② 将句柄数组传给执行上下文
(_, ...elements) => {
return elements;
},
...results,
);
// ③ 在浏览器端把这些元素作为参数传入 pageFunction 执行
...
}
整个调用链可概括为:
frame.$$eval()
└─> this.#document() // 取得当前 frame 的 document 句柄
└─> document.$$(selector) // 查询所有匹配的元素句柄(ElementHandle[])
└─> evaluateHandle(...) // 把元素数组传入浏览器执行上下文
└─> pageFunction(elements, ...args) // 在 frame 上下文中执行用户函数
这也就解释了为什么 pageFunction 能拿到“元素数组”——实际上 Puppeteer 先把每个匹配元素包装成 ElementHandle,再统一放到页面执行上下文中解包为真实的 DOM 元素,最后作为函数的第一个参数传入。
测试用例佐证
在仓库测试目录 test/src/ 中,可以找到大量对 $$eval 行为进行验证的用例,覆盖了 Puppeteer 特有的选择器。例如:
test/src/queryhandler.test.ts:使用page.$$eval('text/text', ...)验证文本选择器在所有匹配元素上的求值行为。test/src/ariaqueryhandler.test.ts:使用page.$$eval('aria/[role="button"]', ...)验证 ARIA 角色选择器。test/src/elementhandle.test.ts:使用page.$$eval('getByClass/foo', ...)验证自定义查询处理器(自定义 query handler)的批量求值。
这些用例说明 $$eval 不仅支持纯 CSS 选择器,还能与 Puppeteer 丰富的扩展选择器体系(text、aria、xpath、自定义查询处理器)无缝协作。
实际使用注意事项
- 框架上下文隔离:
Frame.$$eval会在目标 frame 自己的 JavaScript 执行上下文中运行函数。如果页面存在跨域 iframe,主页面脚本无法访问其内部 DOM,但 Puppeteer 通过 CDP 可以直接操作该 frame,这正是需要针对每个frame单独调用$$eval的原因。 - 获取 Frame 实例:可通过
page.mainFrame()获取主框架,通过frame.childFrames()或page.frames()获取页面中的所有框架(包括 iframe)。Frame.$$eval()应该在这些具体 frame 实例上调用。 - 异步求值会被等待:如果
pageFunction返回 Promise,$$eval会等待其 resolve。这意味着函数内部可以使用async/await或在浏览器端发起Promise.all进行异步处理。 - 不要混用 Node 端变量:
pageFunction在浏览器上下文运行,Node.js 中的变量无法直接捕获,必须通过...args显式传入。同时,参数必须是可序列化的值(数字、字符串、布尔、数组、普通对象等),不能直接传入复杂对象或函数。 - 返回值的可序列化限制:返回值同样遵循 CDP 序列化规则。若返回
undefined、NaN、Infinity或包含Symbol/函数引用等无法序列化的值,会被替换为undefined。需要获取复杂句柄时应使用evaluateHandle。 - 性能考量:
$$eval是一次性批量操作,适合“查完即用”的只读/一次性场景。若需要对同一批元素反复操作(多次点击、多次输入等),更合适的做法是先frame.$$()拿到ElementHandle[]缓存复用,避免反复查询;若需要等待元素出现,则应配合waitForSelector使用。
常见业务场景示例
1. 抓取表格数据(含表头)
// 假设 frame 指向一个包含数据表格的 iframe
const tableData = await frame.$$eval('table tr', rows =>
rows.map(row => {
const cells = [...row.querySelectorAll('td, th')];
return cells.map(cell => cell.textContent?.trim());
})
);
2. 统计并汇总指标
const total = await frame.$$eval(
'.price',
(els, taxRate) =>
els.reduce((sum, el) => sum + Number(el.dataset.price || 0), 0) * taxRate,
1.13 // args 传税率
);
3. 批量校验页面结构
const missingAlt = await frame.$$eval('img', imgs =>
imgs.filter(img => !img.alt).map(img => img.src)
);
if (missingAlt.length > 0) {
console.log('缺少 alt 属性的图片:', missingAlt);
}
4. 配合自定义查询选择器在子框架内定位(参考 docs/api/ 中其他 frame API 的组合使用)
for (const frame of page.frames()) {
const headings = await frame.$$eval('h1', hs => hs.map(h => h.textContent));
console.log(`frame(${frame.url()}) 的标题:`, headings);
}
相关 API 文档索引
在仓库 docs/api/ 目录中,可以找到与本方法相关的更多详细文档:
- Frame 类总览:
Frame.$$eval所属的框架 API 集合,包含$、$$、$eval、waitForSelector、content、goto等常用方法。 - $eval 方法(单元素求值):对第一个匹配元素执行函数求值,是本方法的“单数版本”。
- Page.$$eval 方法:页面级别的批量求值入口,内部委托给主框架的
Frame.$$eval。 - ElementHandle.$$eval 方法:元素句柄级别的批量求值,
Frame.$$eval底层即基于此实现。 - EvaluateFuncWith 类型:定义
pageFunction的参数与返回值类型约束。 - NodeFor 类型:用于根据选择器类型推断对应的 DOM 节点 TypeScript 类型,这也是
$$eval泛型能够获得完整类型推导的基础。
借助这些泛型与类型推导,frame.$$eval() 可以让开发者用近乎原生 DOM 的书写方式,在任意框架(包括 iframe 嵌套与跨域子页面)内完成批量查询、批量读取与批量统计,是 Puppeteer 脚本化浏览器自动化中最常用也最高效的 API 之一。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00