Chrome V3 插件开发:监听并转发 API 请求
基于 Manifest V3 实现 Chrome 插件的网络请求监听与转发
本指南详细说明如何从零开始创建一个 API 监控与转发的 Chrome 扩展插件
📋 目录
- 项目概述
- 核心架构
- Manifest V3 配置
- 核心代码实现
- 国际化配置
- 完整工作流程
- 调试技巧
项目概述
功能目标
创建一个 Chrome 扩展,能够:
- ✅ 拦截网页的
fetch()和XMLHttpRequestAPI 请求 - ✅ 捕获完整的请求/响应数据(headers、body、status 等)
- ✅ 将捕获的数据转发到自定义 Webhook 端点
- ✅ 支持多语言(中文/英文)
- ✅ 提供可视化配置界面
技术栈
- Manifest V3 - Chrome 扩展最新规范
- Content Scripts - 注入脚本到网页
- Service Worker - 后台处理
- Chrome Storage API - 配置存储
- Chrome i18n API - 国际化
核心架构
三层通信架构
┌──────────────────────────┐
│ 目标网页 (MAIN world) │
│ - 拦截 fetch/XHR │
│ - 捕获请求/响应 │
└─────────┬────────────────┘│ postMessage▼
┌──────────────────────────┐
│ Bridge (ISOLATED world) │
│ - 接收 MAIN 数据 │
│ - 转发到 Service Worker │
└─────────┬────────────────┘│ chrome.runtime.sendMessage▼
┌──────────────────────────┐
│ Service Worker 后台 │
│ - 管理配置 │
│ - 转发到 Webhook │
└──────────────────────────┘
为什么需要三层架构?
Manifest V3 的安全限制:
- MAIN world - 可以访问网页的原生 API(fetch、XHR),但无法访问 Chrome API
- ISOLATED world - 可以访问 Chrome API,但无法访问网页的原生对象
- Service Worker - 处理后台任务,无法直接访问网页
解决方案: 使用 postMessage 桥接 MAIN 和 ISOLATED world
Manifest V3 配置
1. 基础配置 manifest.json
{"manifest_version": 3,"name": "__MSG_extName__","description": "__MSG_extDescription__","default_locale": "en","version": "2.0.0","icons": {"16": "logo/logo-16.png","48": "logo/logo-48.png","128": "logo/logo-128.png"},"permissions": ["storage", // 存储配置"tabs" // 获取当前标签页信息],"host_permissions": ["<all_urls>" // 访问所有网站(监控用)],"background": {"service_worker": "service-worker.js"},"content_scripts": [{"js": ["content-main.js"],"matches": ["<all_urls>"],"run_at": "document_start","all_frames": false,"world": "MAIN" // 🔑 关键:运行在 MAIN world},{"js": ["content-bridge.js"],"matches": ["<all_urls>"],"run_at": "document_start","all_frames": false // 默认 ISOLATED world}],"action": {"default_title": "API Mirror","default_popup": "popup/popup.html"},"options_page": "config/config.html"
}
关键配置说明
| 字段 | 说明 | 重要性 |
|---|---|---|
manifest_version: 3 | 使用 V3 规范 | ⭐⭐⭐ |
world: "MAIN" | 运行在网页主环境,可拦截原生 API | ⭐⭐⭐ |
run_at: "document_start" | 页面加载前注入,确保拦截所有请求 | ⭐⭐⭐ |
all_frames: false | 只在主框架运行,避免重复 | ⭐⭐ |
host_permissions | V3 中独立于 permissions | ⭐⭐⭐ |
核心代码实现
1. Content Script (MAIN World) - 拦截 API
文件:content-main.js
(function () {'use strict';// 防止重复注入if (window.__API_MONITOR_INJECTED__) return;window.__API_MONITOR_INJECTED__ = true;const currentDomain = window.location.hostname;let siteConfig = null;let isConfigured = false;// 📨 监听来自 Bridge 的配置window.addEventListener('message', function (event) {if (event.source !== window) return;if (event.data?.type === 'API_MONITOR_CONFIG') {siteConfig = event.data.config;isConfigured = true;}});// 📤 请求配置window.postMessage({type: 'API_MONITOR_REQUEST_CONFIG',domain: currentDomain}, '*');// ✅ 判断是否需要监控此 URLfunction shouldMonitorURL(url) {if (!isConfigured || !siteConfig?.enabled) return false;return siteConfig.apis.some(api => {if (!api.enabled) return false;switch (api.matchType) {case 'exact':return url === api.pattern;case 'contains':return url.includes(api.pattern);case 'startsWith':return url.startsWith(api.pattern);case 'regex':try {return new RegExp(api.pattern).test(url);} catch (e) {return false;}default:return url.includes(api.pattern);}});}// 📤 发送数据到 Bridgefunction sendToBackground(data) {window.postMessage({type: 'API_MONITOR_TO_BRIDGE',payload: data}, '*');}// 🎣 Hook Fetch APIconst originalFetch = window.fetch;window.fetch = function (...args) {const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;// 捕获请求体let requestBody = null;if (args[1]?.body) {try {requestBody = typeof args[1].body === 'string' ? JSON.parse(args[1].body) : args[1].body;} catch (e) {requestBody = args[1].body;}}const fetchPromise = originalFetch.apply(this, args);// 异步捕获响应fetchPromise.then(async response => {if (shouldMonitorURL(url)) {const clonedResponse = response.clone();try {const responseBody = await clonedResponse.json().catch(() => null);const capturedData = {domain: currentDomain,url: url,method: args[1]?.method || 'GET',timestamp: new Date().toISOString(),status: response.status,statusText: response.statusText,requestHeaders: args[1]?.headers || {},requestBody: requestBody,responseBody: responseBody};sendToBackground({type: 'API_CAPTURED',data: capturedData