尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Electron拦截网络请求并读取响应体:webRequest边界与CDP实战
调这个功能的那天晚上我把session.webRequest的文档翻了三遍onHeadersReceived的每个回调参数都打了一遍日志最后确认了一件事Electron 官方的 webRequest 接口从设计上就不会把响应体交给你。这不是版本问题也不是配置没写对是这条路径本身只走到响应头为止。后来换成webContents.debugger走 Chrome DevTools Protocol才真正把 response body 拿到了手中间又踩了 iframe 抓不到、大文件取 body 卡死、getResponseBody报No resource with given identifier found这一串坑。这篇东西就是把这几个月的实操经验摊开讲。核心围绕 Electron 环境下拦截网络请求并读取 response 内容这件事把四条可行路线的能力边界、代码写法、时序陷阱和选型逻辑讲透。适合正在做桌面端抓包工具、接口调试器、页面性能监控、或者单纯想给自己应用加一个网络日志面板的开发者。前端基础够用就能看懂但如果你连webContents和session的关系都还没理顺建议先补一下 Electron 的主进程与渲染进程模型不然下面有些坑你会踩得莫名其妙。1. 先把拦截响应拆成三层能力很多人一上来就问怎么拦截 response这句话其实混了三件完全不同的事。搞不清这三层的区别选型必然跑偏。1.1 改请求头、改响应头、读响应体是三码事第一层是干预请求发起。你可以在请求真正发出之前改 URL、改 method、加 header、甚至直接 cancel 掉。这层能力webRequest.onBeforeRequest和onBeforeSendHeaders都给得很完整想干什么是自由的。第二层是干预响应头。请求回来了服务器返回了状态码和一堆 header你可以在渲染进程看到之前把它们改掉比如强行加Access-Control-Allow-Origin绕过跨域限制或者改Content-Type让浏览器用别的方式解析。这层onHeadersReceived也能做。第三层才是读响应体。也就是真正拿到那坨 JSON、那段 HTML、那张图片的二进制内容。这一层 webRequest 完全不给。大部分人卡住的位置就在第三层。他们看到onHeadersReceived的回调里有responseHeaders、statusLine、statusCode很自然地以为再往下就能摸到 body 了——结果文档翻到底也没有。这个设计不是疏忽后面第 2 节会讲原因。顺带说个反直觉的点请求体反而是能拿到的。onBeforeRequest的 details 里有个uploadData字段POST 上去的 form data、JSON body 都在里面每一项可能是bytesBuffer、file文件路径或者blobUUID。所以你会看到一种诡异的现象——发出去的能看收回来的看不到。我第一次遇到这个反差的时候愣了半天。1.2 四条路线的能力对照在 Electron 里读响应体实际能走的路就四条我把它们的真实能力整理成表方便你直接对号入座。方案能否读响应体覆盖范围实现复杂度主要限制session.webRequest否全部网络请求含 iframe、资源低只有 header 级信息无 bodywebContents.debugger CDP是该 webContents 及其子 frame中与内置 DevTools 互斥需管时序preload 注入改写 fetch/XHR是页面 JS 主动发起的请求低拦不到 img/script/iframe易被检测自定义协议protocol.handle是仅自己注册的协议中对 https/http 无效本地调试代理是全部含下载、WebSocket高工程量大需处理 TLS表格里最后一行我放在这但没展开因为它的工程量跟前面几条不是一个量级——你要自己起 HTTP 服务、处理 CONNECT 隧道、处理 TLS 解密写完之后维护成本也高。除非你要做的是一个通用的、能抓到任何资源的抓包工具否则不建议一上来就走这条路。真正性价比最高的是第二条webContents.debugger。它借用的是 Chromium 自带的调试协议能力跟你在 DevTools 的 Network 面板里看到的基本一致能拿到 body、能拿到 timing、能拿到 initiator 调用栈。代价是它跟内置 DevTools 抢同一个调试端口这个坑后面单独讲。第三条 preload 注入看起来最简单几行代码就能 hook 住window.fetch但它的适用范围比想象中窄得多。页面里的img src、script src、CSS 里的url()、iframe 加载的整个文档这些统统不经过页面的 JS你 hook 不到。而且它还有个隐蔽的上下文隔离问题第 4 节细说。2. webRequest 的真实边界它为什么注定读不到 body先把这条最容易被误用的路讲清楚省得你在上面浪费一整天。2.1 它挂载在网络栈的哪个位置session.webRequest本质上是 Chromium 网络栈对外暴露的一个观察者接口。请求从渲染进程发起经过 URLLoader 走到网络服务这一路上 Electron 能在几个固定节点上挂回调请求发起前、发送 header 前、收到响应头后、请求完成或失败时。关键在于响应体一旦开始流转就直接被打包送进渲染进程的资源加载器了中间不会额外复制一份给 embedder。这是有意的性能取舍——一个 50MB 的视频文件如果为了让你能读到在网络层再留一份完整拷贝内存直接翻倍。所以 Electron 只把元数据级别的东西暴露出来URL、method、状态码、header、资源类型、发起它的 webContents id。我的理解是这个接口的设计目标从一开始就是让应用能做访问控制和安全策略而不是让应用能抓包。你能用它做白名单拦截、能改 UA、能注入 CORS 头这些都在元数据层面就够用了。想读内容就得换一条真正能碰到数据流的路径。2.2 onHeadersReceived 到底能改什么既然提到这把onHeadersReceived的能力边界也说清楚因为它在实际项目里用得极多。它的回调签名大致是这样session.defaultSession.webRequest.onHeadersReceived( { urls: [*://*.example.internal/*] }, (details, callback) { callback({ responseHeaders: { ...details.responseHeaders, Access-Control-Allow-Origin: [*], X-Debug-Injected: [1] } }) } )注意responseHeaders的值是字符串数组不是字符串。这是很多人第一次写会报错的地方{X-Foo: bar}是无效的必须是{X-Foo: [bar]}。多个值就放多个元素比如Set-Cookie。这个能力在本地联调时非常好用。后端没配 CORS、前端又要直连内网测试环境与其让后端改配置不如在客户端这边统一加一个头一次配置全局生效。同样的思路也能用来改Content-Security-Policy把某些被拦的资源放行——当然这只在开发环境干别带到生产。但无论你怎么改改的都是信封信封里的信纸你碰不到。details里永远不会有 body 字段。2.3 一个高频误判filter 加了 urls 就有内容我见过不止一个人这么写以为filter里的urls匹配上了回调里就能拿到内容。实际上urls只是决定哪些请求触发这个回调跟回调里有什么数据毫无关系。还有个小细节值得记一下webRequest的监听器是按 session 生效的。你给session.defaultSession注册的监听器管不到用partition创建出来的独立 session。如果你的应用里开了多个BrowserWindow且用了不同的 partition得分别注册。这个坑在多窗口应用里特别容易漏表现是主窗口能拦到弹窗里的请求一条都收不到。另外webRequest的回调是同步阻塞网络栈的。你在onBeforeRequest里做耗时操作比如同步读文件、跑一次复杂的正则匹配会直接拖慢整个请求链路。真要在这层做重活把结果缓存起来别每次都现算。3. DevTools Protocol 方案从 attach 到 getResponseBody 的完整链路这条是正路。整个流程可以概括成attach 到 webContents、开启 Network 域、监听事件配对 requestId、在请求结束时取 body。3.1 为什么必须用 debugger 而不是现成 APIElectron 里操作webContents.debugger的时候你其实是在跟一个实现了 CDP 的调试客户端对话。Chromium 内部所有跟网络相关的信息DevTools 前端能看到的都通过这个协议暴露。所以能力上限跟你在浏览器里按 F12 看到的那套完全一致。有个前提条件必须提前知道同一个 webContents 只能挂一个调试客户端。如果你的代码已经attach了用户再手动打开内置 DevTools就会抛Another debugger is already attached to the target。反过来也一样DevTools 开着的时候你 attach 会失败。实战中的处理方式有两种。一种是干脆不开放内置 DevTools用自定义的调试面板替代另一种是在 attach 之前先检测状态失败了就延迟重试并且监听detach事件做恢复。我倾向第一种因为两套调试界面共存本身就很别扭。attach 的时机也很讲究。要在页面开始加载之前就挂上去否则首屏那批请求一个都抓不到——Network.enable之前发生的请求CDP 是不会补发的。所以正确的顺序是在BrowserWindow创建之后、loadURL之前完成 attach 和 enable。const { BrowserWindow } require(electron) const win new BrowserWindow({ width: 1280, height: 800 }) const wc win.webContents // 关键先 attach再 loadURL wc.debugger.attach(1.3) await wc.debugger.sendCommand(Network.enable, { maxTotalBufferSize: 100 * 1024 * 1024, maxResourceBufferSize: 10 * 1024 * 1024, maxPostDataSize: 1024 * 1024 }) win.loadURL(https://example.internal/app)Network.enable的三个参数值得说道。maxResourceBufferSize是单个资源的缓冲上限默认值比较小大约 10MB 量级超过这个大小的响应getResponseBody就会拿不到内容。maxTotalBufferSize是所有资源加起来的总量。你把这两个值调大内存占用也会跟着上去所以别一拍脑袋填个 1GB按实际业务需求给。maxPostDataSize控制的是请求体的保留长度如果你同时要抓 POST 内容这个也得调。3.2 requestId 配对表怎么维护CDP 的事件是分散的。一个请求的生命周期里你会先后收到Network.requestWillBeSent、Network.responseReceived、Network.loadingFinished或者Network.loadingFailed它们靠requestId串起来。问题在于你真正需要的信息分散在不同事件里URL 和 method 在requestWillBeSent状态码和响应头在responseReceived而请求结束了可以取 body 了这个信号在loadingFinished。所以必须自己维护一张 Map。const pending new Map() // requestId - { url, method, status, mimeType } wc.debugger.on(message, (event, method, params) { if (method Network.requestWillBeSent) { pending.set(params.requestId, { url: params.request.url, method: params.request.method, status: 0, mimeType: }) } if (method Network.responseReceived) { const item pending.get(params.requestId) if (item) { item.status params.response.status item.mimeType params.response.mimeType item.headers params.response.headers } } if (method Network.loadingFinished) { // 这里才能取 body } if (method Network.loadingFailed) { pending.delete(params.requestId) } })这张表必须双向清理loadingFinished和loadingFailed都要删。只加不删的后果是内存持续增长长时间运行的桌面应用比如挂着跑一整天的那种会出现明显的卡顿甚至 OOM。我最早就是只在 finished 里删结果页面里的请求一旦失败网络中断、被拦截、超时那条记录就永远滞留了。还有一个隐藏的泄漏点页面导航或者刷新的时候正在进行的请求可能既不发loadingFinished也不发loadingFailed。所以还要监听Page.frameNavigated或者渲染进程的崩溃事件做一次全量清理。稳妥的做法是给每条记录加个时间戳定期扫一遍超时的条目。3.3 loadingFinished 与 getResponseBody 的时序窗口这是整个流程里最容易出错的地方。在Network.responseReceived里调getResponseBody基本一定失败因为那一刻响应体还没下载完协议层会返回No resource with given identifier found。必须等到loadingFinished。这个事件的含义是这个请求的所有数据都已经接收完毕此时缓冲区里才有完整内容。if (method Network.loadingFinished) { const item pending.get(params.requestId) pending.delete(params.requestId) // 先做过滤避免为无用资源调协议 if (!item || item.status 304 || item.status 204) return if (/^(image|font|video|audio)\//.test(item.mimeType)) return try { const { body, base64Encoded } await wc.debugger.sendCommand( Network.getResponseBody, { requestId: params.requestId } ) const content base64Encoded ? Buffer.from(body, base64) : body handleContent(item.url, content) } catch (err) { // 大文件、被取消的请求、已经失效的 requestId 都会走到这里 console.warn(getResponseBody failed:, item.url, err.message) } }注意那个try/catch不是可选的。即使到了loadingFinished仍然有几种情况会失败超过maxResourceBufferSize的大文件、请求在最后一刻被取消、以及某些特殊的响应类型。没有兜底的话一个异常就能把整个message监听器搞崩。304 Not Modified和204 No Content也要提前跳过。304 的情况特别坑——服务器返回的是空 body但 CDP 有时会认为这个资源在缓存里存在调getResponseBody会返回一个历史版本的缓存内容你把它当成这次请求的响应就错了。判断状态码直接 return 是最省事的做法。3.4 base64 与编码处理的正确姿势getResponseBody返回的body是字符串base64Encoded是布尔值。当它为true的时候body是 base64 编码的二进制内容你要自己解为false的时候body是直接可读的文本。什么时候是true主要是二进制资源——图片、字体、protobuf、gzip 压缩后没被正常处理的响应等。所以如果你的目标是抓 JSON 接口可以先判断mimeType里含不含json、text、javascript然后走纯文本分支其他情况统一按 base64 处理避免解析出乱码。还有个常见的坑是字符编码。如果服务器返回的是charsetgbk这类非 UTF-8 编码拿到的字符串可能是乱码。稳妥做法是临时把响应强制按二进制取// 先发一次强制拿 base64 原始字节 // 实际上更可靠的做法是在 responseReceived 时记录 response.headers[content-type] // 拿到 base64 后用对应 charset 解码 const raw Buffer.from(body, base64) const text iconv.decode(raw, charsetFromContentType)不过如果你只处理自家后端返回的 UTF-8 JSON这一层可以省。别过早优化。4. preload 注入方案的适用面与反噬这条路的代码量最少但坑的隐蔽程度最高。4.1 改写 fetch 和 XMLHttpRequest 的具体写法思路很直白在页面执行之前把window.fetch和XMLHttpRequest包一层请求返回时把响应体复制一份上报。// preload.js const origFetch window.fetch window.fetch async function (...args) { const res await origFetch.apply(this, args) // 必须 clone否则原始 body 会被消费掉页面拿到空响应 const copy res.clone() copy.text().then(text { window.postMessage({ __tag: net-log, url: res.url, status: res.status, body: text }, *) }).catch(() {}) return res }res.clone()这一步是必须的。Response的 body 是一次性的流读了就没了。如果你直接把res.text()拿去上报页面里的业务代码再读就是空字符串表现是接口突然返回空白排查起来极其费劲。我见过有人在这上面耗了两天。XHR 那边类似但要更小心const OrigXHR window.XMLHttpRequest function WrappedXHR() { const xhr new OrigXHR() const origOpen xhr.open let meta {} xhr.open function (method, url, ...rest) { meta { method, url } return origOpen.call(xhr, method, url, ...rest) } xhr.addEventListener(load, function () { let body if (xhr.responseType || xhr.responseType text) { body xhr.responseText } else if (xhr.responseType json) { body JSON.stringify(xhr.response) } // 上报 }) return xhr } WrappedXHR.prototype OrigXHR.prototype window.XMLHttpRequest WrappedXHRresponseType的处理是这里面最容易漏的分支。如果页面把responseType设成了arraybuffer或blob你读responseText会直接抛异常。所以要先判断类型再决定怎么取。4.2 为什么它拦不到 img、script 这类资源请求这是这条路最大的认知盲区。页面上绝大多数资源根本不是 JS 发的请求img src...由浏览器内核直接发起script src...、link relstylesheet同理CSS 里的background: url(...)也是内核处理iframe 加载整个文档走的是导航流程Service Worker 里的请求跑在独立线程跟你 preload 的 window 完全没关系这些全部绕过window.fetch。所以如果你的需求是统计页面所有网络请求preload 方案会漏掉一大半数据完全不可用。反过来说如果你的需求非常明确——我只关心页面主动调的那几个 API 接口——那 preload 就很合适代码量少、不需要处理 CDP 的时序问题、也不用担心跟 DevTools 冲突。4.3 上下文隔离下 window 到底是谁的这一节是硬核踩坑。Electron 从 12 开始默认开启contextIsolation: true意味着 preload 脚本运行在一个独立的 JavaScript 上下文里它看到的window跟页面主世界的window不是同一个对象。结果就是你在 preload 里写window.fetch ...改的是隔离世界的fetch页面里那套完全不受影响。代码不报错但一条日志都没有。第一次遇到这个的人通常会怀疑人生。绕过去的方式有几种一种是用webFrame.executeJavaScript在页面主世界执行补丁代码配合contextBridge暴露一个回传通道// preload.js const { contextBridge, webFrame } require(electron) contextBridge.exposeInMainWorld(__netBridge, { report: (payload) { /* 走 ipcRenderer 发给主进程 */ } }) webFrame.executeJavaScript( const origFetch window.fetch; window.fetch async function(...args) { const res await origFetch.apply(this, args); const copy res.clone(); copy.text().then(t { window.__netBridge.report({ url: res.url, status: res.status, body: t }); }).catch(() {}); return res; }; )另一个办法是直接关掉contextIsolation。我强烈不建议——这是安全边界关掉之后页面里的恶意脚本能直接摸到 Node 的require风险不在一个量级上。还有一点要提醒无论哪种注入方式页面是可以检测到你的。window.fetch.toString()如果返回的不是[native code]稍微有点心的页面脚本就能发现被劫持了。做内部工具无所谓做通用产品得考虑这个。5. 子框架、重定向、流式响应三个最容易翻车的场景前面讲的是主干流程真正让项目卡住的是这几个边缘情况。5.1 iframe 内的请求为什么默认抓不到webContents.debugger默认只 attach 到主 frame。页面里嵌了 iframe尤其是跨域的 iframe比如内嵌的第三方支付、地图、客服组件它们各自是独立的 target请求事件不会冒泡到你主 frame 的监听器上。解决办法是开启自动 attachawait wc.debugger.sendCommand(Target.setAutoAttach, { autoAttach: true, waitForDebuggerOnStart: true, flatten: true })flatten: true是重点。开启之后子 target 的事件会以扁平化的方式复用同一个message事件但多出一个sessionId参数你必须把它原样带回去才能操作对应的子 target。wc.debugger.on(message, (event, method, params, sessionId) { // sessionId 为空表示主 frame有值表示子 frame if (method Network.loadingFinished) { wc.debugger.sendCommand( Network.getResponseBody, { requestId: params.requestId }, sessionId // 必须传否则找不到这个请求 ) } })不传sessionId的表现是getResponseBody报No resource with given identifier found但你在responseReceived里明明看到请求了。这个矛盾现象就是 sessionId 丢失的典型症状。还有个副作用得提前知道waitForDebuggerOnStart: true会让子 frame 在启动时暂停等你发Runtime.runIfWaitingForDebugger才继续。如果你的代码忘了发这个命令整个 iframe 就卡住不加载了表现是页面白屏但主文档正常。这是个很容易让人抓狂的 bug因为控制台里一条错误都没有。5.2 重定向链里 requestId 的变化规律一个请求如果经历了 301/302 跳转CDP 里会产生多个 requestId而不是复用同一个。具体规律是第一次requestWillBeSent拿到 id A请求返回 302接着你会收到一个新的requestWillBeSentid 是 B它的params.redirectResponse字段里装着那次的 302 响应。然后再来 B 的responseReceived和loadingFinished。这意味着你在配对表里要做好几件事。首先A 这个 id 永远不会收到loadingFinished它会被永久滞留——必须靠有redirectResponse就说明上一跳已经结束这个规则把它清掉。其次如果你关心的是最终响应就只处理 B如果关心整条链路就得把 A 的redirectResponse也记下来。另外一个细节重定向的中间响应体通常是拿不到的302 一般也没 body所以别指望能读到跳转页面的内容。5.3 EventSource 与分块传输的 body 获取EventSourceSSE和分块传输chunked的场景比较特殊。SSE 的请求永远不会发loadingFinished——连接一直开着除非服务端主动关闭。所以如果你只监听loadingFinished这些请求一条都抓不到。CDP 提供了专门的事件来处理if (method Network.eventSourceMessageReceived) { // params.data 就是单条 SSE 消息 // params.eventName, params.eventId 对应事件名和 id }WebSocket 也是类似的道理用Network.webSocketCreated、Network.webSocketFrameSent、Network.webSocketFrameReceived这三个事件。注意 WebSocket 帧的payloadData在某些情况下是 base64 编码的opcode为 2 的二进制帧要判断后再处理。至于普通的 chunked 响应loadingFinished是会正常触发的getResponseBody拿到的是拼接完成的完整内容这个不用特殊处理。但如果你的场景是要实时看到流式输出的每一个片段比如接了 AI 接口做逐字打印的界面getResponseBody就不行了——它只在结束后给你完整结果。这时候Network.dataReceived事件里的dataLength只能告诉你收到了多少字节拿不到内容本身。真要抓流式内容还是得回落到 preload 层 hook fetch 的response.body流。6. 选型与落地不同场景该走哪条路讲完机制最后落到你手上这个需求到底该选哪个。6.1 抓包分析类工具只能上 debugger如果你的产品定位是给页面做网络分析比如做一个类似 DevTools Network 面板的界面需要展示 URL、状态码、耗时、响应体、请求头那没有第二个选择必须用webContents.debugger。这种情况下要做好几件事一是缓冲区大小按业务调别用默认值二是过滤规则前置静态资源图片、字体、媒体在responseReceived阶段就根据mimeType打标记loadingFinished时直接跳过省掉getResponseBody的协议往返——这个调用本身是有开销的几百个资源累积起来能明显感觉到卡顿三是 UI 侧的渲染要做虚拟列表别把几百条记录全部铺到 DOM 上。6.2 接口 Mock 与联调webRequest 更省事如果你的目标是把某个接口的响应换成本地 mock 数据很多人第一反应是用 debugger 拦截然后改写——这是一条弯路。正确姿势是protocol.handle配合自定义协议或者干脆用一个本地 HTTP 服务把session.setProxy指过去。前者适合你自己的应用内部通信后者适合调试第三方页面。用 CDP 拿 body 再改写中间隔了一层时序也更难控。顺带说一句如果只是改响应头比如加 CORS、改缓存策略onHeadersReceived足够了别动 body。6.3 性能与埋点监控关注的是时序不是内容做页面性能监控的团队往往不需要响应体需要的是每个请求的精确时间戳DNS 查询多久、TCP 握手多久、TTFB 多久、下载耗时多久。这些数据在Network.responseReceived的params.response.timing里全都有而且精度到微秒。requestWillBeSent的params.timestamp也给了单调时钟值可以直接用来算时间差。这条路压根不需要getResponseBody所以性能开销小得多Network.enable的缓冲区也可以调小。很多团队在这上面走了弯路为了顺便也存一下 body 方便排查把缓冲区开到很大结果监控本身的性能开销成了问题。6.4 一份可直接抄的最小可用实现最后给一份完整的、能直接跑的骨架包含 attach、事件分发、配对清理、body 提取和错误兜底。const { app, BrowserWindow, ipcMain } require(electron) const pending new Map() const TIMEOUT_MS 60 * 1000 function shouldCapture(item) { if (!item) return false if (item.status 204 || item.status 304) return false if (/^(image|font|video|audio)\//.test(item.mimeType)) return false return true } async function attachMonitor(wc) { if (wc.debugger.isAttached()) return try { wc.debugger.attach(1.3) } catch (e) { console.error(attach failed:, e.message) return } await wc.debugger.sendCommand(Network.enable, { maxTotalBufferSize: 100 * 1024 * 1024, maxResourceBufferSize: 10 * 1024 * 1024 }) await wc.debugger.sendCommand(Target.setAutoAttach, { autoAttach: true, waitForDebuggerOnStart: false, flatten: true }) wc.debugger.on(message, async (event, method, params, sessionId) { if (method Network.requestWillBeSent) { if (params.redirectResponse) { // 上一跳结束清掉旧记录 pending.delete(params.requestId) } pending.set(params.requestId, { url: params.request.url, method: params.request.method, status: 0, mimeType: , ts: Date.now() }) return } if (method Network.responseReceived) { const item pending.get(params.requestId) if (item) { item.status params.response.status item.mimeType params.response.mimeType || } return } if (method Network.loadingFailed) { pending.delete(params.requestId) return } if (method Network.loadingFinished) { const item pending.get(params.requestId) pending.delete(params.requestId) if (!shouldCapture(item)) return try { const res await wc.debugger.sendCommand( Network.getResponseBody, { requestId: params.requestId }, sessionId ) const content res.base64Encoded ? Buffer.from(res.body, base64) : res.body wc.send(net-captured, { url: item.url, method: item.method, status: item.status, mimeType: item.mimeType, size: Buffer.byteLength(content), body: res.base64Encoded ? null : content }) } catch (err) { // 静默失败常见于大文件和已失效的 requestId } } }) wc.debugger.on(detach, (event, reason) { pending.clear() console.warn(debugger detached:, reason) }) // 定期清理僵尸条目 setInterval(() { const now Date.now() for (const [id, item] of pending) { if (now - item.ts TIMEOUT_MS) pending.delete(id) } }, 30000) } app.whenReady().then(() { const win new BrowserWindow({ width: 1280, height: 800, webPreferences: { preload: require(path).join(__dirname, preload.js), contextIsolation: true } }) attachMonitor(win.webContents).then(() { win.loadURL(https://example.internal/app) }) })几个细节值得单独强调。Target.setAutoAttach里我把waitForDebuggerOnStart设成了false意思是子 frame 不用等我放行直接跑。这样避免了忘记发Runtime.runIfWaitingForDebugger导致白屏的问题代价是子 frame 极早期的请求可能漏抓一两条。对大多数场景这个取舍是划算的。shouldCapture的过滤放在loadingFinished而不是responseReceived是因为在responseReceived阶段我还不知道状态码是不是 304——虽然那时其实已经有status了但把判断集中在一个地方更好维护。如果你追求极致性能可以挪到responseReceived阶段打个标记loadingFinished直接看标记。还有一个我没写进去但你应该考虑的wc.send到渲染进程的数据如果是大 bodyIPC 序列化会成为瓶颈。超过几 MB 的内容建议落盘只把路径传过去渲染进程按需读取。最后分享一点实际操作中的体会。这套东西刚跑通的时候我特别得意觉得什么都能抓了。结果接了一个内嵌第三方组件的页面发现那个 iframe 里的请求一条都收不到——就是我前面说的Target.setAutoAttach没开。开了之后又遇到白屏因为waitForDebuggerOnStart没配对runIfWaitingForDebugger。这类问题在文档里都是轻描淡写一句话真正踩上去一个比一个耗时间。我现在做这类功能的习惯是先把抓不到当成默认预期一个个排除attach 成功了吗Network.enable在导航之前发了吗配对表里这条记录清掉了没有sessionId传对了吗状态码是不是 304按这个顺序走一遍八成的问题都能定位到。比漫无目的地翻文档高效得多。
RELATED

相关推荐

Word 2010表格样式调整全攻略与专业美化技巧

Word 2010表格样式调整全攻略与专业美化技巧

1. 项目概述:Word 2010表格样式调整的核心价值十年前我刚入行做文秘的时候,最头疼的就是领导扔过来一份满是表格的文档要求美化。那时候的Word 2010虽然功能强大,但表格样式调整的入口就像捉迷藏——你知道它肯定在某个地方,但就是…

📅 2026/11/2 5:43:09
shp文件地图显示工具:从数据体检、坐标纠偏到网页渲染

shp文件地图显示工具:从数据体检、坐标纠偏到网页渲染

1. 一个 shp 文件其实是六个文件:先把数据底子摸清拿到一个压缩包,解压出来一堆同名不同后缀的文件,很多人第一反应是"怎么这么多"。这不是打包的人手抖,而是shp 文件地图显示工具这条链路里最常见的起点问题&#xff1…

📅 2026/11/2 5:38:09
算法基础与核心特性解析:从概念到实践应用

算法基础与核心特性解析:从概念到实践应用

1. 算法基础概念解析算法是计算机科学中最基础也最重要的概念之一。简单来说,算法就是解决问题的一系列明确指令。就像烹饪食谱一样,它详细说明了从原材料到成品的每一个步骤。但不同于普通食谱,算法必须满足几个关键特性:确定性&…

📅 2026/11/2 5:38:09
MORE NEWS

更多资讯

📰

从R脚本复现GEFCOM14概率负荷预测:分位数回归与QRA集成

简介:面向能源负荷预测学习者与R语言实践者的GEFCOM2014-EPFL资料包,围绕多地区小时级电力负荷数据展开,适合希望用统计模型和机器学习完成时间序列预测练习的中级读者。资料可用于理解负荷数据中的日期、时间、地理位置与能耗量结构&#xf…

📰

基于傅里叶描述子与SVM的Python手势识别源码实践

简介:围绕傅里叶算子实现手势识别的完整Python源码包,面向计算机视觉与机器学习入门到中级学习者,覆盖从图像采集、平滑、OTSU肤色分割、八邻域轮廓检测,到傅里叶描述子与椭圆傅里叶描述子提取、归一化,再用KNN和SVM训…

📰

HyperDbg 单机内核调试:VMM 加载、EPT Hook 与事件排错

简介:这份 ZIP 资料围绕 Ring0 级内核调试器 Hyperdbg 展开,面向具备操作系统与驱动开发基础、希望进入内核调试与系统分析领域的学习者和开发者。它针对内核级事件跟踪、断点设置、内存与寄存器查看等调试需求,提供一套可供研读和编译的源码…

📰

傅里叶描述子与SVM驱动的低算力实时手势识别实践

简介:这份代码包围绕Python手势识别任务,给出从图像预处理到分类识别、界面展示的完整实现,适合图像处理与机器学习课程设计、毕业设计及算法实践者参考。流程覆盖图像平滑、基于OTSU阈值的肤色分割、八邻域搜索轮廓检测,并提取傅…

📰

基于EPT的HyperDbg内核隐形调试与驱动排障实践

简介:面向操作系统内核、驱动开发与恶意软件分析领域的中高级学习者,Hyperdbg 是一套 ring0 级内核调试器源码资料,可用于理解内核事件跟踪、断点设置、内存查看修改及寄存器监控等调试机制,并对照源码研究宿主/客户机协作、VMX 虚…

📰

MFC+WinPcap网络嗅探器:从抓包到协议解析的实战指南

简介:这是一份面向网络编程初学者与协议分析爱好者的原创MFCWinpCap网络嗅探器工程源码,基于VS的MFC框架搭建界面,借助WinPcap抓包驱动实现数据包捕获与解析,可解决学习网络协议、观察真实流量时缺少可视化工具的问题。压缩包共22…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬