Puppeteer 的 page.evaluate 和 evaluateHandle 有什么区别,该怎么取回页面数据
用 Puppeteer 驱动页面后,最常见的一个任务是"把页面上的某个值取回自己的脚本里"。此时会遇到两个方法:page.evaluate 和 page.evaluateHandle,签名几乎一样,但返回的东西完全不同。更常见的情况是:evaluate 里 return document.body 之后拿到的是 {},于是不知道该换哪个方法。
本文基于 Puppeteer 的 JavaScript execution 指南 和 API 文档(Page.evaluate、Page.evaluateHandle、JSHandle、ElementHandle),讲清楚两者的区别,以及取回数据时该走哪条路径。
先建立上下文:在页面里执行 JS 的最小路径
以下代码取自 JavaScript execution 指南,其中的 'YOUR_SITE' 是文档原有占位符,替换为你要访问的实际地址即可:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('YOUR_SITE');
const three = await page.evaluate(() => {
return 1 + 2;
});
console.log(three);
await browser.close();
有一个必须记住的执行机制:函数体是在你的脚本里定义的,但会被 Puppeteer 转成字符串发送到目标页面里执行。因此它无法访问你脚本作用域里的变量,也不能调用脚本里定义的其他函数——整个逻辑必须写在函数体内。文档也支持直接传字符串(如 page.evaluate('1 + 2')),但文档明确建议优先用函数形式:字符串形式下,被求值函数能看到的类型和全局变量无法确定,TypeScript 里尤其要注意被引用对象的类型是否正确。
两者的核心区别:取回的是"值"还是"页面内对象的引用"
API 文档对两者差异的原文定义只有一句话(见 Page.evaluateHandle):
The only difference between page.evaluate and page.evaluateHandle is that evaluateHandle will return the value wrapped in an in-page object.
也就是说:
| 返回什么 | 适用情况 | |
|---|---|---|
page.evaluate |
Promise<Awaited<ReturnType<Func>>>,页面函数执行后的原始值 |
只要一个可以跨上下文传回的最终值 |
page.evaluateHandle |
Promise<HandleFor<...>>,即 JSHandle 或 ElementHandle,页面内对象的引用 |
需要在页面里继续持有这个对象并做后续操作 |
两者对 Promise 的行为一致:如果页面里的函数返回 Promise,Puppeteer 会等它 resolve 后再继续,所以 page.evaluate(() => new Promise(...)) 会阻塞到页面内 Promise 完成。
evaluate:取回纯数据的正确姿势
指南对 evaluate 返回值的说明是:
- 返回基本类型时,自动转换成脚本上下文中的基本类型;
- 返回对象时,Puppeteer 会把它序列化成 JSON 再在脚本端重建——"This process might not always yield correct results"。
所以只要目标数据是字符串、数字、布尔值,或者你手动在页面里拼好的普通对象/数组,用 evaluate 一步到位。
从元素上提取单个值,优先用 page.$eval(Page.$eval:找到第一个匹配元素,把元素作为 pageFunction 的第一个参数)。文档给出的示例:
const searchValue = await page.$eval('#search', el => el.value);
const preloadHref = await page.$eval('link[rel=preload]', el => el.href);
const html = await page.$eval('.main-container', el => el.outerHTML);
TypeScript 下默认参数类型是 Element,访问 value、href 这类子类型字段时需要显式标注:
// if you don't provide HTMLInputElement here, TS will error
// as `value` is not on `Element`
const searchValue = await page.$eval(
'#search',
(el: HTMLInputElement) => el.value,
);
两个行为差异要注意:$eval 找不到匹配元素会抛错,而 [Page.eval`,需要容错判断"元素是否存在"时用 `page.$` 然后自己判 `null`。
page.evaluate 本身也可以接收参数(Page.evaluate):
const three = await page.evaluate(
(a, b) => {
return a + b; // 1 + 2
},
1,
2,
);
参数可以是基本值,也可以是 JSHandle / ElementHandle 实例,比如先 page.$('body') 拿 handle,再传给 evaluate 读 innerHTML。
返回 DOM 节点得到 {} 时:改用 evaluateHandle
指南给出的典型翻车示例:
const body = await page.evaluate(() => {
return document.body;
});
console.log(body); // {}, unexpected!
原因上面已经说了:对象要走 JSON 序列化重建,DOM 节点重建出来是空对象。指南给出的解法就是"按引用返回":
const body = await page.evaluateHandle(() => {
return document.body;
});
console.log(body instanceof ElementHandle); // true
Page.evaluateHandle 文档的说明是:大多数时候返回 JSHandle;如果页面函数返回的是元素引用,你拿到的会是 ElementHandle(它继承自 JSHandle)。
拿元素 handle 之后可以直接执行元素级操作,这是 evaluate 给不了的:
const button = await page.evaluateHandle(() =>
document.querySelector('button'),
);
// can call `click` because `button` is an `ElementHandle`
await button.click();
文档还提示:TypeScript 默认假设 evaluateHandle 返回 JSHandle,如果你确定它返回 ElementHandle,用泛型显式标注以获得完整类型:
const button = await page.evaluateHandle<ElementHandle>(...);
从 handle 里取回数据:jsonValue、getProperty、evaluate
evaluateHandle 把对象留在了页面里,数据要取回脚本侧,JSHandle 提供了这几条路:
1. jsonValue() —— "A vanilla object representing the serializable portions of the referenced object",即拿到引用对象中可序列化部分的普通 JS 对象:
const aHandle = await page.evaluateHandle(() => document.body);
const resultHandle = await page.evaluateHandle(body => body.innerHTML, aHandle);
console.log(await resultHandle.jsonValue());
await resultHandle.dispose();
两个限制必须知道(见 JSHandle.jsonValue):
- 对象存在循环引用时会抛错;
- 即使对象上有
toJSON方法,jsonValue()不会调用它。
2. getProperty(name) —— 从引用对象上取单个属性,拿到的是新 handle,可以再 jsonValue():
const documentHandle = await page.evaluateHandle('document'); // 文档示例
const titleHandle = await documentHandle.getProperty('title');
console.log(await titleHandle.jsonValue());
3. handle.evaluate() —— 以当前 handle 作为第一个参数在页面里继续求值,取回的是原始值:
const url = await windowHandle.evaluate((w) => w.location.href);
文档示例 page.evaluateHandle('document') 展示的就是字符串形式拿 document 的 handle。
不要忘记释放 handle
JSHandle 的机制说明:handle 会阻止引用的对象被垃圾回收,除非你主动 dispose();当关联 frame 发生导航或父 context 销毁时,handle 会自动释放。跨导航场景不用手动管,但在同一页面里创建了大量 handle(例如循环里反复 evaluateHandle 提取数据)时,用完后调用 dispose() 释放:
const bodyHandle = await page.$('body');
const html = await page.evaluate(body => body.innerHTML, bodyHandle);
await bodyHandle.dispose();
这是 Page.evaluate 文档 Example 3 的完整用法。
取数路径怎么选,以及边界
结合上面的机制,取回页面数据可以按这个决策走:
- 目标是单个可序列化的值(字符串、数字、布尔、你在页面里拼好的纯数据对象/数组):
page.$eval(selector, el => el.xxx)(针对单个元素)或page.evaluate(() => ...)(针对整个页面)一步取回。 - 需要持有页面内对象继续操作(点击、滚动、继续取属性):
page.evaluateHandle拿JSHandle/ElementHandle,再用jsonValue()/getProperty()/handle.evaluate()取回具体数据,用完后dispose()。 - 直接
returnDOM 节点这类不可 JSON 化的对象给evaluate,只会得到{}——这是文档明确展示的失败模式,不要靠它取元素。
边界与限制(均来自上述文档):
evaluate返回对象走 JSON 序列化重建,"might not always yield correct results";- 页面函数被字符串化到页面执行,不能引用脚本作用域的变量;
jsonValue()遇循环引用抛错,且不调用toJSON;$eval找不到元素抛错,page.$找不到元素返回null。
如果要继续深入,JSHandle 还有 getProperties()(拿到引用对象所有属性的 handle map)、asElement()(判断 handle 是否为 ElementHandle)等方法;ElementHandle 则提供 $、$$、screenshot、boundingBox 等元素级操作,都是拿 handle 之后的下一组动作。
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