3.元素定位与基础交互
元素定位是自动化测试和网页爬虫的核心能力。打开页面只是第一步,真正的工作在于如何精准找到目标元素并与之交互。Puppeteer 提供了多种定位方式,从传统的 CSS 选择器到智能的 Locator API,每种方式都有其适用场景。本章将深入探讨这些技术,帮助建立稳健的元素定位策略。
Locator API:现代定位的首选方案
Locator API 是 Puppeteer 最新推荐的元素定位方式。它不仅仅是一个选择器,更是一个智能的交互对象,能够自动处理等待、可见性检查、状态验证等繁琐工作。使用 Locator API 编写的脚本更稳定,不易因页面加载时机不同而失败。
基本使用模式
创建 Locator 非常简单,调用 page.locator() 并传入选择器即可。选择器可以是 CSS 选择器、XPath 表达式,或是 Puppeteer 特有的文本、ARIA 选择器。
// 通过 CSS 选择器创建 Locator
const buttonLocator = page.locator('button.submit');
// 通过 XPath 创建 Locator
const xpathLocator = page.locator('::-p-xpath(//div[@class="content"])');
// 通过文本内容创建 Locator
const textLocator = page.locator('::-p-text(立即购买)');
Locator 创建后不会立即执行任何操作,它只是一个描述如何找到元素的"配方"。真正的动作发生在调用交互方法时,比如 click()、fill() 等。
智能等待机制
Locator API 最大的价值在于其内置的智能等待。执行点击或输入操作前,它会自动验证一系列前提条件,确保元素处于可交互状态。这些检查包括:
- 元素是否存在于 DOM 中
- 元素是否在可视区域内
- 元素是否可见(非隐藏、非透明)
- 元素是否启用(非禁用状态)
- 元素的位置是否稳定(连续两帧动画间位置不变)
这种自动等待极大减少了因页面加载异步性导致的测试失败。传统方式需要手动编写 waitForSelector 和状态检查,而 Locator 将这些逻辑封装在内部。
// 点击按钮前,Locator 会自动等待按钮就绪
await page.locator('button.submit').click();
// 填写表单时,Locator 会等待输入框可编辑
await page.locator('input[name="username"]').fill('testuser');
常见交互操作
Locator 支持多种用户交互模拟,覆盖了大部分测试场景。
点击操作
点击是最基础的交互。Locator 的 click() 方法会模拟真实用户的鼠标点击,包括移动光标、按下鼠标、释放鼠标等完整过程。
// 简单点击
await page.locator('.menu-item').click();
// 带选项的点击,例如右键点击
await page.locator('.context-target').click({ button: 'right' });
// 双击
await page.locator('.file-item').dblclick();
输入与清空
fill() 方法专门用于表单输入。它会自动聚焦元素、清空原有内容(对于可清空元素)、输入新值,并触发相应的输入事件。对于 <select> 元素,它也能正确处理选项选择。
// 输入文本
await page.locator('#email').fill('user@example.com');
// 清空输入框
await page.locator('#search').fill('');
// 选择下拉框选项
await page.locator('select[name="country"]').fill('CN');
悬停与滚动
悬停操作常用于触发下拉菜单或工具提示。hover() 方法会将鼠标移动到元素中心位置。
await page.locator('.dropdown-trigger').hover();
滚动操作则用于处理长列表或无限滚动页面。scroll() 方法通过模拟鼠标滚轮事件来滚动元素。
// 向下滚动 500 像素
await page.locator('.content-area').scroll({ scrollTop: 500 });
// 横向滚动
await page.locator('.wide-table').scroll({ scrollLeft: 200 });
高级等待模式
除了交互时的自动等待,Locator 还提供了显式等待方法,用于在特定条件满足后再继续执行。
等待元素可见
wait() 方法会等待元素出现在 DOM 中并可见,然后立即返回。这在需要等待加载指示器消失或新内容出现的场景中很有用。
// 等待加载完成
await page.locator('.loading-spinner').wait();
// 等待新内容出现
await page.locator('.search-results').wait();
等待并返回元素句柄
有时需要获取底层的 ElementHandle 对象以执行 Locator API 不支持的复杂操作。waitHandle() 方法会等待元素就绪并返回其句柄。
const elementHandle = await page.locator('.custom-widget').waitHandle();
// 使用 ElementHandle 的特定方法
await elementHandle.evaluate(node => node.scrollIntoView());
await elementHandle.dispose(); // 记得释放句柄
值映射与过滤
Locator 提供了函数式编程风格的 map() 和 filter() 方法,用于在元素上执行自定义逻辑。
map() 方法将元素映射为任意 JavaScript 值,常用于提取属性或状态。
// 获取按钮的启用状态
const isEnabled = await page
.locator('button.submit')
.map(button => !button.disabled)
.wait();
// 获取多个元素的文本内容
const texts = await page
.locator('.item-title')
.map(elements => elements.map(el => el.textContent))
.wait();
filter() 方法根据条件筛选元素,只有满足条件的元素才会被用于后续操作。
// 只点击文本为"确认"的按钮
await page
.locator('button')
.filter(button => button.innerText === '确认')
.click();
配置与超时控制
Locator 的行为可以通过链式调用的配置方法进行调整。例如,可以禁用某些自动等待条件,或设置自定义超时。
// 禁用所有等待条件,立即尝试点击
await page
.locator('button')
.setEnsureElementIsInTheViewport(false)
.setVisibility(null)
.setWaitForEnabled(false)
.setWaitForStableBoundingBox(false)
.click();
// 设置 5 秒超时
await page.locator('.slow-element').setTimeout(5000).wait();
默认情况下,Locator 使用页面的超时设置(30 秒),但可以为特定操作设置更短或更长的超时。
事件监听
Locator 支持监听内部事件,目前主要支持 Action 事件,在即将执行动作前触发。这可用于调试或记录操作日志。
await page
.locator('button')
.on(LocatorEvent.Action, () => {
console.log('即将点击按钮');
})
.click();
模拟用户点击与键盘输入
除了通过 Locator 进行交互,Puppeteer 还提供了底层的鼠标和键盘 API,允许更精细地控制输入行为。这些 API 直接作用于页面,不依赖于特定元素。
键盘操作
page.keyboard 提供了完整的键盘模拟能力,可以按下、释放单个按键,或输入完整字符串。
// 按下回车键
await page.keyboard.press('Enter');
// 输入文本,每个字符间有延迟
await page.keyboard.type('Hello World', { delay: 100 });
// 组合键操作
await page.keyboard.down('Control');
await page.keyboard.press('KeyA');
await page.keyboard.up('Control');
键盘 API 特别适用于快捷键操作或需要精确控制输入时机的场景。例如,某些富文本编辑器需要模拟真实的键盘事件才能正确触发内容更新。
鼠标操作
page.mouse 允许精确控制鼠标移动、点击、拖拽等行为。可以指定屏幕坐标,实现像素级的操作控制。
// 移动鼠标到指定位置
await page.mouse.move(100, 200);
// 点击鼠标左键
await page.mouse.click(150, 250);
// 拖拽操作
await page.mouse.move(300, 300);
await page.mouse.down();
await page.mouse.move(400, 400, { steps: 10 });
await page.mouse.up();
鼠标 API 的优势在于可以操作页面上没有明确元素的区域,或实现复杂的拖拽交互。
触摸操作
对于移动端测试,page.touchscreen 提供了触摸事件模拟。
// 单点触摸
await page.touchscreen.tap(100, 200);
// 多点触控(需要配合手势协议)
显式等待与隐式等待
等待是自动化脚本稳定性的关键。Puppeteer 提供了多层次的等待机制,从 Locator 的自动等待到手动控制的显式等待,理解这些机制对编写可靠脚本至关重要。
Locator 的隐式等待
Locator API 的最大特点是其"隐式等待"能力。调用交互方法时,不需要额外编写等待代码,Locator 会在内部自动重试,直到元素就绪或超时。这种设计大幅简化了代码,提高了可读性。
// 这行代码会自动等待按钮出现、可见、可点击
await page.locator('button.submit').click();
隐式等待的默认超时时间继承自页面设置,通常为 30 秒。对于快速响应的元素,实际等待时间可能只有几毫秒;对于慢加载的元素,则会持续等待直到就绪。
显式等待:waitForSelector
在某些场景下,需要更精细地控制等待逻辑,或等待非交互元素。waitForSelector 是传统的显式等待方法,它等待元素出现在 DOM 中,并返回 ElementHandle。
// 等待元素出现
const element = await page.waitForSelector('.data-table');
// 等待元素消失
await page.waitForSelector('.loading', { hidden: true });
// 设置超时时间
const element = await page.waitForSelector('.modal', { timeout: 5000 });
与 Locator 不同,waitForSelector 返回的 ElementHandle 需要手动管理内存,使用完毕后必须调用 dispose() 释放。
const element = await page.waitForSelector('.temp-widget');
await element.click();
await element.dispose(); // 重要:防止内存泄漏
等待函数执行
对于复杂的等待条件,可以使用 page.waitForFunction() 等待任意 JavaScript 表达式返回真值。这在需要等待数据加载完成或状态变更时非常有用。
// 等待某个全局变量被设置
await page.waitForFunction('window.appReady === true');
// 等待列表中有至少 10 个项目
await page.waitForFunction(() => {
return document.querySelectorAll('.list-item').length >= 10;
});
等待网络空闲
在导航或提交表单后,通常需要等待网络请求完成。page.waitForNetworkIdle() 会等待所有网络连接至少空闲 500 毫秒。
await page.click('button.submit');
await page.waitForNetworkIdle();
等待特定时间
虽然不推荐,但有时简单的延迟是必要的。page.waitForTimeout() 会暂停指定毫秒数。
// 等待 1 秒
await page.waitForTimeout(1000);
过度使用固定延迟会导致测试变慢且不稳定,应优先使用条件等待。
CSS 选择器与 XPath 表达式
选择器是元素定位的基础。Puppeteer 支持多种选择器语法,每种都有其优势和适用场景。
CSS 选择器
CSS 选择器是最常用、最直观的选择方式。Puppeteer 支持完整的 CSS 选择器语法,包括属性选择器、伪类、组合器等。
// 通过 ID 选择
await page.locator('#username').fill('test');
// 通过类名选择
await page.locator('.btn-primary').click();
// 通过属性选择
await page.locator('input[name="email"]').fill('user@example.com');
// 通过层级关系选择
await page.locator('form > .form-group input').fill('value');
// 通过伪类选择
await page.locator('button:disabled').wait();
CSS 选择器的优势在于简洁、快速,且与前端开发技能完全兼容。对于标准 DOM 结构,CSS 选择器通常是首选。
XPath 表达式
XPath 提供了更强大的查询能力,特别是在需要根据文本内容或复杂层级关系定位元素时。Puppeteer 通过 ::-p-xpath() 伪元素支持 XPath。
// 选择所有 h2 元素
await page.locator('::-p-xpath(//h2)').wait();
// 选择包含特定文本的元素
await page.locator('::-p-xpath(//button[contains(text(), "提交")])').click();
// 选择父元素
await page.locator('::-p-xpath(//input[@id="email"]/..)').wait();
XPath 的强项在于可以向上查询(选择父元素)、根据文本内容精确匹配、使用函数进行复杂条件判断。但 XPath 表达式通常比 CSS 选择器冗长,性能也略低。
文本选择器
文本选择器根据元素的可见文本内容定位,非常适合用户导向的测试。使用 ::-p-text() 伪元素,Puppeteer 会找到包含指定文本的最小元素。
// 点击文本为"登录"的按钮
await page.locator('::-p-text(登录)').click();
// 处理包含特殊字符的文本
await page.locator('::-p-text(价格 \\(含税\\))').click();
// 使用引号处理复杂文本
await page.locator('::-p-text("Say \\"Hello\\" to user")').click();
文本选择器的优势在于不依赖 DOM 结构,即使页面布局变化,只要文本不变,测试依然稳定。但需要注意文本的唯一性,避免匹配到多个元素。
ARIA 选择器
ARIA 选择器通过可访问性树的计算名称和角色定位元素,非常适合测试可访问性要求高的应用。使用 ::-p-aria() 伪元素。
// 通过可访问名称选择
await page.locator('::-p-aria(提交表单)').click();
// 通过角色和名称组合选择
await page.locator('::-p-aria([name="关闭"][role="button"])').click();
ARIA 选择器模拟了屏幕阅读器的元素识别方式,有助于确保应用对辅助技术友好。但依赖浏览器的可访问性计算,某些复杂自定义组件可能无法正确识别。
Shadow DOM 穿透
现代 Web 组件大量使用 Shadow DOM,传统的 CSS 选择器无法穿透 Shadow 边界。Puppeteer 提供了深度组合器来解决这个问题。
>>> 深度后代组合器可以穿透任意层级的 Shadow DOM:
// 在自定义元素内部查找按钮
await page.locator('my-card >>> button.edit').click();
>>>> 深度子组合器只穿透直接子级的 Shadow DOM:
await page.locator('my-list >>>> .list-item').wait();
这些组合器让测试 Web Components 变得可能,无需复杂的 JavaScript 注入。
自定义选择器
对于基于特定框架(如 React、Vue)的应用,可以注册自定义选择器,利用框架的内部 API 定位组件。
// 注册 React 组件选择器
Puppeteer.registerCustomQueryHandler('react-component', {
queryOne: (elementOrDocument, selector) => {
// 在页面上下文中执行,可以访问 React 内部属性
return elementOrDocument.querySelector(`[data-component="${CSS.escape(selector)}"]`);
},
queryAll: (elementOrDocument, selector) => {
return elementOrDocument.querySelectorAll(`[data-component="${CSS.escape(selector)}"]`);
},
});
// 使用自定义选择器
await page.locator('::-p-react-component(UserProfile)').click();
自定义选择器提供了无限扩展可能,但需要注意框架内部 API 的稳定性,避免升级导致测试失效。
选择器性能考量
不同选择器的性能差异显著。一般来说,CSS 选择器最快,因为它直接利用浏览器的原生查询引擎。XPath 次之,需要额外的解析和执行。文本和 ARIA 选择器最慢,因为它们需要遍历 DOM 树并计算每个元素的属性。
在实际项目中,建议优先使用 CSS 选择器,仅在必要时使用其他选择器。对于复杂查询,可以考虑使用组合策略:先用 CSS 选择器缩小范围,再用文本或 ARIA 精确定位。
// 高效组合:先用 CSS 缩小范围,再用文本精确定位
await page.locator('.user-menu ::-p-text(退出登录)').click();
低级查询 API
虽然 Locator API 是首选,但了解低级 API 有助于处理特殊场景和理解 Puppeteer 的内部机制。
$ 和 $$ 方法
page.$() 返回匹配选择器的第一个元素的 ElementHandle,page.$$() 返回所有匹配元素的数组。
// 获取单个元素
const element = await page.$('.single-item');
if (element) {
await element.click();
await element.dispose();
}
// 获取多个元素
const elements = await page.$$('.list-item');
for (const el of elements) {
const text = await el.evaluate(node => node.textContent);
console.log(text);
await el.dispose();
}
这些方法不会自动等待元素出现,如果元素不存在会立即返回 null 或空数组。因此需要配合 waitForSelector 使用。
$eval 和 $$eval 方法
这两个方法在元素上执行 JavaScript 函数,并返回结果。$eval 在第一个匹配元素上执行,$$eval 在所有匹配元素上执行。
// 获取元素的文本内容
const text = await page.$eval('.title', el => el.textContent);
// 获取所有元素的属性
const hrefs = await page.$$eval('a.link', elements =>
elements.map(el => el.href)
);
这些方法简洁高效,适合一次性数据提取,但不适合需要多次交互的场景。
最佳实践总结
元素定位策略直接影响自动化脚本的稳定性和维护成本。基于实际项目经验,总结以下最佳实践:
优先使用 Locator API:利用其自动等待和重试机制,减少手动等待代码。
选择器应稳定且语义化:优先选择 ID、data-testid 等稳定属性,避免依赖动态生成的类名或结构。
组合使用选择器:先用 CSS 选择器定位容器,再用文本或 ARIA 精确定位,平衡性能与稳定性。
避免过度使用 XPath:虽然强大,但可读性差且性能较低,仅在必要时使用。
合理使用显式等待:对于非交互元素或复杂条件,使用
waitForSelector或waitForFunction。管理 ElementHandle 生命周期:使用低级 API 时,务必在完成后调用
dispose()防止内存泄漏。为动态内容设置合理超时:根据应用特性调整超时时间,避免过短导致不稳定,或过长导致测试缓慢。
利用选择器穿透 Shadow DOM:测试 Web Components 时,使用
>>>组合器简化定位逻辑。
掌握这些技术后,就能编写出既稳定又高效的自动化脚本。Locator API 的引入标志着 Puppeteer 向更智能、更可靠的方向发展,建议在新项目中全面采用。对于遗留代码,可以逐步迁移到 Locator,享受自动等待带来的稳定性提升。
下一章将探讨如何在页面上下文中执行 JavaScript,实现更复杂的数据提取和状态操作,进一步扩展自动化能力。