首页
/ Puppeteer Locator.filter() 完全解析:谓词过滤、类型收窄与失败自动重试机制

Puppeteer Locator.filter() 完全解析:谓词过滤、类型收窄与失败自动重试机制

2026-09-07 12:39:59作者:盛欣凯Ernestine

导读

Locator.filter() 是 Puppeteer 定位器(Locator)体系中用于表达"元素必须满足某个条件"的 API:它不直接执行动作,而是把一个谓词(predicate)挂载到定位结果上,只有当前定位到的值通过谓词校验时操作才会继续;一旦条件不满足,定位器会自动重试,直到条件成立或超时。本篇文章以 docs/api/puppeteer.locator.filter.md 的 API 契约为主线,结合 packages/puppeteer-core/src/api/locators/locators.ts 的源码实现与 test/src/locator.test.ts 中的真实测试用例,帮助你彻底掌握 filter 的签名语义、Predicate 参数的两类写法、类型层面的值收窄能力,以及它与 click、hover、wait、map 等方法组合时背后的自动重试与超时原理。

filter 是什么:把"断言"注入定位流程

在 Puppeteer 的定位器体系里,Locator 描述的是"定位对象并对它执行动作的策略"。官方文档对 filter 的定义只有两句,但信息密度很高:

Creates an expectation that is evaluated against located values. If the expectations do not match, then the locator will retry.

翻译过来就是:创建一个"期望",该期望会对被定位到的值求值;如果期望不匹配,定位器会重试。也就是说,filter 解决的是一类典型的动态页面问题——元素存在,但它的状态(属性、文案、可见性等)还没达到我们要求的条件。这类"等它变好再操作"的逻辑过去需要手写 waitForFunction + 轮询,而现在可以声明式地写进定位器里。

它的完整签名在文档中被定义为:

class Locator {
  filter<S extends T>(predicate: Predicate<T, S>): Locator<S>;
}
  • T:当前定位器定位到的值的类型;
  • S extends T:过滤之后新定位器的值类型。正因为 S 可以是 T 的收窄(通过类型守卫),返回值是 Locator<S>
  • predicate:类型为 Predicate<T, S> 的谓词;
  • 返回值:一个新的 Locator<S>,原定位器不受影响。

Predicate 谓词参数:两种形态与类型收窄

filter 的唯一参数是 Predicate<T, S>。这个类型定义在 docs/api/puppeteer.predicate.md 中,其源码形态位于 locators.ts

export type Predicate<From, To extends From = From> =
  ((value: From) => value is To) | ((value: From) => Awaitable<boolean>);

它由两种函数形态组成:

  1. 类型守卫形态 (value: From) => value is To 利用 TypeScript 的 value is To 语法做窄化,谓词返回 true 时编译器会把值收窄为 To,从而让 filter 返回的 Locator<S> 携带更精确的类型信息。

  2. 布尔谓词形态 (value: From) => Awaitable<boolean> 返回普通 boolean,或返回一个 Promise(Awaitable 意味着支持同步与异步两种写法)。测试代码 locator.test.ts 里就同时展示了 async 与同步两种谓词串联使用:

const result = page
  .locator('::-p-text(test)')
  .setTimeout(5000)
  .filter(async element => {
    return element.getAttribute('clickable') === 'true';
  })
  .filter(element => {
    return element.getAttribute('clickable') === 'true';
  })
  .hover();

这段测试还揭示了另一个关键点:filter 可以链式多次叠加,每次追加一个更严格的期望。

一个容易被误解的细节

从 API 文档的纯类型视角看,filter 是对"被定位到的值"求值。但从底层实现看(下面会展开),当前公开 filter 的实现会把这些值当作 DOM 元素、把谓词派发到页面上下文中去求值。因此在实际使用中,filter 最常见的落点仍然是元素型定位器(由 page.locator(selector) 等产生),谓词函数签名里收到的实参是页面里的真实 DOM 节点,可以直接读取它的属性与内容。

filter 返回值:一个延迟求值的新定位器

filter 返回 Locator<S>,但调用 filter 本身并不会立刻在页面上执行任何检查。这与整个 Locator 体系"定义策略、按需执行"的设计一致:locator 上的 clickhoverfillscrollwaitwaitHandle 才是真正触发动作的入口。

也就是说,filtermap 一样,属于"派生定位器"的构造方法。典型用法是先过滤再动作:

// 等待页面中出现一个未禁用的提交按钮,然后点击它
await page
  .locator('button.submit')
  .filter(button => !button.disabled)
  .click();
// 用 filter + map + wait 组合:等某个 div 具备 clickable 属性后再取出该属性值
const result = await page
  .locator('div')
  .filter(element => element.getAttribute('clickable') !== null)
  .map(element => element.getAttribute('clickable'))
  .wait();

上面第二段正是 locator.test.ts 中 "should work with expect" 用例的骨架——页面初始时 <div> 并没有 clickable 属性,随后测试通过 page.evaluate 动态补上该属性,filter 感知到条件满足后链路继续向下执行。这个用例精准地说明了 filter 的"等待语义"。

自动重试机制的源码级原理

filter 最核心的行为是"期望不满足就重试",它的实现并不神秘。公开 filter 的实现位于 locators.ts

filter<S extends T>(predicate: Predicate<T, S>): Locator<S> {
  return new FilteredLocator(this._clone(), async (handle, signal) => {
    await (handle as ElementHandle<Node>).frame.waitForFunction(
      predicate,
      {signal, timeout: this._timeout},
      handle,
    );
    return true;
  });
}

可以看到三个关键信息:

  1. 谓词通过 waitForFunction 在页面内求值filter 内部把谓词包装成一个 handle 级谓词,调用被定位元素的 frame.waitForFunction(predicate, ...),并把当前定位到的 handle 作为参数传入。谓词因此是在浏览器渲染进程内执行的,而非 Node 侧。
  2. 超时与取消信号被透传waitForFunction 收到了 {signal, timeout: this._timeout}timeout 来自定位器自身的超时配置,默认是 30000ms(见 locators.tsprotected _timeout = 30000,默认值跟随 Page.getDefaultTimeout());signal 是来自外部的 AbortSignal,可用于提前取消等待。
  3. 返回一个新的 FilteredLocator。注意第一步是 this._clone()——filter 不会修改原定位器,而是基于克隆产生派生实例。

真正承担"求值 + 重试"的是内部类 FilteredLocator,定义在 locators.ts

override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> {
  return this.delegate._wait(options).pipe(
    mergeMap(handle => {
      return from(Promise.resolve(this.#predicate(handle, options?.signal))).pipe(
        filter(value => value),
        map(() => {
          return handle as HandleFor<To>;
        }),
      );
    }),
    throwIfEmpty(),
  );
}

这段 RxJS 管线的执行逻辑可以这样理解:

  • 先让委托的底层定位器(delegate)去定位元素,产出一个 handle;
  • 对 handle 执行谓词(Promise.resolve 表明这里兼容同步返回值与 Promise);
  • 谓词返回 false 时,filter(value => value) 会把该值丢弃,最终因 throwIfEmpty() 抛错,表示"本次求值失败";
  • 抛错后,外层由统一的重试机制接手:定位器在 retryAndRaceWithSignalAndTimer 中以 retry({delay: RETRY_DELAY}) 重新订阅,而 RETRY_DELAY 定义在文件末尾(locators.ts):
export const RETRY_DELAY = 100;

即每次失败后等待 100ms 再重试,同时用 timeout(this._timeout)fromAbortSignal(signal) 与超时/取消信号进行竞速。因此完整的失败闭环是:定位不到 → 重试;定位到了但谓词不满足 → 丢弃并重试;总时长超过定位器 timeout → 抛出超时错误。这也是为什么测试 "should resolve as soon as the predicate matches"(locator.test.ts)在 fake clock 推进 2000ms、元素刚被加上 clickable="true" 后,hover 能立刻成功。

FilteredLocator 与选项传播:DelegatedLocator 的妙处

从源码结构看,filter 返回的 FilteredLocator 是抽象类 DelegatedLocator<T, U>locators.ts)的子类。所谓"委托定位器",是指它包装着另一个定位器(delegate),自身负责在委托结果之上叠加逻辑。

这一继承结构带来一个对使用者非常友好的行为:对 filter 之后的新定位器调用各种 set* 配置方法,配置会自动下放到被包装的底层定位器。例如在 DelegatedLocator 中:

  • setTimeout 会同时更新自身与 delegate(locators.ts);
  • setVisibilitysetWaitForEnabledsetEnsureElementIsInTheViewportsetWaitForStableBoundingBox 同样会改写 delegate。

测试 locator.test.ts 专门验证了这一点:对 map/filter 派生的委托定位器执行 setTimeout(500) 后,其 timeout 应当与原始定位器不同——即配置成功生效在派生实例上。这意味着你可以这样链式书写而无需担心配置丢失:

await page
  .locator('button.submit')
  .filter(button => !button.disabled)
  .setTimeout(10_000)   // 该超时会同步作用于内部定位
  .click();

此外 DelegatedLocator_clone 机制(locators.ts)保证了在配置方法内部调用 _clone 时,克隆体依然持有相同的谓词与委托,派生链不会被破坏。

类型收窄:filter 在类型层面的收益

Predicate 的第一种形态是类型守卫 (value: From) => value is To。当页面元素类型存在层次时(例如先 page.locator('input') 得到元素定位器,再用类型守卫过滤出某类具体输入控件),filter 能在类型层面把后续处理收窄到更具体的类型,返回值也随之变成 Locator<S>。这与 map 的"类型改写"(把 Locator<T> 变成 Locator<To>)形成互补:map 改变值的投影,filter 收窄值的集合与类型。两者通常配合使用,构造出一条类型安全、语义清晰的定位链路。

使用注意事项与边界

结合源码与测试,使用 filter 时有几点需要留意:

  • 谓词在页面上下文运行:谓词函数被序列化后经由 waitForFunction 在浏览器内执行,因此它无法直接闭包捕获 Node 进程中的外部变量;需要传参时,应考虑用页面内可访问的状态,或在谓词内通过元素的属性/内容判断。
  • filter 只是期望,不是动作:必须由后续的 clickhoverfillwait 等方法触发执行。仅调用 filter 本身不会产生任何页面副作用。
  • 异步谓词完全可用Predicate 支持返回 Awaitable<boolean>,实现会通过 Promise.resolve 统一吸收同步与异步两种结果,测试中也展示了 async element => ... 的写法。
  • 超时控制:谓词长时间不满足最终受定位器 timeout 约束(默认 30000ms,可用 setTimeout 调整,传 0 可关闭超时,见 Locator.setTimeout 文档)。需要更短等待时请显式设置。
  • 重试节奏固定:失败重试间隔为常量 RETRY_DELAY = 100ms,无法通过参数调整,这是内部实现细节,了解即可,不要依赖其精确时序编写脆弱的断言。

小结

Locator.filter() 用一句谓词把"条件等待"声明式地带入 Puppeteer 定位器:谓词不满足时由 FilteredLocator + 100ms 重试 + 定位器超时共同保证"等到条件成立为止"。它既可做布尔断言(可同步可异步),也可做类型守卫以实现类型收窄,还能与 mapclickwait 自由组合、链式叠加。想要在真实动态页面上"等一个按钮从禁用变为可用再点击"这类需求,filter 就是最贴合官方设计的表达方式。若要继续深入,建议按顺序阅读 Locator 类总览map 方法Predicate 类型,并在 locators.ts 中对照 FilteredLocatorDelegatedLocatorRETRY_DELAY 三处实现,即可完整吃透整条"定位—过滤—重试—动作"的链路。

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

项目优选

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