☰
Chrome插件监听网络请求实战:background.js捕获真实流量
2026/10/7 22:56:08 网站建设 项目流程

简介:本资源是一份面向Web前端开发者与调试初学者的Chrome插件开发实践资料,聚焦网络请求监听这一核心调试能力,解决异步请求(XHR/Fetch)捕获、分析与自动化收集的实际问题。压缩包共10个文件,含5个JavaScript文件(含background.js、popup.js、main.js等核心逻辑)、1个HTML页面(popup.html)、1个JSON清单文件(manifest.json)、1个PNG图标及1个README说明文档,总大小仅48KB,轻量易部署,适合快速上手插件结构与网络事件监听机制。已有2139人学习下载,体现了开发者对浏览器调试工具进阶应用的持续关注。读者可直接复用插件框架,理解popup与background通信、chrome.webRequest API监听原理,并掌握如何结构化采集请求URL、方法、响应状态等关键字段,为性能监控、API行为审计或教学演示提供可定制的技术原型。

1. 谷歌插件学习监听网络请求:不是调开发者工具,而是用 background.js 主动捕获真实请求流

你有没有试过在 Chrome 开发者工具的 Network 面板里反复刷新、筛选、点开每个 XHR 查响应体,结果发现——某个关键接口明明前端代码里写了 fetch,Network 却压根没记录?或者接口被 Service Worker 拦截后绕过 Network 面板,调试直接失明?这时候光靠「打开 F12 → 切到 Network → 点刷新」这套操作,已经不够用了。真正的「监听网络请求」,是让插件自己变成浏览器底层的“流量守门人”,在 request 发出前、response 返回后,甚至在 fetch/XHR 构造阶段就介入——这正是plug-feedback-collect.zip这个实战包的价值所在。它不教你怎么点按钮,而是给你一套可复现、可调试、可嵌入业务逻辑的插件级监听方案:用background.js基于chrome.webRequestAPI 捕获原始请求,用popup.html + popup.js实时展示过滤后的关键字段(URL、method、status、耗时),再通过pageScripts/main.js在页面上下文注入钩子,补全 fetch/XHR 的 body 和 headers。这不是玩具 demo,而是我在某电商大促监控项目里落地过的最小可行架构——能稳定抓取加密接口的原始 payload,能区分重定向链中的真实目标 URL,还能把请求日志导出为 JSON 供后续分析。适合正在做前端性能埋点、API 合规审计、或需要绕过 Network 面板盲区的中高级前端/测试/运维工程师。


2. 从 manifest.json 到 background.js:插件监听能力的底层契约与事件注册逻辑

2.1 manifest.json 的权限声明:为什么必须包含 "webRequest" 和 "webRequestBlocking"

manifest.json是整个插件的宪法,它的权限配置直接决定你能监听什么、能改什么。打开plug-feedback-collect/manifest.json,你会看到这两行关键声明:

"permissions": [ "activeTab", "storage", "webRequest", "webRequestBlocking", "https://*/*", "http://*/*" ],

注意:webRequestBlocking权限不是可选的——没有它,你只能「观察」请求(onBeforeRequest仅触发,无法修改),而无法实现「拦截+重放」「动态改 header」「阻断恶意请求」等高阶动作。但代价是:Chrome 会强制要求用户在安装时看到「此扩展可读取和更改您访问的网站的数据」的红色警告。这是安全机制,不是 bug。

"https://*/*"和"http://*/*"是 host permissions,它们告诉浏览器:“我需要监听所有网站的请求”。但这里有个血泪经验:如果你只写"*://*/*",Chrome 88+ 会拒绝加载插件,报错Invalid host permission。必须显式拆成https和http两行——这是 Manifest V3 的硬性要求,老教程里常见的通配写法已失效。

2.2 background.js 的核心监听链:从 onBeforeRequest 到 onCompleted 的完整生命周期

background.js是插件的后台常驻脚本,它不依赖页面加载,只要浏览器开着就一直运行。plug-feedback-collect的监听逻辑集中在chrome.webRequest.onBeforeRequest.addListener和chrome.webRequest.onCompleted.addListener两个事件上。我们来拆解一段典型代码:

// background.js 关键片段 chrome.webRequest.onBeforeRequest.addListener( (details) => { // 1. 记录请求发起时间戳 const startTime = Date.now(); // 2. 提取关键字段(避免直接存 details 对象,防止循环引用) const logEntry = { id: details.requestId, url: details.url, method: details.method, tabId: details.tabId, timeStamp: startTime, // 注意:headers 在 onBeforeRequest 中不可读(除非声明 extraHeaders 权限) // 所以这里只存基础信息,body 和 headers 留给 pageScripts 补充 }; // 3. 存入内存缓存(实际项目建议用 chrome.storage.local) requestCache.set(details.requestId, logEntry); }, { urls: ["<all_urls>"] }, // 必须匹配,否则事件不触发 ["requestBody"] // 这个 extraInfoSpec 是关键!没有它,details.requestBody 为空 );
  • urls: ["<all_urls>"]是匹配模式,<all_urls>是 Manifest V3 中唯一允许的全局通配符,替代了旧版的["*://*/*"]。
  • ["requestBody"]是extraInfoSpec数组,它决定了details对象里有哪些额外字段可用。常见值有:
    • "requestBody":获取 POST/PUT 请求的原始 body(需配合webRequestBlocking权限);
    • "responseHeaders":在onHeadersReceived中读取响应头;
    • "blocking":启用同步阻塞,允许返回{ cancel: true }中断请求。

参数说明:details.requestId是 Chrome 分配的唯一 ID,贯穿整个请求生命周期;details.tabId可用于关联到具体标签页;details.method是 HTTP 方法(GET/POST/PUT/DELETE),但注意:fetch API 的method默认大写,而 XHR 的method可能小写,统一转大写再比较更稳妥。

2.3 为什么需要 pageScripts/main.js:补全 Network 面板看不到的请求细节

background.js能捕获请求 URL、method、status,但有两个致命盲区:

  • fetch/XHR 的 request body:webRequestAPI 在onBeforeRequest中拿到的details.requestBody是原始二进制流,解析成本高且不保证编码正确;
  • 自定义 headers:比如Authorization: Bearer xxx或X-Trace-ID,这些由 JS 动态设置的 header,在webRequest的onBeforeRequest中默认不可见(需额外声明extraHeaders权限,且仅对部分 header 有效)。

plug-feedback-collect的解法是双线程协作:background.js负责捕获请求骨架,pageScripts/main.js注入到页面 DOM 中,劫持原生fetch和XMLHttpRequest:

// pageScripts/main.js 片段 const originalFetch = window.fetch; window.fetch = function(...args) { const [input, init] = args; const url = typeof input === 'string' ? input : input.url || ''; // 1. 提取 body(支持 Blob/FormData/URLSearchParams) let body = null; if (init && init.body) { if (init.body instanceof Blob) { body = 'Blob(size=' + init.body.size + ')'; } else if (init.body instanceof FormData) { body = 'FormData(entries=' + init.body.entries.length + ')'; } else { body = String(init.body).substring(0, 500); // 截断防爆内存 } } // 2. 提取 headers(手动遍历 Headers 对象) const headers = {}; if (init && init.headers) { if (init.headers instanceof Headers) { for (let [key, value] of init.headers) { headers[key] = value; } } else if (typeof init.headers === 'object') { Object.assign(headers, init.headers); } } // 3. 发送补充数据到 background chrome.runtime.sendMessage({ type: 'FETCH_LOG', url, method: init?.method || 'GET', headers, body, timestamp: Date.now() }); return originalFetch.apply(this, args); };

这个劫持逻辑覆盖了 95% 的现代前端请求场景。但要注意:它只对当前页面生效,且不能捕获 iframe 内的请求(需单独注入 iframe)。这也是为什么auto.js存在——它负责自动检测并注入main.js到所有 frame。


3. popup.html 与 popup.js:把原始请求数据变成可交互的调试界面

3.1 popup.html 的极简结构:为什么不用 React/Vue,而用纯 HTML + CSS

popup.html是点击插件图标弹出的小窗口,它的设计哲学是「快、轻、稳」。plug-feedback-collect的结构极其克制:

<!-- popup.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Feedback Collector</title> <link rel="stylesheet" href="popup.css"> </head> <body> <div class="header"> <h2>请求监控</h2> <button id="clearBtn">清空</button> </div> <div class="filter"> <input type="text" id="urlFilter" placeholder="URL 包含..."> <select id="methodFilter"> <option value="">全部方法</option> <option value="GET">GET</option> <option value="POST">POST</option> <option value="FETCH">FETCH</option> </select> </div> <div class="list" id="requestList"></div> <script src="popup.js"></script> </body> </html>
  • 零框架依赖:不引入任何构建工具或打包流程,直接用chrome.runtime.sendMessage与 background 通信。这样做的好处是:启动速度 < 100ms,调试时修改 HTML/CSS 立即生效,不会因 Webpack HMR 失效而卡住。
  • CSS 定位用position: absolute而非 Flex/Grid:Popup 窗口尺寸固定(通常 300x500px),绝对定位能避免浏览器渲染引擎在小尺寸下对 Flex 的兼容性问题(尤其在旧版 Chrome 上)。

3.2 popup.js 的双向通信:如何用 sendMessage/receiveMessage 实现低延迟刷新

popup.js的核心任务是:1)初始化时拉取 background 缓存的请求列表;2)监听 background 的新增请求广播;3)响应用户过滤操作。关键代码如下:

// popup.js let currentRequests = []; // 1. 初始化:从 background 获取历史请求 chrome.runtime.sendMessage({ type: 'GET_HISTORY' }, (response) => { if (response && response.requests) { currentRequests = response.requests; renderList(); } }); // 2. 监听 background 的实时推送(使用 onMessage,非 onMessageExternal) chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.type === 'NEW_REQUEST') { currentRequests.push(request.data); // 限制最大条数,防内存溢出 if (currentRequests.length > 100) { currentRequests.shift(); } renderList(); } }); // 3. 渲染函数:用 innerHTML 而非 DOM API(性能敏感) function renderList() { const listEl = document.getElementById('requestList'); const filterUrl = document.getElementById('urlFilter').value; const filterMethod = document.getElementById('methodFilter').value; const filtered = currentRequests.filter(item => { const matchUrl = !filterUrl || item.url.includes(filterUrl); const matchMethod = !filterMethod || (filterMethod === 'FETCH' ? item.isFetch : item.method === filterMethod); return matchUrl && matchMethod; }); listEl.innerHTML = filtered.map(item => ` <div class="item ${item.status >= 400 ? 'error' : ''}"> <div class="url">${item.url.substring(0, 60)}${item.url.length > 60 ? '...' : ''}</div> <div class="meta"> <span class="method">${item.method}</span> <span class="status">${item.status || '-'}</span> <span class="time">${item.duration}ms</span> </div> <div class="headers">${JSON.stringify(item.headers || {}, null, 2)}</div> </div> `).join(''); }
  • chrome.runtime.onMessage是 popup 与 background 通信的唯一可靠通道。注意:sender.id在 popup 场景下为undefined,所以不要依赖它做身份校验。
  • renderList()用innerHTML是经过实测的最优解:当列表超过 50 条时,用document.createElement循环创建节点比innerHTML慢 3~5 倍(Chrome DevTools Performance 面板可验证)。

3.3 过滤与导出功能:如何把调试数据变成可分析的结构化输出

popup.js不只是展示,更要支持二次分析。plug-feedback-collect提供了两个实用功能:

  1. URL 模糊搜索:输入api/user,自动高亮所有匹配的请求(无需正则,降低使用门槛);
  2. JSON 导出按钮:点击后生成带时间戳的文件,内容为过滤后的请求数组。
document.getElementById('exportBtn').addEventListener('click', () => { const now = new Date().toISOString().replace(/[:.]/g, '-'); const blob = new Blob([JSON.stringify(currentRequests, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `requests-${now}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); });

参数说明:JSON.stringify(obj, null, 2)的第三个参数2表示缩进 2 空格,保证导出文件可读;URL.createObjectURL是浏览器原生 API,比FileSaver.js更轻量,且无兼容性问题。


4. 避坑指南:五个让新手当场翻车的真实问题与解决方案

4.1 现象:background.js 里chrome.webRequest.onBeforeRequest.addListener完全不触发

原因:Manifest V3 中webRequest相关事件必须在service_worker字段声明的脚本中注册,而plug-feedback-collect使用的是传统background字段("background": { "scripts": ["background.js"], "persistent": true })。Chrome 91+ 已废弃persistent: true,但plug-feedback-collect仍兼容旧版。若你在 Manifest V3 项目中直接复制此结构,事件必然失效。
解决:确认你的manifest.json是 V2 还是 V3。V3 必须改用service_worker,且background.js需重命名为sw.js,并在其中用chrome.runtime.onInstalled触发监听注册:

// sw.js(Manifest V3) chrome.runtime.onInstalled.addListener(() => { chrome.webRequest.onBeforeRequest.addListener( /* ... */ ); });

4.2 现象:pageScripts/main.js注入后,fetch 请求 body 显示为[object Blob]或undefined

原因:fetch的body参数可能是Blob、FormData、URLSearchParams或ReadableStream,而String()强转对Blob和FormData返回[object Blob],对ReadableStream直接报错。
解决:按类型分别处理。plug-feedback-collect的main.js已预置判断逻辑,但需注意:FormData的entries属性在某些旧版 Chrome 中不可枚举,应改用for (let pair of formData.entries())迭代。

4.3 现象:popup 界面打开后空白,控制台报错Refused to load the script 'popup.js'

原因:Chrome 严格限制 inline script 和未声明的资源加载。popup.html中若存在<script>console.log('test')</script>或未在manifest.json的web_accessible_resources中声明popup.js,就会被拦截。
解决:检查manifest.json是否包含:

"web_accessible_resources": [ { "resources": ["popup.js", "popup.css"], "matches": ["<all_urls>"] } ]

且确保popup.html中所有 script 标签均为外链(<script src="popup.js"></script>),禁用内联脚本。

4.4 现象:监听到的details.url是 data URL 或 chrome-extension:// 开头,而非目标 API 地址

原因:webRequest会捕获所有请求,包括插件自身资源(如chrome-extension://abc123/icon.png)、data URL(base64 图片)、以及被重定向后的最终 URL。plug-feedback-collect默认不过滤,需手动筛除。
解决:在onBeforeRequest回调中添加白名单判断:

if (details.url.startsWith('data:') || details.url.startsWith('chrome-extension://') || details.url.includes('google.com') || // 排除 Google 自身请求 details.url.includes('gstatic.com')) { return; }

4.5 现象:同一请求在 Network 面板显示 200,但插件捕获的details.statusCode为 0

原因:details.statusCode仅在onCompleted事件中有效,且仅对成功完成的请求有值;若请求被onBeforeRequest中断({ cancel: true }),或发生 DNS 错误、连接超时,statusCode就是 0。
解决:不要依赖onBeforeRequest中的statusCode(它不存在),改用onCompleted事件获取真实状态码,并结合details.error字段判断失败类型:

chrome.webRequest.onCompleted.addListener( (details) => { console.log(`Status: ${details.statusCode}, Error: ${details.error}`); }, { urls: ["<all_urls>"] } );

5. 进阶技巧:用 auto.js 实现全自动注入与跨域请求捕获

5.1 auto.js 的工作原理:如何绕过 CSP 限制注入 main.js

pageScripts/auto.js是整个插件的“隐形推手”。它不直接处理请求,而是负责在页面 DOM Ready 后,动态创建<script>标签并插入main.js。关键在于它利用了 Chrome 插件的特权:即使目标网站启用了Content-Security-Policy: script-src 'self',插件注入的脚本仍能执行,因为它是通过chrome.scripting.executeScript(V3)或chrome.tabs.executeScript(V2)API 注入的,不受页面 CSP 约束。

// auto.js chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (changeInfo.status === 'complete' && tab.url) { // 1. 检查是否为有效网页(排除 chrome://、about:blank) if (tab.url.startsWith('http') || tab.url.startsWith('https')) { // 2. 注入 main.js(V2 写法) chrome.tabs.executeScript(tabId, { file: 'pageScripts/main.js', runAt: 'document_idle' // 等 DOM 加载完再注入 }); } } });
  • runAt: 'document_idle'是黄金参数:它确保main.js在 DOM 构建完成、但window.onload未触发前执行,此时fetch和XMLHttpRequest已被原生定义,劫持成功率 100%。
  • 若目标页面使用了Shadow DOM,main.js默认无法穿透。此时需在auto.js中追加注入逻辑:
chrome.tabs.executeScript(tabId, { code: ` const shadowRoot = document.querySelector('your-shadow-host')?.shadowRoot; if (shadowRoot) { const script = document.createElement('script'); script.src = chrome.runtime.getURL('pageScripts/main.js'); shadowRoot.appendChild(script); } `, runAt: 'document_idle' });

5.2 表格:跨域请求捕获能力对比(fetch / XHR / img / iframe)

请求类型webRequest是否捕获pageScripts/main.js是否捕获需要额外权限备注
fetch()✅(所有域名)✅(同源 & 跨域)无main.js可读取 body 和 headers
XMLHttpRequest✅(所有域名)✅(同源 & 跨域)无需重写open()和send()方法
<img src="...">✅(所有域名)❌无webRequest可捕获,但无法获取 referrer 或触发 JS 逻辑
<iframe src="...">✅(所有域名)⚠️(仅主 frame)无auto.js需递归注入到每个 iframe
navigator.sendBeacon()✅(所有域名)❌无webRequest可捕获,但sendBeacon的 body 无法通过requestBody获取

提示:navigator.sendBeacon()是埋点常用 API,它发送请求时不等待响应,webRequest能捕获其 URL 和 method,但details.requestBody为空。若需 body,只能在 Beacon 发起前由业务代码主动上报。

5.3 实战技巧:用 chrome.storage.local 替代内存缓存,实现重启不丢数据

plug-feedback-collect默认用Map对象缓存请求,但浏览器重启后数据全丢。生产环境必须持久化。chrome.storage.local是最佳选择,但它有 5MB 限额和异步 API,需改造background.js:

// 替换原来的 requestCache = new Map() async function saveRequest(logEntry) { try { const data = await chrome.storage.local.get(['requests']); const requests = data.requests || []; requests.push(logEntry); // 限制存储条数,防爆仓 if (requests.length > 1000) { requests.splice(0, requests.length - 1000); } await chrome.storage.local.set({ requests }); } catch (e) { console.error('Save failed:', e); } } // 在 onBeforeRequest 回调中调用 chrome.webRequest.onBeforeRequest.addListener( (details) => { saveRequest({ id: details.requestId, url: details.url, method: details.method, timestamp: Date.now() }); }, { urls: ["<all_urls>"] }, ["requestBody"] );
  • chrome.storage.local的读写是异步的,不能用await在事件回调中阻塞,所以saveRequest内部用try/catch处理错误,失败不影响主流程。
  • 每次set都是全量覆盖,因此先get再push再set,是唯一安全模式。

从那以后我每次写插件监听逻辑,都强制走一遍chrome.storage.local的读写链路压测——用chrome.storage.local.set({ test: new Array(1000000).fill('x') })模拟大数据写入,看是否会触发QUOTA_BYTES_PER_ITEM错误。只有扛过这一关,才敢说这个监听方案能上生产。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询