14.跨浏览器自动化与WebDriver BiDi
Puppeteer 最初是 Chrome 的专属工具,靠着 Chrome DevTools Protocol(CDP)这把钥匙,在 Chromium 世界里畅通无阻。但浏览器生态从来不只是 Chrome 的独角戏。从 v23.0.0 开始,Puppeteer 正式拥抱 Firefox,背后的功臣是一个叫 WebDriver BiDi 的新协议。这不仅是多支持一个浏览器那么简单,而是整个自动化领域迈向标准化的重要一步。
Firefox 浏览器支持
在 v23.0.0 之前,Puppeteer 对 Firefox 的支持一直处于实验阶段,依赖的是 Firefox Nightly 版本。Nightly 就像浏览器的"每日构建版",功能最新但稳定性欠佳,适合尝鲜不适合生产。从 v23.0.0 起,Puppeteer 开始捆绑 Firefox 稳定版,这意味着可以在生产环境中放心使用 Firefox 进行自动化测试了。
版本对应关系非常严格。每个 Puppeteer 版本都精确匹配特定的 Firefox 版本,这种绑定是为了确保底层协议的兼容性。查看 revisions.ts 文件就能找到当前版本对应的 Firefox 版本号。比如 Puppeteer v24.34.0 对应 Firefox 146.0.1,v23.0.0 对应 Firefox 129.0。这种精确匹配避免了浏览器更新导致自动化脚本意外失效的问题。
启动 Firefox 的方式简单直接,只需要在 launch 方法中指定 browser 参数:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
browser: 'firefox',
});
const page = await browser.newPage();
await page.goto('https://example.com');
await browser.close();
这段代码会启动 Firefox 并使用 WebDriver BiDi 协议进行通信。不需要额外配置,Puppeteer 会自动处理协议细节。Firefox 的启动参数、用户数据目录、无头模式等配置与 Chrome 保持一致,降低了学习成本。
需要注意的是,Firefox 支持并非意味着所有 Chrome 上的功能都能无缝迁移。某些依赖 Chrome 特定实现的功能在 Firefox 上可能表现不同或不被支持。这引出了下一个话题:WebDriver BiDi 协议本身。
WebDriver BiDi 协议详解
WebDriver BiDi 是一个正在开发中的跨浏览器自动化协议,目标是融合 WebDriver Classic 和 CDP 的优点。名字里的 BiDi 代表"双向"(Bidirectional),指的是浏览器和自动化工具之间可以双向通信。传统的 WebDriver 是单向的:工具发送命令,浏览器执行并返回结果。CDP 虽然也是双向的,但它是 Chrome 的私有协议。WebDriver BiDi 试图建立一个开放标准,让所有浏览器都能实现。
这个协议的核心价值在于标准化。过去,自动化工具要面对两个选择:使用 CDP 获得强大功能但绑定 Chrome,或者使用 WebDriver 实现跨浏览器但功能有限。WebDriver BiDi 希望打破这个困境,提供既强大又通用的解决方案。它基于 WebDriver 标准扩展,加入了事件监听、实时日志、网络拦截等 CDP 风格的特性。
在 Puppeteer 中使用 WebDriver BiDi 非常直观。对于 Firefox,这是默认协议。对于 Chrome,需要显式指定:
// Firefox 默认使用 WebDriver BiDi
const firefoxBrowser = await puppeteer.launch({
browser: 'firefox',
});
// Chrome 需要显式指定协议
const chromeBrowser = await puppeteer.launch({
browser: 'chrome',
protocol: 'webDriverBiDi',
});
protocol 参数接受两个值:'cdp' 和 'webDriverBiDi'。不指定时,Chrome 默认使用 'cdp',Firefox 默认使用 'webDriverBiDi'。这种设计保证了向后兼容性,现有代码无需修改就能继续运行。
WebDriver BiDi 的另一个优势是性能。由于支持双向通信,浏览器可以主动推送事件,不需要自动化工具不断轮询。这在处理页面加载、网络请求、控制台日志等场景时效率更高。协议设计也考虑了安全性,所有通信都在浏览器进程外进行,隔离了潜在风险。
不过,作为新兴标准,WebDriver BiDi 的功能覆盖还在完善中。某些 CDP 的独门绝技尚未标准化,这导致了功能上的差异。
CDP 与 BiDi 功能对比
理解 CDP 和 WebDriver BiDi 的功能差异是跨浏览器自动化的关键。Puppeteer 官方维护了一份详细的支持清单,可以分为三类:完全支持、部分支持和不支持。
完全支持的功能包括基础自动化操作、脚本评估、选择器定位、输入模拟、对话框处理等。这些功能构成了自动化测试的核心,在两种协议下表现一致。
// 这些代码在 CDP 和 BiDi 下都能正常工作
await page.goto('https://example.com');
const title = await page.evaluate(() => document.title);
await page.click('button.submit');
await page.screenshot({ path: 'example.png' });
部分支持的功能主要是截图和 PDF 生成。截图支持 clip、encoding、fullPage 参数,但其他参数如 omitBackground 可能不被支持。PDF 生成只支持 format、height、landscape、margin、pageRanges、printBackground、scale、width 这些基础参数,高级排版控制暂时不可用。
不支持的功能列表较长,可以分为几类:
第一类是各种模拟功能。page.emulate() 用于模拟设备,page.emulateCPUThrottling() 用于 CPU 降速,page.emulateVisionDeficiency() 用于模拟视觉缺陷,这些在 BiDi 下都无法使用。媒体模拟如 emulateMediaType() 和 emulateMediaFeatures() 也不支持。setBypassCSP() 用于绕过内容安全策略,这是 CDP 的专属功能。
第二类是 CDP 特定功能。page.createCDPSession() 用于创建原始 CDP 会话,显然在 BiDi 下没有意义。HTTPRequest.client() 返回 CDP 客户端实例,HTTPRequest.resourceType() 返回请求资源类型,这些依赖 CDP 内部实现的方法都会抛出 UnsupportedOperation 错误。
第三类是高级调试功能。Accessibility 树获取、代码覆盖率统计、性能追踪(Tracing)目前都不支持。这些功能对测试很重要,但标准化进程较慢。
第四类是网络相关的特殊功能。page.setOfflineMode() 用于模拟离线状态,page.emulateNetworkConditions() 用于模拟网络环境,page.setBypassServiceWorker() 用于绕过 Service Worker,这些在 BiDi 下都不支持。不过基础的网络请求拦截是支持的,只是功能有所简化。
// 基础请求拦截在 BiDi 下支持
await page.setRequestInterception(true);
page.on('request', request => {
if (request.url().endsWith('.png')) {
request.abort();
} else {
request.continue();
}
});
// 但自定义错误原因不支持
// request.abort('connectionrefused'); // 会报错
第五类是拖拽操作。专门的拖拽 API 如 input.drag()、input.dragAndDrop() 不支持,但基础的鼠标事件可以模拟拖拽。
// 使用鼠标事件模拟拖拽
await page.mouse.move(100, 100);
await page.mouse.down();
await page.mouse.move(200, 200);
await page.mouse.up();
第六类是其他杂项功能。page.metrics() 获取页面性能指标、page.queryObjects() 查询 JavaScript 对象、page.screencast() 视频录制、page.waitForDevicePrompt() 等待设备提示等都不支持。
这种差异意味着从 CDP 迁移到 BiDi 需要评估现有代码。如果大量使用了不支持的特性,可能需要调整测试策略或等待协议完善。
跨浏览器迁移指南
迁移到跨浏览器自动化不是简单的切换浏览器字符串,而是需要系统性的规划和代码调整。
第一步是评估现有代码库。检查是否使用了 CDP 专属功能,特别是那些在不支持列表中的方法。可以编写一个扫描脚本,搜索关键方法调用:
// 扫描代码中使用的 CDP 专属功能
const unsupportedPatterns = [
'page.emulate()',
'page.emulateCPUThrottling',
'page.emulateIdleState',
'page.emulateMediaFeatures',
'page.emulateMediaType',
'page.emulateVisionDeficiency',
'page.setBypassCSP',
'createCDPSession',
'resourceType()',
'page.metrics()',
'page.screencast',
'setDragInterception',
'setOfflineMode',
'waitForDevicePrompt',
];
// 扫描项目文件
// 输出使用了哪些不支持的 API
第二步是抽象浏览器差异。不要直接在测试代码中硬编码浏览器特定逻辑,而是封装一个浏览器适配层:
class BrowserAdapter {
private browser: string;
private page: Page;
constructor(browser: string, page: Page) {
this.browser = browser;
this.page = page;
}
async emulateIfSupported(options: any) {
if (this.browser === 'chrome') {
// CDP 下支持完整模拟
await this.page.emulate(options);
} else {
// Firefox 下只支持部分模拟
if (options.viewport) {
await this.page.setViewport(options.viewport);
}
if (options.userAgent) {
await this.page.setUserAgent(options.userAgent);
}
// 其他模拟参数忽略或记录警告
console.warn('Full emulation not supported on Firefox');
}
}
async getMetricsIfSupported() {
if (this.browser === 'chrome') {
return await this.page.metrics();
}
// Firefox 下返回空对象或模拟数据
return {};
}
}
这种适配器模式让测试代码保持简洁,浏览器差异在内部处理。
第三步是配置测试矩阵。在 CI/CD 环境中同时运行 Chrome 和 Firefox 测试,确保功能一致性。可以使用环境变量控制浏览器选择:
// 在测试配置中
const browserType = process.env.TEST_BROWSER || 'chrome';
const protocol = process.env.TEST_PROTOCOL || 'cdp';
const browser = await puppeteer.launch({
browser: browserType,
protocol: browserType === 'firefox' ? 'webDriverBiDi' : protocol,
});
第四步是处理功能降级。对于不支持的特性,考虑是否有替代方案。比如,CDP 的 page.metrics() 可以替换为 page.evaluate() 执行自定义的性能指标收集脚本:
// 使用 evaluate 替代 metrics
const metrics = await page.evaluate(() => {
return {
timestamp: Date.now(),
// 自定义性能指标
domContentLoaded: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart,
loadComplete: performance.timing.loadEventEnd - performance.timing.navigationStart,
// 内存使用(如果可用)
memory: (performance as any).memory ? {
usedJSHeapSize: (performance as any).memory.usedJSHeapSize,
totalJSHeapSize: (performance as any).memory.totalJSHeapSize,
} : null,
};
});
第五步是关注协议演进。WebDriver BiDi 还在快速发展,定期查看 Puppeteer 更新日志和协议规范。今天不支持的特性可能下个月就实现了。可以在代码中添加版本检查,当检测到新版本支持某个特性时自动启用:
// 检查 Puppeteer 版本是否支持某个特性
function isFeatureSupported(feature: string, version: string): boolean {
const versionMap: Record<string, string[]> = {
'page.emulateMediaType': ['24.0.0', 'chrome'],
'page.metrics': ['23.0.0', 'cdp-only'],
// 随着协议发展更新这个映射
};
const requirement = versionMap[feature];
if (!requirement) return false;
const [requiredVersion, protocol] = requirement;
// 版本比较逻辑
return compareVersions(version, requiredVersion) >= 0;
}
第六步是社区协作。跨浏览器自动化是整个行业的事,遇到问题可以参与协议讨论,向 Puppeteer 或浏览器厂商反馈需求。Puppeteer 团队本身也在积极 dogfood 新协议,帮助发现 bug 和填补功能空白。
最后,不要急于全面迁移。可以采用混合策略:新测试用例使用 BiDi 编写,确保跨浏览器兼容;旧测试用例逐步迁移,优先迁移那些对 Chrome 依赖不深的部分。对于必须使用 CDP 特性的场景,可以保留 Chrome 专用测试,同时添加 Firefox 的简化版本。
跨浏览器自动化不是一蹴而就的工程,而是持续演进的过程。WebDriver BiDi 的出现让这个过程有了明确的方向。作为开发者,既要拥抱新标准带来的便利,也要理解技术演进的节奏,在稳定性和先进性之间找到平衡。
下一章将进入项目实战阶段,把这些理论知识应用到完整的爬虫项目中,看看在真实场景下如何处理错误、组织代码、监控性能。