十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Puppeteer CDPSession.detach() 详解:CDP 会话的分离、detached 状态与生命周期管理

Puppeteer CDPSession.detach() 详解:CDP 会话的分离、detached 状态与生命周期管理 Puppeteer CDPSession.detach() 详解CDP 会话的分离、detached 状态与生命周期管理【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer在 Puppeteer 中CDPSession是与浏览器直接对话 Chrome DevTools ProtocolCDP原始通道的入口而detach()则是这个通道的关闭开关调用后该会话与目标Target断开不再发射任何事件也无法再发送协议命令。本篇围绕 CDPSession.detach() 文档 展开结合 CDPSession 抽象类、CDP 后端实现 与 BiDi 后端实现 的源码讲清楚detach()的签名、detach 后的行为边界、detached属性的真实含义、浏览器侧被动断开的自动处理机制以及官方测试对这套生命周期契约的验证方式。方法签名与基本语义detach 文档 给出的官方定义如下class CDPSession { abstract detach(): Promisevoid; }参数无返回值Promisevoiddetach 命令在协议层面确认后 resolve语义将 cdpSession 从目标target上分离。一旦分离该 cdpSession 对象不再发射任何事件也不能用于发送消息。这是一个抽象方法声明于 api/CDPSession.ts 的 CDPSession 基类。CDPSession本身继承自EventEmitterCDPSessionEvents见 CDPSession 类文档因此不再发射事件意味着 detach 后挂载在其上的所有client.on(...)监听器都会永久静默——这是理解 detach 行为的核心前提。一个典型的使用场景通过page.createCDPSession()拿到会话、启用某个协议域如Animation.enable、订阅事件并发送命令后用完主动 detach 释放资源import puppeteer from puppeteer; const browser await puppeteer.launch(); const page await browser.newPage(); const client await page.createCDPSession(); 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); // 用完即断开此后 client 不再收事件send 也会立即失败 await client.detach();上述启用域 → 订阅事件 → 发送命令的模式正是 CDPSession 类文档 中给出的标准示例detach()是这条生命周期中收尾的最后一步。CDP 后端的实现一条 Target.detachFromTarget 命令在 CDP 后端detach()由内部类CdpCDPSession实现见 cdp/CdpSession.tsoverride async detach(): Promisevoid { if (this.detached) { throw new Error( Session already detached. Most likely the ${this.#targetType} has been closed., ); } await this.#connection.send(Target.detachFromTarget, { sessionId: this.#sessionId, }); this.#detached true; }从源码结构可以读出三个关键行为幂等性不成立——重复 detach 会抛错。如果会话已经处于 detached 状态自己 detach 过或页面/连接先一步关闭了它再次调用detach()会抛出Session already detached. Most likely the targetType has been closed.。这与detach 后不能再发送消息的文档语义一致对象进入终态。真正断开靠协议命令Target.detachFromTarget且注意它发送在底层的connection浏览器级 CDP 连接上而不是会话自身——因为 detach 本身就是注销这个 sessionId的元命令无法再从会话通道发出。会话自身的 id 即sessionId由id()返回。协议确认后本地置位#detached true此后detached属性为true。detached属性的判定逻辑detached是 CDPSession 文档 中列出的只读布尔属性True if the session has been detached, false otherwise.。CDP 后端的 getter 实现值得注意CdpSession.ts#L80-L82override get detached(): boolean { return this.#connection._closed || this.#detached; }也就是说detached true有两种来源自己调用了detach()或底层 WebSocket 连接已经关闭connection._closed。后者对应浏览器进程退出、页面崩溃等场景——会话事实上已死。因此在写健壮的代码时detached是判断这个会话还能不能用的统一前置检查if (!client.detached) { const res await client.send(Runtime.evaluate, { expression: 1 2, returnByValue: true, }); }detach 之后 send() 会发生什么detach 后继续send()会走 send 方法开头的短路分支同步地以TargetCloseError拒绝 PromiseProtocol error (method): Session closed. Most likely the targetType has been closed.官方测试 test/src/cdp/CDPSession.test.ts 中 should be able to detach session 精确验证了这一点it(should be able to detach session, async () { const {page} await getTestState(); const client await page.createCDPSession(); await client.send(Runtime.enable); const evalResponse await client.send(Runtime.evaluate, { expression: 1 2, returnByValue: true, }); expect(evalResponse.result.value).toBe(3); await client.detach(); let error!: Error; try { await client.send(Runtime.evaluate, { expression: 3 1, returnByValue: true, }); } catch (error_) { if (isErrorLike(error_)) { error error_ as Error; } } expect(error.message).toContain(Session closed.); });注意错误类型它不是普通的Error而是 TargetCloseErrorPuppeteerError的子类。这意味着你可以用error.name TargetCloseError或在捕获时区分会话级关闭与协议层报错对 detach 竞态做容错。浏览器侧的被动断开Connection 的兜底机制detach 不只是本地行为——浏览器端同样可以单方面断开会话例如目标页面被关闭、iframe 被移除。这一方向的收口在 cdp/Connection.ts 的 onMessage 中} else if (object.method Target.detachedFromTarget) { const session this.#sessions.get(object.params.sessionId); if (session) { session.onClosed(); this.#sessions.delete(object.params.sessionId); this.emit(CDPSessionEvent.SessionDetached, session); const parentSession this.#sessions.get(object.sessionId); if (parentSession) { parentSession.emit(CDPSessionEvent.SessionDetached, session); } } }当浏览器发来Target.detachedFromTarget事件时Connection 会做四件事调用session.onClosed()——内部实现CdpSession.ts#L170-L174清除所有挂起的协议回调#callbacks.clear()保证那些send()的 Promise 不会永远悬着、置#detached true并发射内部事件CDPSessionEvent.DisconnectedSymbol 事件internal从 Connection 的#sessions映射中删除该会话在 Connection 上发射sessiondetached事件载荷就是该会话对象如果存在父会话auto-attach 层级同时在父会话上再发射一次便于上层感知子会话如 OOP iframe 的会话被断开。这里有两个 API 细节值得说明CDPSessionEvent.SessionDetached是公开事件名字符串sessiondetached定义在 api/CDPSession.ts对应 CDPSessionEvent 文档CDPSessionEvent.DisconnectedSymbol与Swapped、Ready一样是internal事件Puppeteer 内部用它串联 FrameManager、TargetManager 等模块的清理逻辑不建议在业务代码中依赖。另一个兜底路径在send()里如果协议返回Session with given id not found比如目标在命令在途时被关闭实现会自动onClosed()并把错误转成TargetCloseError: Protocol error (method): Session with given id not found.CdpSession.ts#L108-L119。官方测试 should handle session callbacks when Chrome sends error without sessionId 专门验证了这个错误会被包装成TargetCloseError抛出。detach 之后底层 Connection 并不会死一个容易混淆的点detach 一个会话不等于关闭 CDP 连接。官方测试明确断言了这一点test/src/cdp/CDPSession.test.ts should keep the underlying connection after being detachedit(should keep the underlying connection after being detached, async () { const {page} await getTestState(); const client await page.createCDPSession(); const connection client.connection(); await client.detach(); expect(client.connection()).toBe(connection); });client.connection()见 connection() 文档在 detach 前后返回同一个Connection实例而detached属性的状态变化由另一个测试固化should expose detached stateit(should expose detached state, async () { const {page} await getTestState(); const client await page.createCDPSession(); expect(client.detached).toBe(false); await client.detach(); expect(client.detached).toBe(true); });可以推断其设计意图一次launch()/connect()建立的浏览器级连接被该浏览器下的所有页面、所有会话共享detach 只是注销某一个sessionId连接与兄弟会话同一 target 上的其他会话、其他 target 的会话均不受影响。这决定了detach 后重建会话是安全且廉价的做法// 会话已 detach 或目标仍在直接新建一个会话继续工作 if (client.detached) { const fresh await page.createCDPSession(); await fresh.send(Runtime.enable); }BiDi 模式下 detach 的差异化行为CDPSession在 WebDriver BiDi 后端也有一个实现BidiCdpSessionbidi/CDPSession.ts其detach()语义与 CDP 后端存在两处可观察的差异override async detach(): Promisevoid { if ( this.#connection undefined || this.#connection.closed || this.#detached ) { return; } try { await this.frame.client.send(Target.detachFromTarget, { sessionId: this.id(), }); } finally { this.onClose(); } }静默成功 vs 抛错当浏览器不支持 CDPcdpSupported为 false 时#connection为undefined、连接已关闭、或已经 detach 时BiDi 后端的detach()直接return而不抛异常CDP 后端在已 detach时会抛出Session already detached...错误。从源码结构看这是两种后端对detach 幂等性的有意分歧跨后端编写代码时不应依赖重复 detach 必然抛错这一行为detach 命令的发送通道不同BiDi 模式没有独立的 CDP 连接Target.detachFromTarget通过goog:cdp.*兼容通道frame.client即 BiDi 会话下发且finally中保证onClose()从全局sessions表移除并置 detached一定执行。BiDi 下send()在 detach 后同样抛TargetCloseErrorbidi/CDPSession.ts#L67-L93错误文案为Session closed. Most likely the page has been closed.。对 API 使用者而言两套后端对外承诺的核心契约是一致的detach 后detached为 true、send()失败、事件停止差异主要在错误行为与底层通道。内部实现视角Puppeteer 自己为何不直接用 session.detach()一个能反映detach()命令路由细节的内部用例来自 cdp/TargetManager.ts。Puppeteer 管理 OOP iframe、worker 等子 target 的会话时内部走的是#silentDetach// We dont use session.detach() because that dispatches all commands on // the connection instead of the parent session. await parentSession.send(Target.detachFromTarget, { sessionId: session.id(), });源码注释直接解释了原因session.detach()会把Target.detachFromTarget命令发到底层 connection上而 TargetManager 在 auto-attach 层级中需要通过父会话下发以保证命令路由到正确的 target 域。这从侧面印证了上一节的实现分析也提示如果你要在自定义 Target 管理逻辑中精细控制 detach理解命令从哪个会话/连接发出是必要的而常规业务代码只需用page.createCDPSession()client.detach()即可无需触碰这一层。另外注意区分两组容易混淆的概念CDPSession.detach()分离的是协议会话而测试代码如 test/src/utils.ts 的 detachFrame与framedetached事件中的 detach 指的是DOM 中 iframe 从页面移除属于 Frame 生命周期与本方法无直接调用关系。实践要点速查结合文档语义与源码行为detach 相关的实践检查清单如下场景行为以 CDP 后端为准依据首次await client.detach()发送Target.detachFromTarget确认后 resolvedetached变trueCdpSession.ts#L155-L165重复detach()抛Error: Session already detached...同上detach 后send()拒绝为TargetCloseError含 Session closed.CdpSession.ts#L94-L105、detach 测试连接关闭浏览器退出等detached自动为true挂起回调被清除CdpSession.ts#L80-L82、onClosed()浏览器单方面断开页面关闭Connection 移除会话并发射sessiondetached连接与父会话各一次Connection.ts#L225-L236detach 后connection()仍返回同一 Connection 实例测试BiDi 后端重复/不支持场景detach()静默返回不抛错bidi/CDPSession.ts#L95-L110总结来说CDPSession.detach()在 Puppeteer 中承担的是 CDP 会话生命周期的显式终结职责它是一条经由浏览器级连接下发的Target.detachFromTarget命令配合detached属性、TargetCloseError错误族与 Connection 侧的sessiondetached事件构成了一套主动断开、被动断开、误用兜底三位一体的会话治理机制。理解了 cdp/CdpSession.ts 与 cdp/Connection.ts 中的这段实现再配合 test/src/cdp/CDPSession.test.ts 中的官方验证用例就可以在高可用的 Puppeteer 脚本与框架中放心地管理 CDP 会话资源。【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表