首页
/ Playwright CDPSession 完全指南:直接与 Chrome DevTools Protocol 对话

Playwright CDPSession 完全指南:直接与 Chrome DevTools Protocol 对话

2026-09-06 22:13:09作者:贡沫苏Truman

CDPSession 是 Playwright 提供的“逃生舱口”(escape hatch):它让自动化脚本绕过 Playwright 的高层 API,直接调用原始的 Chrome DevTools Protocol(CDP)方法、订阅原始 CDP 事件。本文基于 Playwright 仓库中的 API 文档 docs/src/api/class-cdpsession.md 与对应实现源码展开,完整覆盖 newCDPSession 的创建方式、send/on/off/event/detach 全部 API、四种语言(JavaScript/Python/C#/Java)用法,并结合仓库源码剖析从客户端到 Chromium 后端的完整调用链,帮助你在 Playwright API 覆盖不到的场景(如动画控制、底层调试、OOPIF 操作)中自如地“下沉”到协议层。

什么是 CDPSession,适用于哪些浏览器

自 v1.8 起,Playwright 提供 CDPSession 实例来与原始 Chrome DevTools Protocol 对话:

  • 协议方法通过 session.send 调用;
  • 协议事件通过 session.on 订阅。

CDP 是 Chromium 浏览器(Chrome、Chromium、Edge 等)暴露的底层调试协议,域(domain)包括 RuntimeNetworkDebuggerAnimationPage 等。由于 CDP 是 Chromium 系浏览器专属,newCDPSession 仅在 Chromium 下可用——这一点在源码中有硬性校验:browserContextDispatcher.ts 中,如果浏览器类型不是 chromium,会直接抛出 CDP session is only available in Chromium

官方 CDP 协议文档可在 DevTools Protocol 官方文档站点查阅(DevTools Protocol Viewer),Playwright 仓库内也内置了协议类型定义,见 protocol 目录 下的 protocol.d.ts(由 CDP 生成)。

创建会话:BrowserContext.newCDPSession

会话通过 BrowserContext.newCDPSession(pageOrFrame) 创建,参数必须是 PageFrame 二选一。四种语言的创建与使用示例如下(完整继承自 API 文档):

JavaScript

const client = await page.context().newCDPSession(page);
await client.send('Animation.enable');
client.on('Animation.animationCreated', () => console.log('Animation created!'));
const response = await client.send('Animation.getPlaybackRate');
console.log('playback rate is ' + response.playbackRate);
await client.send('Animation.setPlaybackRate', {
  playbackRate: response.playbackRate / 2
});

Python(异步)

client = await page.context.new_cdp_session(page)
await client.send("Animation.enable")
client.on("Animation.animationCreated", lambda: print("animation created!"))
response = await client.send("Animation.getPlaybackRate")
print("playback rate is " + str(response["playbackRate"]))
await client.send("Animation.setPlaybackRate", {
    "playbackRate": response["playbackRate"] / 2
})

Python(同步)

client = page.context.new_cdp_session(page)
client.send("Animation.enable")
client.on("Animation.animationCreated", lambda: print("animation created!"))
response = client.send("Animation.getPlaybackRate")
print("playback rate is " + str(response["playbackRate"]))
client.send("Animation.setPlaybackRate", {
    "playbackRate": response["playbackRate"] / 2
})

C#

var client = await Page.Context.NewCDPSessionAsync(Page);
await client.SendAsync("Runtime.enable");
client.Event("Animation.animationCreated").OnEvent += (_, _) => Console.WriteLine("Animation created!");
var response = await client.SendAsync("Animation.getPlaybackRate");
var playbackRate = response.Value.GetProperty("playbackRate").GetDouble();
Console.WriteLine("playback rate is " + playbackRate);
await client.SendAsync("Animation.setPlaybackRate", new() { { "playbackRate", playbackRate / 2 } });

Java

CDPSession client = page.context().newCDPSession(page);
client.send("Runtime.enable");

client.on("Animation.animationCreated", (event) -> System.out.println("Animation created!"));

JsonObject response = client.send("Animation.getPlaybackRate");
double playbackRate = response.get("playbackRate").getAsDouble();
System.out.println("playback rate is " + playbackRate);

JsonObject params = new JsonObject();
params.addProperty("playbackRate", playbackRate / 2);
client.send("Animation.setPlaybackRate", params);

客户端入口的校验逻辑

客户端 browserContext.tsnewCDPSession 会先做参数校验:

async newCDPSession(page: Page | Frame): Promise<api.CDPSession> {
  if (!(page instanceof Page) && !(page instanceof Frame))
    throw new Error('page: expected Page or Frame');
  const result = await this._channel.newCDPSession(
    page instanceof Page ? { page: page._channel } : { frame: frame._channel },
    kNoTimeout
  );
  return CDPSession.from(result.session);
}

注意这里用 kNoTimeout 发起请求,即创建会话不受默认 action timeout 限制。服务端校验则更严格(browserContextDispatcher.ts#L373-L380):

  • 浏览器必须为 Chromium,否则抛 CDP session is only available in Chromium
  • pageframe 必须恰好提供一个(不能都空、也不能都给);
  • 传入 Frame 时,该 Frame 必须拥有独立的 CDP 会话(即 out-of-process iframe)。从源码 crBrowser.ts#L599-L614 看,若 frame 没有独立会话会抛出 This frame does not have a separate CDP session, it is a part of the parent frame's session。测试 tests/library/chromium/session.spec.ts#L107-L114 验证了这一点:对普通 iframe 调用 newCDPSession 会得到上述错误,而对主 frame(page.mainFrame())则可以正常工作。

内部创建流程为:CRBrowserContext.newCDPSession 取出目标 targetId(Page 直接取 CRPage._targetId,Frame 取其独立 session 的 _targetId),再通过根会话执行 Target.attachToTarget 附加上去。这条链路解释了为什么 Page 与 Frame 都能作为入口——最终都归一化为一个 CDP target。

CDPSession.send:调用协议方法

send 是最核心的方法,签名为 send(method, params?)

参数 类型 说明
method string 协议方法名,如 'Animation.enable''Runtime.evaluate'
params Object(JS/Python)/ Map<string, Object>(C#,别名 args)/ JsonObject(Java,别名 args 可选的方法参数

返回值按语言而异:JS 返回 Object;C# 返回 JsonElement?;Java 返回 JsonObject

客户端实现位于 cdpSession.ts#L48-L54

async send<T extends keyof Protocol.CommandParameters>(
  method: T,
  params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
  const result = await this._channel.send({ method, params }, kNoTimeout);
  return result.result as Protocol.CommandReturnValues[T];
}

两个值得注意的细节:

  1. 类型安全:JS 客户端的 send 用 CDP 协议类型 Protocol.CommandParameters[T] 做了泛型约束,method 必须是合法 CDP 命令名,参数与返回值类型都能被 TypeScript 推导;
  2. 无超时:与创建会话一致,send 使用 kNoTimeout,即等待 CDP 响应时不受 Playwright action timeout 约束(但连接断开或目标关闭仍会抛错)。

错误处理同样有测试保障(session.spec.ts#L84-L94):发送不存在的命令时,错误信息会包含原始命令名(如 ThisCommand.DoesNotExist),且错误堆栈会保留调用方函数名,便于定位。

订阅 CDP 事件:on / off / event / close

按名称订阅:session.on(及 Java 的 on/off)

JS/Python 端 CDPSessionEventEmitter,直接用 session.on('CDP.事件名', handler) 订阅;C# 端通过 session.Event(eventName) 获取事件发射器(返回 CDPSessionEvent)再挂 OnEvent;Java 端提供 session.on(eventName, handler) 与对称的 session.off(eventName, handler) 用于注册和注销监听。

事件来源的透传机制在客户端 cdpSession.ts#L32-L35

this._channel.on('event', event => {
  this.emit(event.method, event.params);
  this.emit('event', event);
});

服务端(crConnection.ts#L214-L221)把 Chromium 发来的每条 CDP 消息以 Target.receivedMessageFromTarget 的形式转发为 event(携带 methodparams),再经 cdpSessionDispatcher.ts#L30 通过 _dispatchEvent('event', { method, params }) 推送给客户端。也就是说,你监听的每个 CDP 事件名都对应一条“Chromium → 服务端 → dispatcher → 客户端 channel”的透传链路。

一个典型用法是监听网络请求(测试 session.spec.ts#L32-L39):

const client = await page.context().newCDPSession(page);
await client.send('Network.enable');
const events = [];
client.on('Network.requestWillBeSent', event => events.push(event));
await page.goto(server.EMPTY_PAGE);
expect(events.length).toBe(1);

全量兜底订阅:CDPSession.event(仅 JS,v1.59+)

如果你不想事先知道事件名,可以订阅 event 事件,它会为每条从会话收到的 CDP 事件触发,参数为:

  • methodstring):CDP 事件名;
  • paramsObject?):CDP 事件参数。
session.on('event', ({ method, params }) => {
  console.log(`CDP event: ${method}`, params);
});

这正对应上文客户端实现中的 this.emit('event', event) 一行——它在按名称分发的同时把原始 {method, params} 再广播一次,专门服务于这种“全量嗅探”场景(如调试时记录页面上发生的所有 CDP 活动)。

会话关闭事件:CDPSession.close(v1.59+)

当会话关闭时触发,参数为被关闭的 CDPSession 本身。触发条件有二:

  1. 目标(target)被关闭,例如页面/浏览器上下文关闭、target 崩溃;
  2. 调用了 session.detach()

服务端在 crConnection.ts#L217-L220 监听父会话上的 Target.detachedFromTarget,一旦 sessionId 匹配当前会话即触发 close;dispatcher 层则把它映射为 close 事件并自我销毁(cdpSessionDispatcher.ts#L31-L34),客户端 channel 收到后 emit('close', this)。实践中可以用它做资源清理:

client.on('close', () => console.log('CDP session closed'));

CDPSession.detach:主动断开

detach 为异步方法(各语言异步版本名略有差异,如 C# 的 DetachAsync),作用是把 CDPSession 从目标上分离:一旦分离,该对象不再发出任何事件,也无法再发送消息。

服务端实现(crConnection.ts#L178-L186)包含两个步骤:

// Ideally, detaching should resume any target, but there is a bug in the backend,
// so we must Runtime.runIfWaitingForDebugger first.
await this._sendMayFail('Runtime.runIfWaitingForDebugger');
await this._parentSession.send('Target.detachFromTarget', { sessionId: this._sessionId });

即先发 Runtime.runIfWaitingForDebugger(规避后端 detach 时不恢复 target 的 bug),再向父会话发送 Target.detachFromTarget。测试 session.spec.ts#L69-L82 验证了 detach 后的行为:再次 send 会抛出 Target page, context or browser has been closed

另有一个重要的正确性细节(session.spec.ts#L116-L120):持有 CDP 会话时调用 page.close() 不会死锁,且 detach 之后关闭页面同样安全——Playwright 内部对“会话存活”与“页面生命周期”做了隔离处理。

域(Domain)的启停是相互独立的

CDP 的很多域需要先 enable 才会发事件。测试 session.spec.ts#L52-L67 验证了一个关键特性:不同域的 enable/disable 互不干扰。用例中先通过 client.send 启用 RuntimeDebugger 域,随后 Playwright 自己的 JS coverage 功能会独立地启用并停用 Debugger 域;结果用户通过 CDPSession 收到的 Debugger.scriptParsed 事件依然完整触发。这保证了你的 CDP 会话不会被 Playwright 内部的协议操作“踢掉”。

典型应用场景与限制小结

结合文档与源码,可以总结以下使用要点:

  • 仅 Chromium:Firefox/WebKit 下调用 newCDPSession 会直接报错;
  • 参数约束:必须恰好传入一个 PageFrame;普通(进程内)frame 没有独立 CDP 会话,需 OOP iframe 或主 frame;
  • 生命周期:目标关闭或 detach() 都会触发 close 事件,之后会话不可用;未决的 send 回调会在会话关闭时被批量 reject(crConnection.ts#L194-L202),错误类型标记为 crashedclosed
  • 无超时语义newCDPSessionsend 均使用 kNoTimeout,不会因 action timeout 而中断,适合长耗时协议操作;
  • 协议参考:所有 method/事件名以官方 Chrome DevTools Protocol 文档为准,仓库内的 Protocol 类型定义(packages/playwright-core/src/server/chromiumprotocol.d.ts)可作为完整方法清单的速查表。

CDPSession 的定位是“Playwright 覆盖不到的地方才下沉”:对于动画速率控制、底层调试、OOPIF 级操作、自定义协议扩展等场景,它是与 CDP 直接对话的唯一官方入口;而日常测试应优先使用 Playwright 高层 API,以获得更好的跨浏览器兼容性与稳定性。

完整的行为验证可以参考测试文件 tests/library/chromium/session.spec.ts,协议层命令定义见 packages/protocol/spec/browserContext.yml 中的 newCDPSession 条目。

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