ARTICLE DETAIL

资讯详情

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

Chrome插件监听网络请求:webRequest与页面注入全解析

Chrome插件监听网络请求:webRequest与页面注入全解析 简介『谷歌插件学习 监听网络请求』是一套面向Web开发初学者与前端调试人员的Chrome插件学习资料核心围绕开发者工具中Network面板的实际应用帮助读者掌握查看请求列表、筛选XHR/Fetch请求、分析请求头与响应头、模拟网络条件、利用时间线定位性能瓶颈等关键操作。压缩包共10个文件包含5个JS脚本、1个HTML页面、1个JSON清单、1个TXT说明及图标等其中background.js与popup.js分别对应插件后台与弹窗逻辑manifest.json声明清单配置readme.txt提供使用要点目录功能清晰便于对照学习。资源包大小约48KB当前已有2139人学习。通过修改插件代码并配合Network面板读者可自定义追踪特定API调用频率、收集关键性能指标从而提升网络请求调试与页面优化的实战能力。1. 谷歌插件监听网络请求为什么手动翻 Network Tab 不够用做 Web 调试的人都知道 Chrome DevTools 的 Network 面板有多好用按 F12 就能看到每个请求的 URL、状态码、耗时和请求头。但真要去监听一个页面在跳转、轮询、点击按钮时发出的所有网络请求尤其是想把这些请求自动收集下来做统计或留存手动复制就完全不够用了。这个plug-feedback-collect插件资源包就是用来解决这个问题的它把 Chrome 插件监听网络请求的完整流程拆开——用background.js通过chrome.webRequest抓请求元信息再用pageScripts/main.js注入页面拦截 XHR 和 fetch 的响应体最后统一在popup.html里展示。适合想从零写一个浏览器扩展、又对网络请求这套机制不熟的开发者也适合需要长期监控某个页面 API 调用的场景。我拆完这个包后最大的感受是思路不复杂但你知道每一步在做什么坑才躲得开。2. 插件骨架拆解从 manifest.json 到三个核心文件的职责2.1 文件清单里藏着插件的基本盘把plug-feedback-collect.zip解压后第一眼先看文件结构就能猜到作者的设计意图。典型的解压目录是这样plug-feedback-collect/ ├── manifest.json ├── background.js ├── popup.html ├── popup.js ├── icon.png ├── pageScripts/ │ ├── main.js │ ├── auto.js │ └── jquery-2.0.0.min.js └── readme.txtmanifest.json是插件的身份证告诉 Chrome 这个扩展需要什么权限、后台脚本是谁、弹出窗口是谁。background.js是插件的后台中枢负责监听浏览器层面的网络请求事件。popup.html和popup.js是用户点击图标后看到的弹窗界面负责把收集到的请求展示出来。pageScripts/下的脚本属于内容脚本Content Script它们被注入到网页里用来拦截页面内发起的 AJAX 和 fetch 请求——这部分是 DevTools 本身做不到的因为网络面板只给你看结果不让你自动化收集。我用这套结构重写了一份 MV3 版本的manifest.json兼容当前主流 Chrome 版本{ manifest_version: 3, name: plug-feedback-collect, version: 1.0.0, description: 监听并收集页面网络请求, permissions: [ webRequest, storage, tabs ], host_permissions: [ all_urls ], background: { service_worker: background.js }, action: { default_popup: popup.html, default_icon: icon.png }, content_scripts: [ { matches: [all_urls], js: [pageScripts/jquery-2.0.0.min.js, pageScripts/main.js], run_at: document_start, all_frames: true } ] }代码里几个关键字段需要说明webRequest权限允许后台监听浏览器发起的网络请求storage用来把请求缓存下来防止扩展被回收后丢数据host_permissions是访问所有 URL 的前提漏了它你会发现自己只能监听到当前页面的同源请求跨域的全部静默丢失。run_at: document_start是为了保证main.js能在页面自己的脚本执行之前注入否则 fetch 和 XHR 的原型已经被页面改动你后面拦截不到。all_frames: true则是考虑页面里有 iframe 嵌套的情况很多人写的插件会漏掉 iframe 里的请求。2.2 background.js监听和转存的枢纽background.js在 MV3 里是一个 Service Worker它不像老版本的常驻后台那样一直挂内存而是按需唤醒。它的核心任务是注册chrome.webRequest事件把每次请求的关键字段抽出来存入chrome.storage.local顺便通过chrome.runtime.sendMessage推给 popup前提是 popup 正开着。下面是一段能直接跑通的基础版background.js// background.js const cacheLimit 100; // 最多缓存多少条请求 chrome.webRequest.onCompleted.addListener( (details) { const record { url: details.url, // 请求地址 method: details.method, // GET / POST / PUT ... statusCode: details.statusCode, // 200 / 404 / 500 timeStamp: details.timeStamp, // 触发时间 fromCache: details.fromCache, // 是否来自缓存 ip: details.ip, // 服务器 IP requestId: details.requestId, // 唯一 ID tabId: details.tabId // 哪个标签页发起的 }; // 先把数据存到本地popup 打开时能直接读 chrome.storage.local.get({ requests: [] }, (result) { const requests result.requests; requests.push(record); if (requests.length cacheLimit) { requests.shift(); // 超出上限就丢最老的防止 storage 膨胀 } chrome.storage.local.set({ requests: requests }); }); // 通知 popup 刷新界面如果存在 chrome.runtime.sendMessage({ type: NEW_REQUEST, data: record }).catch(() { // popup 没打开时这里会报错返回的 promise 会 reject忽略即可 }); }, { urls: [all_urls] }, [] );这段代码里onCompleted是请求成功完成后触发的事件能拿到最完整的元信息。有人会用onBeforeRequest那更适合拦截和改请求而不是做统计因为在请求还没发出时拿不到状态码和耗时。urls过滤条件写成[all_urls]表示监听所有地址你也可以改成[*://*.api.example.com/*]只关注某个域名下的请求。fromCache字段可以帮你判断页面是不是被缓存坑了。2.3 popup.html 与 popup.js唯一能给你看的界面popup 是我们的用户入口。它要做的事很简单打开时读取chrome.storage.local里的requests渲染成列表同时监听后台发来的NEW_REQUEST消息实时追加新记录。popup.html只放一个容器和统计信息!DOCTYPE html html head meta charsetutf-8 style body { width: 420px; font-family: sans-serif; } #stats { margin-bottom: 8px; color: #555; } #requestList { max-height: 400px; overflow-y: auto; } .req { padding: 6px; border-bottom: 1px solid #eee; font-size: 12px; } .req .url { word-break: break-all; } /style /head body div idstats已收集 0 条请求/div div idrequestList/div script srcpopup.js/script /body /html对应的popup.js这样写// popup.js let requests []; // 从本地存储取出历史记录 chrome.storage.local.get({ requests: [] }, (result) { requests result.requests; render(); }); // 监听后台推送的新请求 chrome.runtime.onMessage.addListener((message) { if (message.type NEW_REQUEST) { requests.push(message.data); if (requests.length 100) requests.shift(); render(); } }); function render() { const list document.getElementById(requestList); list.innerHTML requests.map((req) { const time new Date(req.timeStamp).toLocaleTimeString(); return div classreq div${req.method} ${req.url}/div div状态码 ${req.statusCode} — ${time}/div /div; }).join(); document.getElementById(stats).textContent 已收集 ${requests.length} 条请求; }这里有个容易被忽略的点chrome.runtime.sendMessage在 popup 没有打开时调用方会收到一个 rejection所以我在 background 里加了一个.catch(() {})。很多人第一次写插件在这里直接报错误以为扩展坏了其实只是小弹窗没开而已。3. 监听网络请求的两种实现webRequest API 和页面脚本注入3.1 webRequest API 能拿到什么粒度Chrome 扩展的chrome.webRequest是按浏览器底层网络栈的事件来暴露的它能观测到几乎所有请求包括图片、CSS、JS、API 调用。常用的事件有六个onBeforeRequest请求发出前、onBeforeSendHeaders请求头发送前、onSendHeaders请求头已发送、onHeadersReceived收到响应头、onResponseStarted响应体开始传输、onCompleted请求完成。在你只做监听统计时onCompleted足够。这个 API 拿不到什么拿不到响应体也就是你无法直接知道这个接口返回的 JSON 内容。它能拿到的是状态码、IP、耗时、请求头、响应头这些“外围”信息。如果你需要把接口返回的数据也收集下来用于分析就必须走页面注入那一套。另外一个细节是权限层级。在 MV3 里webRequest权限本身可以注册监听但如果你的监听回调里想要阻截请求比如把某个请求重定向到另一个 URL就必须额外声明webRequestBlocking权限。我们只做收集不需要这个权限所以manifest.json里没有写它。有人从老教程里复制代码带上 blocking 权限在 MV3 下可能触发警告甚至导致扩展无法加载注意这一点。3.2 页面脚本注入绕开 webRequest 拿不到响应体的限制要拿到响应体就需要在页面上下文中动手脚。pageScripts/main.js的作用是在页面运行之前改写window.fetch和XMLHttpRequest让它们在发请求和拿到响应时把数据“copy”一份再通过window.postMessage发给 content script最后由 content script 转发给 background。核心的注入代码大致如下// pageScripts/main.js (function () { // 只处理 http/https 请求 function shouldCapture(url) { return /^https?:\/\//.test(url); } // 拦截 fetch const originalFetch window.fetch; window.fetch function (...args) { const url typeof args[0] string ? args[0] : args[0].url; const options args[1] || {}; if (shouldCapture(url)) { const startTime performance.now(); // 等 fetch 返回 Promise 后异步复制响应 return originalFetch.apply(this, args).then(async (response) { try { const cloned response.clone(); // 克隆一份不影响页面使用 const body await cloned.text(); window.postMessage({ type: FETCH_CAPTURED, url: url, method: options.method || GET, status: response.status, body: body.slice(0, 2000) // 只截取前 2000 字符防止撑爆内存 }, *); } catch (e) { // 跨域或 body 不可读时静默失败 } return response; }); } return originalFetch.apply(this, args); }; // 拦截 XMLHttpRequest const originalXHROpen XMLHttpRequest.prototype.open; const originalXHRSend XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open function (method, url) { this.__capturedUrl url; this.__capturedMethod method; return originalXHROpen.apply(this, arguments); }; XMLHttpRequest.prototype.send function (...args) { this.addEventListener(load, function () { if (shouldCapture(this.__capturedUrl)) { try { window.postMessage({ type: XHR_CAPTURED, url: this.__capturedUrl, method: this.__capturedMethod, status: this.status, body: this.responseText.slice(0, 2000) }, *); } catch (e) { /* ignore */ } } }); return originalXHRSend.apply(this, args); }; })();这段代码里有两个关键设计。一个是response.clone()因为 fetch 的 response 只能被读取一次你不克隆的话页面自己的逻辑就读不到数据整个网页会直接挂掉。另一个是window.postMessage到*这是因为 content script 和页面脚本属于两个不同的 JS 世界只有通过消息通道中转才能通信。slice(0, 2000)是我自己惯用的截断避免存入大量图片 base64 或者超大响应体把 localStorage 撑爆。auto.js在这个包里应该承担了自动触发的职责比如定时刷新页面或者自动点击某个按钮让页面发起更多请求。典型实现是setInterval(() { document.querySelector(button).click(); }, 5000);这种配合main.js就能做持续监控。3.3 为什么 content script 和页面脚本是“两个世界”很多新手搞不懂为什么有了 content script 还要往页面里再塞一份脚本。其实 Chrome 的扩展系统中content script 虽然能操作 DOM但它运行在一个被隔离的环境里不共享页面中的 JavaScript 变量和函数。也就是说即使你在 content script 里重写了window.fetch页面上原本的fetch也不会受影响因为它们是两份拷贝。只有当代码被直接注入到页面的 main world主环境时才能改动页面真正的fetch和XMLHttpRequest。这也是pageScripts/main.js存在的意义。它不一定非要是 content script也可以在 background 里用chrome.scripting.executeScript动态注入。静态声明在manifest.json里更省事但缺点是你没法按需控制注入时机。动态注入则灵活得多比如只在你访问特定页面时才注入。4. 数据流转与弹窗渲染从 background 到 popup 的消息链路4.1 消息传递两段式和存储兜底插件里的数据流转可以拆成两条链路。第一条是页面到后台页面脚本postMessage→ content script 监听 →chrome.runtime.sendMessage发到 background。第二条是后台到界面background 收到消息后既存本地又把消息推给 popup。popup 打开后也会先主动读一次chrome.storage.local这样即使后台被休眠历史数据也还在。content script 和中转段可以用下面这段代码接住页面脚本发来的数据// pageScripts 内部或 content script 中 window.addEventListener(message, (event) { if (event.source ! window) return; // 只处理同页面消息 if (event.data.type FETCH_CAPTURED || event.data.type XHR_CAPTURED) { chrome.runtime.sendMessage({ type: PAGE_REQUEST_DATA, payload: event.data }); } });event.source ! window这个判断一定要写否则页面里其他脚本也能伪造消息污染你的数据。我见过有人漏了这个判断结果页面的广告脚本发来的消息全被当成请求记录收进来。后台收到PAGE_REQUEST_DATA后把它和webRequest拿到的元信息合并这才是最终完整的记录。一个常见做法是用url tabId作为关联键把两拨数据拼在一起。难点在于时序——webRequest 的onCompleted可能和注入脚本的load事件触发顺序不一致我一般不会硬拼而是分别存成两个数组在展示层统一按 URL 去重或合并。4.2 在 popup 里按域名分组显示请求列表如果只是一条条平铺看多了会头晕。我习惯把 popup 做成按域名分组每个域名后面显示请求数量和最近一条请求的时间。这样既能快速定位某个接口的调用情况也能一眼看出哪些请求是反复发的。// popup.js 里增加分组渲染逻辑 function groupByDomain(requests) { const map {}; requests.forEach((req) { try { const domain new URL(req.url).hostname; if (!map[domain]) map[domain] []; map[domain].push(req); } catch (e) { // URL 解析失败可能是一个被截断的地址 } }); return map; } function renderGroups(requests) { const groups groupByDomain(requests); const container document.getElementById(requestList); container.innerHTML ; Object.keys(groups).forEach((domain) { const list groups[domain]; const block document.createElement(div); block.className group; block.innerHTML div classdomain${domain} — ${list.length} 条/div; list.forEach((req) { block.innerHTML div classreq${req.method} ${req.url} (${req.statusCode})/div; }); container.appendChild(block); }); }new URL(req.url)在解析非标准 URL 时会抛异常所以我用try/catch包裹这样某个脏数据不会拖垮整个弹窗。组内的排序默认按数组顺序也就是时间顺序如果你想反过来看最新请求可以requests.slice().reverse()后再分组。4.3 数据持久化别把存储当垃圾桶chrome.storage.local的默认容量是 10MBMV3 下无上限提示但浏览器会限制存几千条请求 JSON 很轻松。但你要是把响应体完整存下来很快就爆了。我的习惯是只存 URL、方法、状态码、时间戳、响应状态响应体截断到前 1000 个字符同时给记录数设一个上限超过上限后先把最老的数据打包成 JSON 下载再清空继续存这样既保证实时监控不断又不丢数据。5. 插件排坑避雷四个让我折腾半天的细节5.1 Manifest 版本选错后台监听静默失效现象装好扩展后打开任意网站发现 popup 里一条请求都没有后台也没有报错。原因这个plug-feedback-collect包里的文件结构看上去是 MV2 时代的写法。MV2 的background是常驻页面可以直接用background.html引入脚本MV3 改成了service_worker某些 API 的使用方式有差异。如果你用 MV2 的脚本硬塞进 MV3容易碰到chrome.webRequest回调不执行或者 Service Worker 刚启动就被休眠。解决把manifest.json里的background字段改成service_worker: background.js并保证background.js里没有使用window、document等 DOM API。每条网络请求都可能唤醒 Service Worker但如果你在里面用了定时器保存数据定时器会在休眠时断掉所以一定要用chrome.storage持久化。5.2 host_permissions 漏掉跨域请求全部丢失现象能监听到页面的静态资源请求但页面调用第三方 API比如支付、统计 SDK的请求一个都看不到。原因webRequest的监听范围受host_permissions控制。如果只写了自己网站的域名那扩展只会捕获匹配到的域名请求。默认permissions里的webRequest只是授予了使用 API 的能力不决定具体监听哪些 URL。解决在manifest.json加上host_permissions: [all_urls]。如果你只想监听一部分域名就写明确域名列表比如[*://*.googleapis.com/*, *://*.myapi.com/*]。注意Host 权限申请得越多Chrome 商店审核越严格如果是自用本地加载那就无所谓。5.3 popup 打开时一片空白因为后台数据被回收现象运行几分钟后点开扩展图标列表是空的点一下刷新页面列表又恢复了。原因MV3 的 Service Worker 在空闲时会被浏览器回收内存里缓存的数据全部丢失。只有存入chrome.storage.local的数据才能在下次唤醒时恢复。解决background 里每次拿到新请求立刻通过chrome.storage.local.set写入popup 打开时先读存储再监听实时消息。存储的回调是异步的你要注意写入顺序不要靠set的返回值去判断下一个操作。我习惯把所有写入操作封装成一个函数内部用队列串行执行避免并发写导致覆盖。5.4 注入脚本时机太晚拦截 fetch 变成一句空话现象页面上有的 fetch 请求被记录到了有的是空的尤其是页面早期发起的请求一条都没有。原因run_at默认是document_idle也就是页面全部加载完才注入脚本。此时页面自身的脚本早已执行可能已经初始化了fetch或者把原生XMLHttpRequest.open缓存了你的重写根本来不及覆盖。解决把run_at改成document_start。同时要在manifest.json的 content_scripts 里把pageScripts/main.js放在最前面确保它先于页面任何业务脚本执行。另外页面如果有 Service Worker 内部发起的请求内容脚本是拦截不到的因为这些请求不经过页面 DOM 环境要覆盖这类请求只能依赖chrome.webRequest。6. 进阶技巧把收集到的请求一键导出为 JSON到了这一步你已经能实时看到请求列表了但监控的终点还是导出分析。我给 popup 加一个“导出数据”按钮把当前缓存的请求记录打包成 JSON 下发到本地方便用 Python 或者 Excel 做进一步统计。这个功能在调试接口频繁、需要留存证据的场景下很实用。首先在popup.html里加一个按钮和一段提示button idexportBtn导出 JSON/button div idexportHint stylemargin-top:6px;/div然后在popup.js里实现导出// popup.js 导出功能 document.getElementById(exportBtn).addEventListener(click, () { chrome.storage.local.get({ requests: [] }, (result) { const exportData { exportedAt: new Date().toISOString(), count: result.requests.length, requests: result.requests.map((req) ({ url: req.url, method: req.method, statusCode: req.statusCode, time: new Date(req.timeStamp).toISOString(), fromCache: req.fromCache || false })) }; const blob new Blob([JSON.stringify(exportData, null, 2)], { type: application/json }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download requests_${Date.now()}.json; a.click(); URL.revokeObjectURL(url); document.getElementById(exportHint).textContent 已导出 ${exportData.count} 条; }); });这个URL.createObjectURL用法在 MV3 的 popup 里也可以正常工作因为 popup 本身是一个页面环境。要注意的是a.click()后一定要revokeObjectURL释放内存否则大量导出会累积内存碎片。导出的 JSON 里我把timeStamp统一转换成了 ISO 格式方便排序和去重。从那以后我每次自己写收集类扩展都会先把数据格式定死再从透传、存储、导出这条链路往下排。这样做最大的好处是出问题时你知道是哪个环节丢了数据而不是对着一个黑匣子瞎猜。希望这份插件资源和这份拆解能帮到你让你在监听网络请求这件事上少走几段弯路。本文还有配套的精品资源点击获取
返回列表