Playwright 的 CDP Session 机制详解

一、为什么 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 接管已有浏览器),可以继续深入探讨!

相关推荐
aqi002 小时前
15天学会AI应用开发(八)使用向量数据库实现RAG功能
人工智能·python·大模型·ai编程·ai应用
小虎AI生活2 小时前
知识库踩坑实录,用 WorkBuddy + IMA 搭知识库最容易犯的 5 个大坑
ai编程
怕浪猫2 小时前
第一章:AI Agent概览:开启智能体时代
aigc·agent·ai编程
混沌福王3 小时前
Electron三端统一架构:运行时Adapter、IPC能力边界与分层设计
人工智能·agent·ai编程
唐老板4 小时前
AI 辅助开发的工程体系:从定规则到基础设施
ai编程
Alson_Code4 小时前
人机协作项目文档--HITL-AgentScope
后端·aigc·ai编程
小虎AI生活19 小时前
Agent 工具那么多,为什么我押注 WorkBuddy 加 ima
ai编程
leeyi1 天前
Prompt 模板:用变量组装发给 AI 的消息
aigc·agent·ai编程