首页
/ Puppeteer Frame.$$eval 方法完全指南:在页面框架中批量查询并对多元素执行函数求值

Puppeteer Frame.$$eval 方法完全指南:在页面框架中批量查询并对多元素执行函数求值

2026-09-08 10:31:58作者:蔡丛锟

关联文档: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,例如对每个元素读取 textContentclassNamevalueattributes 等属性,或调用 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);
}

该实现有几个值得注意的内部机制:

  1. @throwIfDetached 装饰器:如果该 frame 已从页面中分离(detached,例如对应 iframe 被移除),则调用此方法会直接抛出错误,避免在无效的上下文中执行。
  2. withSourcePuppeteerURLIfNone:为调试器提供便利——当 pageFunction 是内联字符串时,会为该函数标记一个带方法名的虚拟 URL,从而在 DevTools 或错误堆栈中能更好地定位到调用源头(例如 pptr:evaluate)。
  3. 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、自定义查询处理器)无缝协作。

实际使用注意事项

  1. 框架上下文隔离Frame.$$eval 会在目标 frame 自己的 JavaScript 执行上下文中运行函数。如果页面存在跨域 iframe,主页面脚本无法访问其内部 DOM,但 Puppeteer 通过 CDP 可以直接操作该 frame,这正是需要针对每个 frame 单独调用 $$eval 的原因。
  2. 获取 Frame 实例:可通过 page.mainFrame() 获取主框架,通过 frame.childFrames()page.frames() 获取页面中的所有框架(包括 iframe)。Frame.$$eval() 应该在这些具体 frame 实例上调用。
  3. 异步求值会被等待:如果 pageFunction 返回 Promise,$$eval 会等待其 resolve。这意味着函数内部可以使用 async/await 或在浏览器端发起 Promise.all 进行异步处理。
  4. 不要混用 Node 端变量pageFunction 在浏览器上下文运行,Node.js 中的变量无法直接捕获,必须通过 ...args 显式传入。同时,参数必须是可序列化的值(数字、字符串、布尔、数组、普通对象等),不能直接传入复杂对象或函数。
  5. 返回值的可序列化限制:返回值同样遵循 CDP 序列化规则。若返回 undefinedNaNInfinity 或包含 Symbol/函数引用等无法序列化的值,会被替换为 undefined。需要获取复杂句柄时应使用 evaluateHandle
  6. 性能考量$$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 集合,包含 $$$$evalwaitForSelectorcontentgoto 等常用方法。
  • $eval 方法(单元素求值):对第一个匹配元素执行函数求值,是本方法的“单数版本”。
  • Page.$$eval 方法:页面级别的批量求值入口,内部委托给主框架的 Frame.$$eval
  • ElementHandle.$$eval 方法:元素句柄级别的批量求值,Frame.$$eval 底层即基于此实现。
  • EvaluateFuncWith 类型:定义 pageFunction 的参数与返回值类型约束。
  • NodeFor 类型:用于根据选择器类型推断对应的 DOM 节点 TypeScript 类型,这也是 $$eval 泛型能够获得完整类型推导的基础。

借助这些泛型与类型推导,frame.$$eval() 可以让开发者用近乎原生 DOM 的书写方式,在任意框架(包括 iframe 嵌套与跨域子页面)内完成批量查询、批量读取与批量统计,是 Puppeteer 脚本化浏览器自动化中最常用也最高效的 API 之一。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391