首页
/ Puppeteer Locator.race() 并发定位指南:让多个定位器并行竞争并保证单一元素命中

Puppeteer Locator.race() 并发定位指南:让多个定位器并行竞争并保证单一元素命中

2026-09-07 10:28:48作者:廉彬冶Miranda

在 Puppeteer 中,Locator 是描述"如何找到元素并对它执行动作"的声明式策略对象。而 Locator.race() 是一个静态方法,它把多个定位器组合成一个"竞赛":这些定位器并行地尝试定位各自的元素,但只要其中任何一个先成功,整个组合定位器就立即胜出,并且后续的点击、填写等动作只会作用在最终命中的那一个元素上

本文以 docs/api/puppeteer.locator.race.md 为骨架,结合 Puppeteer 仓库中 Locator 核心实现locator 集成测试,讲清该 API 的类型签名、并行竞速语义、单命中元素保证、超时与中止控制,并给出可直接复制的实战代码。读完你将能写出更健壮、对页面结构变化(如 A/B 弹窗、多版本 UI)容忍度更高的自动化脚本。

一、官方 API 定义:静态方法签名与返回类型

先看官方 API 文档中的原始签名(见 puppeteer.locator.race.md):

class Locator {
  static race<Locators extends readonly unknown[] | []>(
    locators: Locators,
  ): Locator<AwaitedLocator<Locators[number]>>;
}

逐个拆解:

  • 调用方式Locator.race(...)Locator 类上的 static 方法,不是实例方法。因此调用时写 Locator.race([locatorA, locatorB]),而不是 locatorA.race(...)。这一点与 Locator 实例上的 .click().fill() 等动作方法不同。
  • 参数 locators:一个可迭代的只读数组,类型约束为 readonly unknown[] | []。其中每个元素应当是一个 Locator 实例,通常来自 page.locator(selector)frame.locator(selector)
  • 泛型约束 Locators extends readonly unknown[] | []:既允许传入普通数组,也保留了元组(tuple)字面量,从而让 TypeScript 能对数组每个位置的元素类型分别做精确推导
  • 返回类型 Locator<AwaitedLocator<Locators[number]>>Locators[number] 取出数组成员联合类型,再经 AwaitedLocator 解包得到每个候选定位器目标类型,作为组合结果的泛型参数。所以返回的依然是一个功能完整的 Locator,你可以继续对它调用 click()fill()setTimeout() 等方法,而非一次性的 Promise。

AwaitedLocator 的定义在 docs/api/puppeteer.awaitedlocator.md

export type AwaitedLocator<T> = T extends Locator<infer S> ? S : never;

意思是:若 TLocator<S> 形态,则解出它内部目标值类型 S;若传入的是 Locator<Promise<...>> 等更复杂的形状,也一并解包为真正的元素类型。这正是 .race() 返回值能保持强类型、await 后直接得到目标类型的原因。

官方对该方法的一句话语义描述为:Creates a race between multiple locators trying to locate elements in parallel but ensures that only a single element receives the action.(在多个定位器并行查找元素之间创建竞赛,同时确保只有一个元素收到动作。)

二、仓库源码佐证:静态入口 → 数组校验 → RaceLocator

对照源码实现,静态方法入口位于 packages/puppeteer-core/src/api/locators/locators.ts

static race<Locators extends readonly unknown[] | []>(
  locators: Locators,
): Locator<AwaitedLocator<Locators[number]>> {
  return RaceLocator.create(locators);
}

它把参数转发给内部类 RaceLocator 的工厂方法 create。在 RaceLocator.create 中,第一件事是调用 checkLocatorArray运行时类型校验locators.ts):

function checkLocatorArray<T extends readonly unknown[] | []>(
  locators: T,
): ReadonlyArray<Locator<AwaitedLocator<T[number]>>> {
  for (const locator of locators) {
    if (!(locator instanceof Locator)) {
      throw new Error('Unknown locator for race candidate');
    }
  }
  return locators as ReadonlyArray<Locator<AwaitedLocator<T[number]>>>;
}

这带来两个重要结论:

  1. 数组的每一个成员都必须是真正的 Locator 实例。如果你误传了裸 CSS 选择器字符串(如 ['#a', '.b'])或 ElementHandle,会直接抛出 Unknown locator for race candidate 错误。
  2. 校验通过后,checkLocatorArray 会得到统一的 ReadonlyArray<Locator<...>>RaceLocator 构造器将其保存到私有字段 #locators,并沿用第一个候选定位器的 logger(若候选为空则 logger 为空),见 RaceLocator 构造器

RaceLocator 本质是一个组合定位器(composite locator),它自身也是 Locator<T> 的子类,因此可以嵌套使用——例如把一个 Locator.race([...]) 的结果再作为另一个 Locator.race 的候选。

三、实现原理:基于 RxJS race 的并行竞速

RaceLocator 的关键在于对"等待目标元素"这一过程的重写。它在 _wait 方法里把每个候选定位器的 _wait(options) 流交给 RxJS 的 race 操作符合并(locators.ts):

override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
  return race(
    ...this.#locators.map(locator => {
      return locator._wait(options);
    }),
  );
}

这正是整篇文章语义的引擎。RxJS 的 race 语义为:同时订阅多个 Observable,哪一个先发出值,就采用哪一个,并自动取消其余订阅。映射到 DOM 自动化场景:

  • 多个定位器同步并行地轮询各自的选择器,谁先找到"就绪可用"的元素谁胜出;
  • 一旦某个候选先 emit,其余候选的等待与重试立即被放弃;
  • 后续的动作链(点击、填写等)只消费赢家的元素句柄,因此绝不会出现两个元素同时被点击/填写的情况。

从文件顶部的导入可以看到,race 确实来自仓库内置的 RxJS(locators.ts 中导入了 raceraceWithmergeMapretrytimeoutfromAbortSignal 等操作符),说明整个 Locator 体系是建立在"可重试的响应式事件流"之上的:每个候选定位器内部都有自动重试机制(RETRY_DELAY 间隔)、可见性/可交互性前置条件检查以及超时控制。当一个候选失败时它不会立刻终止,而是按节奏重试,直到赢得竞速或整体超时。

单一命中保证的进一步体现可以看 click 动作的内部实现:#clickthis._wait(options) 拿到句柄,随后经 conditions(滚动进视口、等待稳定包围盒、等待可用)后,仅对那一个 handle 调用 handle.click(options)locators.ts)。因为 _wait 已经在 race 阶段收敛出了唯一赢家,下游自然只动作一个元素。

组合后的 Locator 依然"可克隆、可配置"

RaceLocator 覆写了 _clone 方法(locators.ts),它会把每个候选逐一 clone(),再构造新的 RaceLocator 并调用 copyOptions(this) 把当前组合定位器的超时、可见性、等待可用等选项复制过去。这意味着:

const racing = Locator.race([a, b]).setTimeout(8000); // 克隆出新组合,不改动 a/b

setTimeoutsetVisibilitysetWaitForEnabledsetEnsureElementIsInTheViewportsetWaitForStableBoundingBox 这些链式配置方法都遵循"克隆-改参"模式(见 setTimeoutsetVisibility 等),因此race 结果的二次配置不会污染原始候选定位器,天然适合共享/复用。

四、实战:什么时候需要 Locator.race,怎么写

.race() 的价值在于把"页面可能以多种形态呈现同一交互目标"这一不确定性收敛成一个动作。典型场景:

  • 站点使用不同版本的 UI 框架,同一按钮可能是 <button> 也可能是 aria 角色组件;
  • Cookie 同意横幅有多种文案/按钮变体;
  • 元素可能位于不同框架或不同 Shadow DOM 分支(可用各自域内的 locator 组合);
  • 功能开关(feature flag)导致桌面端与移动端 DOM 结构不同。
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com/');

// 候选 1:经典按钮
const byButton = page.locator('button#submit');
// 候选 2:新版 role=button 组件
const byRole = page.locator('[role="button"][data-testid="submit"]');

// 谁先可交互谁生效,且只会点击其中一个
await Locator.race([byButton, byRole]).click();

再例如,处理不确定文案的 Cookie 弹窗:

const acceptVariants = ['#accept-all', '#agree', 'button:has-text("Got it")']
  .map(sel => page.locator(sel));

await Locator.race(acceptVariants).click();

候选定位器彼此是独立对象,你甚至可以给它们各自配置不同的可见性策略后再合并。注意 Locatorpage.locator 的关系可参考 puppeteer.locator.mdPage.locator()

五、超时、中止与"永不命中"的处理

Locator.race 返回的仍是 Locator,因此超时、取消信号等控制能力完整保留:

  • 默认超时:与普通 Locator 一致,默认值为 Page.getDefaultTimeout(),即默认 30 秒(源码中 Locator#timeout 初始值即 30000,见 locators.ts)。
  • 整体超时(必须所有候选都失败才超时):竞速的整体超时只在所有候选都在各自期限内找不到元素时触发。仓库测试 should time out when all locators do not match 用两个 not-found 定位器配合 .setTimeout(5000) 验证:clock.tick(5100) 后 Promise 以 TimeoutError: Timed out after waiting 5000ms 拒绝(见 test/src/locator.test.ts)。
  • 只要有一个命中就不超时:测试 should not time out when one of the locators matchesnot-found 与真实 button 一起竞速,结果正常 resolve(test/src/locator.test.ts)。这印证了 race 是"或"语义:任一候选成功即整体成功。
  • AbortSignal 中止:测试 can be aborted 通过 new AbortController() 传入 .click({signal}),在等待中途 abort() 后 Promise 以 /aborted/ 错误拒绝(test/src/locator.test.ts)。
const controller = new AbortController();
const task = Locator.race([mobileBtn, desktopBtn])
  .setTimeout(10_000)
  .click({signal: controller.signal});

// 需要时中途取消
controller.abort();

六、单一命中的行为保证与测试验证

把文章开头那句官方语义翻译成可验证的断言就是:即便多个候选定位器解析到同一个元素,动作也只会执行一次。仓库测试 races multiple locators 直接覆盖了这一点(test/src/locator.test.ts):

await page.setContent(html`
  <button onclick="window.count++;">test</button>
`);
await page.evaluate(() => {
  // @ts-expect-error different context.
  window.count = 0;
});
await Locator.race([
  page.locator('button'),
  page.locator('button'),   // 两个候选指向同一个按钮
]).click();
const count = await page.evaluate(() => {
  // @ts-expect-error different context.
  return globalThis.count;
});
expect(count).toBe(1);      // 只点击了一次!

这个用例同时验证了两种行为:两个候选并行等待;竞速胜出后动作不会重复派发。这也是 .race()Promise.race([...].click()) 这类朴素写法最本质的区别——朴素 Promise.race 只能保证"最先 settle",却不能阻止其他候选后续各自把动作执行一遍;而 Locator.race 在事件流层面就让输家彻底退出,从机制上杜绝了重复动作。

七、注意事项与类型陷阱小结

  1. 入参必须是 Locator 实例数组:传选择器字符串等非 Locator 会抛出 Unknown locator for race candidate
  2. 空数组边界:类型上允许 []readonly unknown[] | []),但空竞速意味着没有任何候选能命中,最终会走向超时;使用时应保证至少一个候选在页面中可能成立。
  3. 返回值继续链式操作:组合结果是完整的 Locator.click() / .fill() / .setTimeout() / .setVisibility() 等都可继续使用,也可以再次参与外层 race
  4. 结果类型是取并集:返回 Locator<AwaitedLocator<Locators[number]>>,若各候选目标类型不同,最终操作会按并集类型推断,通常建议候选指向同构的交互目标。
  5. 适用对象:本方法面向 "locate an element 并施加单一动作" 的场景;若你是在为多个网络请求或事件做超时竞速,应使用 Promise.raceDeferred.race 等通用并发原语,而非本 API。

结语与延伸阅读

Locator.race 是 Puppeteer Locator 体系把"响应式竞速"抽象到页面自动化层面的代表性 API。理解它的关键在于把握两层模型:并发层(多个候选 Locator 各自携带独立的重试、前置条件检查与超时,并行推进)与收敛层(RxJS race 只放行唯一赢家进入动作阶段)。

想进一步钻研,可按下面的仓库路径继续阅读:

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

项目优选

收起
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++
916
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