为什么 Verdex 直接使用 CDP

发布: (2025年12月11日 GMT+8 06:52)
7 min read
原文: Dev.to

Source: Dev.to

引言

Verdex 是一款开发时的创作工具,需要对 DOM 检查和 JavaScript 执行上下文进行深度、特定的控制。相对地,Playwright 是一个执行时的测试运行器,旨在实现跨浏览器的可靠性。这两种根本不同的使用场景导致了截然不同的架构选择。

关于灵感的说明

Verdex 在设计上大量借鉴了 Playwright——其可访问性树实现、隔离世界处理以及元素生命周期管理都被深入研究。Verdex 旨在与 Playwright 的精细度保持一致(ARIA 兼容的可访问性树、稳健的框架处理、隔离执行上下文),但在架构上有所区别:它为创作时的选择器构建添加了结构探索原语(get_ancestorsget_siblingsget_descendants),而不是侧重于执行时的测试可靠性。

执行时 vs 创作时:不同的问题,不同的解决方案

执行时 (Playwright)创作时 (Verdex)
选择器处理每次操作都重新解析选择器在多个查询之间保持稳定引用
失效元素错误通过重新解析防止支持多步骤 DOM 探索
跨浏览器一致性必需不是主要关注点
元素句柄短暂的会话期间的持久映射

Playwright 的定位器理念

Playwright 的定位器会在每次操作时自动重新解析,这可以防止失效元素错误:

“每次使用定位器执行操作时,都会在页面中定位到最新的 DOM 元素。” – Playwright 文档

对于创作时的分析,Verdex 需要在 同一个 元素上执行顺序查询:

// 创作时对同一元素的顺序查询
get_ancestors("e3");     // 从该特定元素向上遍历
get_siblings("e3", 2);   // 检查同一元素的兄弟节点
get_descendants("e3");   // 探索同一元素的子节点

Playwright 的定位器每次都会重新查询 DOM,如果页面发生变化,可能会返回不同的元素。虽然 Playwright 提供了 ElementHandle 用于持久引用,但框架不鼓励使用,并在操作后自动释放它们,以强化重新解析的理念。

“等等——失效元素怎么办?”

Selenium 那臭名昭著的 StaleElementReferenceException 发生在 运行时

element = driver.find_element(By.ID, "submit")
# ... 测试执行期间页面重新渲染 ...
element.click()  # ❌ 已失效

Verdex 的持久引用仅在 创作 阶段存在,即分析静态快照时:

// 创作会话(稳定快照)
resolve_container("e3");          // 从元素向上遍历
get_siblings("e3", 2);            // 检查兄弟节点
extract_anchors("e3", 1);         // 查找唯一内容

// 输出:纯 Playwright 代码(自动解析)
getByTestId("product-card")
  .filter({ hasText: "iPhone 15 Pro" })
  .getByRole("button", { name: "Add to Cart" });

关键区别

  • 生命周期阶段 – Verdex 的引用存在于静态分析期间,而非动态测试执行。
  • 无页面交互 – 探索过程不会触发重新渲染。
  • 安全的输出 – 生成的测试代码使用 Playwright 的自动解析定位器。

因此,Verdex 能够在多步骤结构探索中使用持久引用,而最终的测试代码则受益于 Playwright 稳健的运行时行为。

CDP 层:架构选择的重要性

Playwright 与 Verdex 都基于 Chrome DevTools Protocol (CDP)。创建隔离世界的代码看起来类似:

// Playwright 使用 CDP
const client = await page.context().newCDPSession(page);
await client.send('Page.createIsolatedWorld', {
  frameId: mainFrameId,
  worldName: 'verdex-bridge',
  grantUniveralAccess: true
});
// Puppeteer 使用 CDP
const client = await page.createCDPSession();
const { executionContextId } = await client.send('Page.createIsolatedWorld', {
  frameId: mainFrameId,
  worldName: 'verdex-bridge',
  grantUniveralAccess: true
});

真正的复杂性出现在跨多个操作使用元素时。

阻抗不匹配

Verdex 维护一个持久的 Map,在分析步骤之间追踪 DOM 节点。使用 Playwright + CDP 时,需要在两种对象模型之间搭桥:

  • Playwright 自动管理的 ElementHandle(实用世界)
  • 手动管理的 CDP objectId(隔离世界)

在它们之间转换需要额外的 evaluate 调用和上下文切换,实际上绕过了 Playwright 的抽象层,却仍要承担其跨浏览器包的开销。

为什么 Puppeteer 更自然契合

Puppeteer 直接操作 CDP 原语,保持单一抽象层级:

const session = await page.target().createCDPSession();

// 创建隔离世界
const { executionContextId } = await session.send('Page.createIsolatedWorld', {
  frameId: mainFrameId,
  worldName: 'verdex-bridge',
  grantUniveralAccess: true
});

// 注入桥接代码
await session.send('Runtime.evaluate', {
  expression: bridgeCode,
  contextId: executionContextId
});

// 多步骤操作的稳定 objectId
const { result } = await session.send('Runtime.evaluate', {
  expression: 'document.querySelector("[data-testid=product]")',
  contextId: executionContextId
});

// 在后续调用中使用相同的 objectId
const analysis = await session.send('Runtime.callFunctionOn', {
  objectId: result.objectId,
  functionDeclaration: 'function() { return window.verdexBridge.fullAnalysis(this); }',
  executionContextId,
  returnByValue: true
});

const { result: ancestors } = await session.send('Runtime.callFunctionOn', {
  functionDeclaration: 'function() { return window.verdexBridge.get_ancestors(this); }',
  objectId: result.objectId,
  executionContextId,
  returnByValue: true
});

没有阻抗不匹配、没有与自动释放的斗争,也不需要在对象模型之间搭桥。puppeteer 包(约 2 MB)是 CDP 原生的,而 playwright-core(约 10 MB)则包含了 Verdex 从未使用的跨浏览器抽象。

Verdex architecture diagram

Back to Blog

相关文章

阅读更多 »

别再买Mac来修复 CSS 了

“黑客”方式在 Windows 与 Linux 上调试 Safari 说实话:Safari 已经成了新的 Internet Explorer。作为网页开发者,我们主要使用 Chromium Chrome……