2026/10/15 8:47:06

基于CDP的浏览器消息采集与Control UI实时监控方案

基于CDP的浏览器消息采集与Control UI实时监控方案 我们在做桌面工具的时候经常遇到一个很尴尬的场景页面跑到一半某个请求卡住了控制台报了一堆错但用户那边只看到白屏。你想在自研的 Control UI 面板里把这些“浏览器内部消息”捞出来实时展示却发现不知道从哪里下手。这篇文章就来聊聊怎么把浏览器内部产生的消息——网络请求、控制台日志、页面生命周期事件、业务自定义事件——完整地采集下来再以可控的方式转发到你自己的控制面板里。先说明一下这里的 Control UI 不是一个特定开源项目而是指你自研的控制端界面统称 Control UI。这套“获取浏览器消息的流程”适合三类人做自动化测试平台的人想做浏览器实时监控/调试工具的人以及需要在桌面端嵌套浏览器但想看内部状态的开发者。我会尽量把链路拆开讲清楚再给出一套可以直接抄的落地代码。1. 先分清要抓哪些“浏览器消息”消息来源与场景拆解1.1 “消息”并不只是 window.postMessage 那一种很多人一听到“浏览器消息”就以为是页面 JS 里用 postMessage 发出去的那点东西。这格局小了。你要在 Control UI 里还原浏览器现场至少会碰到三类消息第一类是网络层消息。包括 HTTP 请求的 URL、请求头、请求体、响应状态码、耗时、以及 WebSocket 的帧内容。这类消息对于排查接口超时、鉴权失败、数据异常至关重要。第二类是页面运行时消息。包括 console.log/warn/error、未捕获异常、Promise rejection、DOM 事件、路由切换、以及业务代码自定义的全局事件。这类消息直接反映了页面逻辑的执行情况。第三类是浏览器内核级消息。页面加载状态、资源加载失败、断网离线、Service Worker 状态、权限请求、Performance 指标等。这类消息通常不会主动暴露给页面 JS需要从浏览器调试协议层去拿。这三类消息合在一起才构成完整的浏览器现场。只抓其中一类Control UI 的价值就大打折扣。1.2 为什么不能全靠在业务代码里“埋点”你可能想说那我直接在业务工程里写一个全局事件上报函数不就完了问题是埋点代码是侵入式的。你得让业务团队配合你改代码改动量一大就会遇到阻力而且线上代码和调试代码混在一起这是很多人接受不了的。埋点会遗漏“浏览器自己的消息”。页面 JS 崩溃之前、网络栈内部的失败细节、渲染进程的异常这些都是页面代码看不到的。业务埋点能拿到页面层的部分消息但拿不到浏览器层的原生消息。埋点本身有滞后性。如果页面白屏发生在早期代码执行阶段埋点代码甚至还没加载完消息就已经丢了。所以正确思路是把采集逻辑放在页面之外通过浏览器调试协议这类官方通道去“旁路”读取消息然后在 Control UI 这一侧统一处理。这样业务代码零侵入页面层、网络层、内核层的消息都能覆盖到。1.3 Control UI 的特殊需求不只是“看”还要“控”单纯把消息展示出来还不够。Control UI 的本质是一个控制端它除了被动接收消息往往还需要主动下发指令强制刷新页面、修改请求头、注入一段脚本、终止某个请求、切换路由。这就意味着消息通道必须是双向的而不是单向的“浏览器→UI”。这个双向需求直接决定了方案选型。很多人在第一步就走错了方向选了一个只能“读”的方案后面想加“控”的功能就得推翻重来。我下面要讲的这套架构从一开始就是为“采集 控制”双通道设计的。2. 方案选型从注入脚本到 CDP 到自动化框架封装2.1 常见的四条技术路线我实际调研并试过四条路线简单说下各自的优缺点方案原理优点缺点自建 WebSocket 页面注入脚本往页面里注入采集脚本通过 WebSocket 上报消息简单直接、能拿到业务上下文侵入性强、CSP 可能挡注入脚本、拿不到内核级消息浏览器扩展利用扩展 API 和背景页监听事件能拿到较完整信息、有成熟的 API开发和分发成本高、webstore 审核麻烦、多浏览器兼容要额外适配CDP 原生协议通过 WebSocket 连接 Chromium 调试端口订阅事件官方能力、全面、可控性强协议细节多、字段复杂、要自己处理事件流和状态管理自动化框架封装Playwright/Puppeteer 类在 CDP 之上封装事件监听和页面控制开发效率高、API 友好、生态成熟部分底层事件被封装过滤二开时需要对框架原理有一定了解我的结论是从零攒一个完整系统直接用 CDP 会痛苦完全依赖框架封装又会失去底层灵活性。最合理的做法是用自动化框架去“拉起和管理浏览器”在需要监听底层事件的时候通过框架暴露的底层协议去订阅消息两侧结合。2.2 我选型的核心考量我这里说的自动化框架泛指基于 CDP 的上层工具不特指某个产品。选这套组合的原因有三点第一CDP 事件的覆盖范围最全。从 Network.requestWillBeSent 到 Network.responseReceived到 Console.messageAdded到 Page.frameNavigated再到 Runtime.exceptionThrown浏览器自己产生的每一条关键消息都有对应事件。只要订阅事件就能拿到第一手数据。第二自动化框架解决的“管理浏览器”问题是真正的体力活。自己去下载浏览器、启动进程、处理崩溃重连、管理多标签页这些工作量大且没有创造性。框架把这些封装好了启动浏览器只需几行配置。第三这套方案的升级路径清晰。今天做的是“消息采集面板”明天想加自动化测试的录制回放底层事件数据可以直接复用。你不会因为选错了技术底座而推倒重来。2.3 统一消息格式把三类消息装进同一个信封三类消息来源不同但最终送到 Control UI 的格式必须统一。我在实际项目中采用的统一事件格式如下字段含义eventType 是消息类型名source 是消息来源network、console、runtime、page、systempayload 是具体内容对象timestamp 是采集端的接收时间sequence 是全局自增序号。sequence 这个字段特别重要后面排查消息乱序和丢消息全靠它。我专门用了一个自增序号而不是直接用时间戳。原因是时间戳在毫秒级别可能重复且不同消息在 UI 上排序时会出现先后颠倒自增序号则可以保证消息排列顺序与采集顺序完全一致。2.4 整体链路架构这套链路一共四层采集层浏览器自动化和 CDP 协议订阅浏览器产生的事件并主动发送控制命令。协议层把浏览器原生事件转换统一消息格式负责字段映射、数据清洗、消息过滤。这一层是整个系统的翻译官。消息总线负责消息的存储、排序、分发、订阅通知。它是连接采集层和 UI 层的中点。视图层Control UI 前端接收消息推送展示消息列表、过滤、详情并下发控制指令。这个架构最大的好处是每一层都可以单独替换。比如采集层从本机浏览器换成远程浏览器消息总线的存储从内存换成消息队列视图层从 Web 面板换成桌面应用其他层不受影响。3. 从零搭建消息采集到 Control UI 展示的完整实操3.1 环境准备我这里用 Node.js 做示例因为生态里对 CDP 的处理最顺滑。你需要准备Node.js 环境、一个支持 CDP 的 Chromium 内核浏览器、自动化框架库我这里以通用 CDP 客户端为例实际上 Playwright 这类封装也要注意版本匹配Chromium 版本和 CDP 版本需要一致否则会出现事件字段对不上的问题。为了方便演示这里的示例代码不接完整框架而是直接用浏览器调试端口。这个方式能让你看到 CDP 最本质的链路调试端口起来之后一切消息都是通过 WebSocket 进出的。3.2 启动带调试端口的浏览器并建立连接启动浏览器时加 --remote-debugging-port 参数指定一个空闲端口比如 9222。这样浏览器会启动一个本地调试服务它的根路径 /json/version 会返回浏览器整体的 WebSocket 调试地址/json/list 会返回每个标签页的调试地址。拿到标签页的 webSocketDebuggerUrl 之后用 WebSocket 客户端连上去就建立了一条和浏览器对话的专用通道。连接建立后你需要发送一条 activate 意图的消息来启用某个事件域比如启用网络域才能收到网络事件。之所以要“启用”才收事件是因为浏览器不会默认把所有内部事件都推给你。每个事件域都对应一组相关事件启用网络域后就订阅了请求发起、响应接收、失败中断等网络类消息启用运行时域后才能监听到页面执行异常。3.3 订阅浏览器原生事件并转成统一消息格式下面是实际可运行的最小代码核心逻辑就是三件事启动浏览器、订阅事件、格式化转发。const http require(http); const WebSocket require(ws); // 1. 获取标签页调试地址 function getTargetUrl() { return new Promise((resolve, reject) { http.get(http://127.0.0.1:9222/json/list, (res) { let data ; res.on(data, chunk data chunk); res.on(end, () { const targets JSON.parse(data); const page targets.find(t t.type page); resolve(page.webSocketDebuggerUrl); }); }).on(error, reject); }); } // 2. 统一消息格式 let seq 0; function formatMessage(source, eventType, payload) { return JSON.stringify({ source, eventType, payload, timestamp: Date.now(), sequence: seq }); } async function main() { const wsUrl await getTargetUrl(); const ws new WebSocket(wsUrl, { maxPayload: 64 * 1024 * 1024 }); ws.on(open, () { // 启用网络、控制台、运行时、页面事件域 [Network, Console, Runtime, Page].forEach(domain { ws.send(JSON.stringify({ id: Math.floor(Math.random() * 10000), method: ${domain}.enable, params: {} })); }); }); ws.on(message, (data) { const msg JSON.parse(data); if (msg.method) { // 把 CDP 原生事件转成统一消息格式 const source msg.method.split(.)[0]; const eventType msg.method; const payload msg.params || {}; console.log(formatMessage(source, eventType, payload)); } }); } main();代码里的关键细节是启用事件域这一段。如果你不启用某个域那么该域的事件永远不会到达而且浏览器不会主动提醒你。很多人第一步就把网络域忘了启用导致面板上永远看不到请求消息。按上面的代码四个域全部启用即可覆盖绝大多数调试场景。3.4 在 Control UI 侧推送消息和下发指令采集服务拿到消息之后需要通过一个通道推给前端。最简单的实现是 WebSocket 服务端采集服务同时充当消息转发器一端连着浏览器一端连着前端面板。const WebSocketServer require(ws).WebSocketServer; // 建立前端连接池 const wss new WebSocketServer({ port: 9300 }); const clients new Set(); wss.on(connection, (client) { clients.add(client); client.on(close, () clients.delete(client)); }); // 浏览器消息到来时转发给所有Control UI前端 function broadcast(message) { for (const client of clients) { if (client.readyState WebSocket.OPEN) { client.send(message); } } } // 在 ws.on(message) 里追加一行 // broadcast(formatMessage(...));这里注意一点转发时要处理慢消费者问题。如果前端面板刷新慢消息就会被阻塞在 TCP 缓冲区里。实际项目中可以给每个客户端加一个发送队列并且主动丢弃落后的旧消息保证控制面板始终展示最新状态。“控”的方向同样走这条 WebSocket前端向服务端发一条指令服务端把指令转成 CDP 方法消息发给浏览器。比如前端点击“刷新页面”服务端发送 Page.reload前端想执行一段脚本服务端发送 Runtime.evaluate。3.5 前端面板设计的几个核心点Control UI 的界面不用花哨但要解决几个实际问题消息列表必须能扛住高频刷新。直接渲染成千上万条 DOM 节点会卡死页面我用的是虚拟滚动方案只渲染可视区域内的消息这一点实际项目里是刚需。过滤条件要快。按来源过滤、按类型过滤、按关键字过滤这三个过滤条件缺一不可。消息多了以后没有过滤条件的面板就是一堆噪音。详情看板要分层。列表里只展示摘要信息时间、来源、事件名点开之后在侧边栏展示完整 payload。这样既保持列表清爽又能看细节。时序关系要清晰。同一时间可能发生多个事件比如网络请求失败和页面异常几乎同时发生。面板上要有统一的时序轴配合 sequence 自增序号排序避免时间戳相同导致乱序。3.6 从采集到展示的调试方法整套系统跑起来之后你可能发现面板上没消息。不要慌先按三个层次排查先确认浏览器侧有没有产生事件。手动在页面里操作一下看一下采集进程的终端日志里有没有打印出消息。如果采集进程有输出而面板没有问题在采集服务到前端的转发链路。如果采集进程没有输出检查 WebSocket 是否连上了、事件域是否完成了启用。用调试端口的时候最好在浏览器里打开 about:blank 页面避免页面的初始加载事件漏掉。如果事件域启用了但某些事件还是没有八成是 CDP 版本问题。不同 Chromium 版本的事件字段并不完全一致有些事件被废弃、被拆分需要在协议层做数据清洗兼容。4. 实战中的问题清单与排查技巧实录4.1 高频消息让事件循环卡死实际运行中最容易踩的坑是页面加载一瞬间会同时产生大量网络和运行时事件如果你每来一条消息就同步转发一次Node.js 的事件循环会被打爆Control UI 也会跟着疯狂渲染。我的解法是在采集服务里做“微批量”聚合。把消息先推进缓存队列每 100 毫秒批量打包转发一次或者累计到 500 条就触发一次转发。这样既保证了实时性又避免了高频抖动。4.2 消息时序错乱时间戳骗了你有一次排查半天发现 A 请求明明比 B 请求先发起面板上显示的却是反的。后来发现如果只是用浏览器事件里的时间去排序某些事件的时间戳是发送时间、某些是接收时间本身就存在偏差。我的做法是采集端本地打点时间戳 自增序号。排序时以自增序号为准时间戳只作为展示字段。前端对消息进行唯一性去重时也以 (source eventType sequence) 作为联合标识而不是用时间戳。4.3 跨域页面拿不到接口细节CORS 会挡掉页面里的接口数据这是正常的浏览器安全行为。用 CDP 监听网络事件时请求和响应的完整数据是可以拿到的但有一些跨域场景下响应体默认不展开需要额外发送 Network.enable 并开启特定参数。如果页面设置了严格 CSP直接注入脚本会被拒绝需要在调试模式下给浏览器启动参数加上禁 CSP 的开关。这里要强调禁 CSP 只限本地调试环境不要用到生产环境否则等于把安全边界拆了。4.4 WebSocket 帧数据解析如果你要采集页面里 WebSocket 的消息内容CDP 的 Network.webSocketFrameReceived 事件只能拿到帧的 payload 文本拿不到二进制帧的完整内容。如果需要分析二进制帧就得在采集层做 base64 编码转换不然控制面板显示的就是乱码。4.5 浏览器升级导致事件字段失效我遇到过最典型的“兼容问题”某版本浏览器升级后Network.requestWillBeSent 事件里的 requestId 字段变化规律变了之前能正常合并的请求上下文全部错乱。解决办法是给协议层加一个“事件适配器”每个浏览器版本对应一份字段映射表。虽然维护成本高但这是长期稳定运行的必经之路。常见问题根因快速排查最终解法面板无消息事件域未启用或 WebSocket 断开查看采集进程日志检查 enable 和连接状态消息乱序同毫秒时间戳重叠去除时间排序改用序号增加自增序号请求上下文错乱浏览器升级字段变化比较版本差异加事件适配器前端卡顿消息渲染没有虚拟化查看 DOM 数量虚拟滚动 微批量响应体拿不到未开启完整网络数据检查发送参数开启对应参数5. 这套流程还能扩展成什么5.1 自动化回归测试的实时反馈台把消息采集和控制面板结合起来就形了一个天然的测试反馈台。测试用例跑起来的时候你可以在面板上看到每一步触发了哪些网络请求、页面有没有报错、路由是否按预期跳转。与传统测试报告相比这种实时反馈大大缩短了定位问题的时间。5.2 异常监控与数据上报线上环境不能开调试端口但内网测试环境可以。把采集到的 console.error、failed 请求、未捕获异常汇总起来按 session 为单位落库再接入告警系统内网环境的页面质量情况就会一目了然。这个场景下Control UI 变成了一个监控中心。5.3 多人协作调试与远程下发如果你想远程调试可以把采集服务的 WebSocket 转发层拆出来单独部署到一台内网机器上浏览器端、采集端、控制端不在一台设备上也没关系。多人同时接入时各自的 Control UI 面板都能看到同一份消息流这就实现了调试现场的共享。5.4 低代码录制回放平台这套流程最让我看好的扩展方向是低代码录制回放。采集层记录了完整的网络事件和运行时事件再加上 Control UI 下发的指令流相当于把一次人工操作完整地录了下来。回放时重放指令、比对消息序列就能实现自动化回归。这个方向价值很大因为它不要求测试人员写脚本只要会操作页面就够了。最后想多说一句Control UI 获取浏览器消息这套流程本质上不是在“偷看”浏览器而是在“接它的内部总线”。浏览器的设计本来就允许外部通过调试协议来观察和控制关键是要把这些原始事件组织成一条有序、可过滤、可回溯的消息流。我自己在初始版本里踩了不少坑尤其是消息乱序和丢失的问题一度被折磨到怀疑人生。后来把“统一消息格式 递增序号 事件适配器”这三板斧立起来整个系统才真正稳定下来。如果你也在搭类似的工具建议别一上来就堆功能先把链路打通、把消息字段设计好后面加什么扩展都是水到渠成的事。