一、为什么 Playwright 需要 CDP Session?
Playwright 的核心 API 是跨浏览器抽象层(Chromium / Firefox / WebKit 统一接口),但有些能力只有 CDP 才能提供:
| 场景 | Playwright 高层 API | CDP 直连 |
|---|---|---|
| 截图 | ✅ page.screenshot() |
✅ Page.captureScreenshot |
| 网络拦截 | ✅ page.route() |
✅ Network.setRequestInterception |
| 获取原始响应体 | ❌ 只能拿到 body | ✅ Network.getResponseBody(含 headers) |
| 性能 Trace | ❌ | ✅ Tracing.start/end |
| 覆盖地理位置 | ✅ 有限 | ✅ Emulation.setGeolocationOverride |
| JS 覆盖率 | ❌ | ✅ Profiler.startPreciseCoverage |
| 证书错误处理 | ❌ | ✅ Security.handleCertificateError |
| 多 Target 通信 | ❌ | ✅ Target.attachToTarget |
CDP Session 就是 Playwright 打开的一扇"后门" ------在保持高层 API 优雅的同时,让你能直达 CDP 协议层。
二、CDPSession 的创建
Playwright 提供两种方式获取 CDP Session:
1. 页面级 Session(最常用)
javascript复制
javascript
const { chromium } = require('playwright');
const browser = await chromium.launch();
const page = await browser.newPage();
// 创建与当前页面关联的 CDP Session
const cdp = await page.context().newCDPSession(page);
// 现在可以直接发 CDP 命令
await cdp.send('Network.enable');
2. 浏览器级连接(高级)
javascript复制
ini
// 直接通过 WebSocket 连接到已运行的 Chrome
const browser = await chromium.connectOverCDP('http://localhost:9222');
// 获取已有页面的 CDP Session
const contexts = browser.contexts();
const page = contexts[0].pages()[0];
const cdp = await page.context().newCDPSession(page);
三、CDPSession 核心 API
CDPSession 对象只有三个方法 + 两个事件模式:
code复制
csharp
┌──────────────────────────────────────────┐
│ CDPSession │
│ │
│ Methods: │
│ send(method, params) → Promise<result>│
│ detach() → Promise<void> │
│ │
│ Events: │
│ on(event, handler) │
│ off(event, handler) │
│ │
│ Lifecycle: │
│ 'disconnected' event │
└──────────────────────────────────────────┘
send(method, params) --- 发送 CDP 命令
javascript复制
javascript
// 等价于 CDP 的 JSON-RPC 请求
const result = await cdp.send('Runtime.evaluate', {
expression: 'document.title',
returnByValue: true
});
console.log(result.result.value); // "Example Domain"
on(event, handler) --- 监听 CDP 事件
javascript复制
csharp
// 监听网络请求
await cdp.send('Network.enable');
cdp.on('Network.requestWillBeSent', (params) => {
console.log('→', params.request.url);
});
cdp.on('Network.responseReceived', (params) => {
console.log('←', params.response.status, params.response.url);
});
detach() --- 断开 Session
javascript复制
csharp
await cdp.detach();
// 之后 send() 会抛出错误
四、实战场景
场景 1:获取网络请求的完整响应头
Playwright 的 response.headers 只返回部分头,CDP 能拿到全部:
javascript复制
javascript
const cdp = await page.context().newCDPSession(page);
await cdp.send('Network.enable');
const responseHeaders = new Map();
cdp.on('Network.responseReceived', ({ response }) => {
responseHeaders.set(response.url, response.headers);
});
await page.goto('https://example.com');
// 遍历所有响应头(包含 Playwright 不暴露的 security headers 等)
for (const [url, headers] of responseHeaders) {
console.log(url, headers);
}
场景 2:性能 Trace(Chrome Performance Panel 同款数据)
javascript复制
javascript
const cdp = await page.context().newCDPSession(page);
// 开始 Trace
await cdp.send('Tracing.start', {
traceConfig: {
includedCategories: ['devtools.timeline', 'disabled-by-default-devtools.timeline']
}
});
await page.goto('https://example.com');
await page.click('#some-button');
// 结束 Trace
await cdp.send('Tracing.end');
// 收集 Trace 数据
cdp.on('Tracing.tracingComplete', ({ value }) => {
// value 是 trace 文件路径
console.log('Trace saved to:', value);
});
场景 3:JS 代码覆盖率
javascript复制
javascript
const cdp = await page.context().newCDPSession(page);
await cdp.send('Profiler.enable');
await cdp.send('Profiler.startPreciseCoverage', {
callCount: true,
detailed: true
});
await page.goto('https://example.com');
// ... 用户操作 ...
const coverage = await cdp.send('Profiler.takePreciseCoverage');
for (const script of coverage.result) {
console.log(`File: ${script.url}`);
for (const func of script.functions) {
console.log(` Function: ${func.functionName}, Uses: ${func.hitCount}`);
}
}
await cdp.send('Profiler.stopPreciseCoverage');
场景 4:地理位置欺骗(比 Playwright 原生更细粒度)
javascript复制
csharp
const cdp = await page.context().newCDPSession(page);
await cdp.send('Emulation.setGeolocationOverride', {
latitude: 35.6762,
longitude: 139.6503, // 东京
accuracy: 100
});
await page.goto('https://maps.google.com');
// 页面会显示东京位置
场景 5:拦截并修改响应(CDP 方式)
javascript复制
dart
const cdp = await page.context().newCDPSession(page);
await cdp.send('Network.enable');
await cdp.send('Network.setRequestInterception', {
patterns: [{ urlPattern: '*api/data*' }]
});
cdp.on('Network.requestIntercepted', async ({ interceptionId, request }) => {
// 修改请求后继续
await cdp.send('Network.continueInterceptedRequest', {
interceptionId,
url: request.url.replace('v1', 'v2') // 重写 URL
});
});
场景 6:处理 SSL 证书错误
javascript复制
javascript
const cdp = await page.context().newCDPSession(page);
await cdp.send('Security.enable');
await cdp.send('Security.setOverrideCertificateErrors', {
override: true
});
cdp.on('Security.certificateError', ({ eventId, errorType }) => {
console.log(`Certificate error: ${errorType}`);
// 继续加载(忽略证书错误)
cdp.send('Security.handleCertificateError', {
eventId,
action: 'continue'
});
});
五、多 Session 机制
Playwright 允许同一个页面创建多个独立的 CDP Session,它们互不干扰:
javascript复制
javascript
const cdp1 = await page.context().newCDPSession(page);
const cdp2 = await page.context().newCDPSession(page);
// Session 1: 监控网络
await cdp1.send('Network.enable');
cdp1.on('Network.requestWillBeSent', (e) => {
console.log('[Network]', e.params.request.url);
});
// Session 2: 监控控制台
await cdp2.send('Runtime.enable');
cdp2.on('Runtime.consoleAPICalled', (e) => {
console.log('[Console]', e.params.type, e.params.args);
});
code复制
scss
┌──────────┐ ┌──────────────┐
│ Playwright│ │ Chrome │
│ Page │────►│ │
│ │ │ │
│ cdp1 ─────┼────►│ DevTools │
│ (Network) │ │ Agent │
│ │ │ │
│ cdp2 ─────┼────►│ │
│ (Runtime) │ │ │
└──────────┘ └──────────────┘
每个 Session 独立的 WebSocket 连接
六、connectOverCDP --- 连接已有浏览器
这个 API 让 Playwright 接管一个已经在运行的 Chrome(保留了登录态、Cookie、已有标签页):
javascript复制
csharp
// 1. 先启动一个带调试端口的 Chrome
// chrome --remote-debugging-port=9222
// 2. Playwright 连接上去
const browser = await chromium.connectOverCDP('http://localhost:9222');
// 3. 获取已有上下文和页面
const context = browser.contexts()[0]; // 已有的 BrowserContext
const page = context.pages()[0]; // 已有的标签页
// 4. 既可以走 Playwright 高层 API,也可以开 CDP Session
await page.click('#login');
const cdp = await context.newCDPSession(page);
await cdp.send('Network.enable');
典型场景:
- 复用已登录的浏览器(避免重复登录)
- 操作已有的标签页
- 与其他 CDP 客户端共享同一个浏览器实例
七、Playwright CDP vs Puppeteer CDP 对比
| 维度 | Playwright | Puppeteer |
|---|---|---|
| CDP Session 创建 | context.newCDPSession(page) |
page.createCDPSession() |
| 连接已有浏览器 | chromium.connectOverCDP(url) |
puppeteer.connect({ browserWSEndpoint }) |
| 默认 Session | 隐藏,内部管理 | 暴露 page._client() |
| 多浏览器 | CDP 仅 Chromium 通道 | 天然 Chromium-only |
| 事件命名 | 去掉域前缀 requestWillBeSent |
保留域前缀 可选 |
| 类型安全 | 部分 | 完整 .d.ts |
| Session 生命周期 | 与页面绑定,页面关闭自动断开 | 同左 |
关键差异 :Playwright 的 CDP 事件名不带域前缀:
javascript复制
csharp
// Playwright
cdp.on('requestWillBeSent', handler); // ✅ 无域前缀
cdp.on('Network.requestWillBeSent', handler); // ❌ 不工作
// Puppeteer
cdp.on('Network.requestWillBeSent', handler); // ✅ 带域前缀
八、注意事项 & 陷阱
1. CDP 与 Playwright API 的冲突
javascript复制
csharp
// ❌ 不要同时用两层做同一件事
await page.route('**/*', handler); // Playwright 拦截
await cdp.send('Network.setRequestInterception', { patterns: [...] }); // CDP 拦截
// 两者会冲突,导致请求卡住!
原则:同一功能只用一层,不要混用。
2. CDP 只在 Chromium 上可用
javascript复制
csharp
// Firefox / WebKit 不支持 CDP Session
const cdp = await page.context().newCDPSession(page);
// → 在 Firefox 上会抛出: "CDPSession is only supported in Chromium"
3. Session 断开处理
javascript复制
javascript
const cdp = await page.context().newCDPSession(page);
cdp.on('disconnected', () => {
console.log('CDP Session 已断开(页面可能被关闭)');
});
// 安全使用
try {
await cdp.send('SomeMethod');
} catch (e) {
if (e.message.includes('Target closed')) {
// 页面已关闭,Session 无效
}
}
4. send() 是异步的,注意时序
javascript复制
csharp
// ❌ 错误:可能错过早期事件
await page.goto('https://example.com');
await cdp.send('Network.enable'); // 太晚了,页面加载的网络请求已经结束
// ✅ 正确:先启用,再导航
await cdp.send('Network.enable');
await page.goto('https://example.com');
5. 性能开销
javascript复制
scss
// 每个 CDPSession 是一个独立的 WebSocket 连接
// 不要无节制地创建
// ❌
for (let i = 0; i < 100; i++) {
await page.context().newCDPSession(page); // 100 个 WebSocket!
}
九、架构全景图
code复制
scss
┌─────────────────────────────────────┐
│ Playwright │
│ │
用户代码 ─────────►│ 高层 API │
page.click() │ ├── Page │
page.goto() │ ├── Frame │
page.route() │ ├── Locator │
│ └── ... │
│ │
cdp.send() ──────►│ CDP Session Layer │
cdp.on() │ ├── CDPSession (page-level) │
│ ├── CDPSession (browser-level) │
│ └── connectOverCDP │
│ │
│ Transport Layer │
│ ├── Pipe (默认,更快) │
│ └── WebSocket (connectOverCDP) │
└──────────┬──────────────────────────┘
│
┌──────────▼──────────┐
│ Chrome Browser │
│ DevTools Agent │
└─────────────────────┘
十、TypeScript 类型支持
Playwright 为 CDP 提供了基础类型,但不如 Puppeteer 完整:
typescript复制
typescript
import { CDPSession } from 'playwright';
const cdp: CDPSession = await page.context().newCDPSession(page);
// send 的 method 是 string,params 是 object
// 返回值类型为 any(需要自行断言)
const result = await cdp.send('Runtime.evaluate', {
expression: '1 + 1',
returnByValue: true
}) as { result: { type: string; value: number } };
console.log(result.result.value); // 2
如果需要完整的 CDP 类型定义,可以安装:
bash复制
swift
npm install @types/chrome-devtools-protocol
总结
| 概念 | 一句话 |
|---|---|
| CDPSession | Playwright 与 Chrome CDP 之间的通信通道 |
| 创建方式 | context.newCDPSession(page) |
| 核心方法 | send() 发命令,on() 监事件 |
| 多 Session | 同一页面可创建多个独立 Session |
| connectOverCDP | 连接已有 Chrome 实例,复用登录态 |
| 适用场景 | Playwright 高层 API 覆盖不到的 CDP 专有能力 |
| 限制 | 仅 Chromium,不能与 Playwright API 冲突使用 |
| 事件名 | 无域前缀(requestWillBeSent 而非 Network.requestWillBeSent) |
CDPSession 的本质是:在 Playwright 的跨浏览器抽象之上,开一扇直达 Chromium 内核的活板门。
以上就是 Playwright CDP Session 机制的完整解析。如果你有具体的实战需求(比如用 CDP 做性能 Trace 分析、或者 connectOverCDP 接管已有浏览器),可以继续深入探讨!