写在开头
嘿,各位UU们好呀!👋
最近周末小编去惠州爬了罗浮山,从烈日爬到了夜晚,这山海拔有 1296 米,爬起来还真有点费腿,但看了山顶的极致风光后,一切都值了。🌄
🎯需求背景
最近,小编在开发浏览器插件的时候,遇到了一个需求:需要拦截第三方网站的网络请求,获取特定接口的响应体,然后把数据上报。
浏览器插件开发小编使用的是 WXT 框架,原因无它,就是方便,不需要处理太多兼容性问题。
经过一番研究和实践,小编在 WXT 框架中采用 JS 注入的方式,通过代理 XMLHttpRequest 和 fetch API,成功实现了对第三方网站请求的拦截,并获取到了想要的接口数据。
接下来,小编将分享这个实现过程。咱们以拦截掘金的用户接口为例哈👻,因为小编的需求本质就是拦截某个网站的用户接口,获取其中的用户信息。
https://api.juejin.cn/user_api/v1/user/get?
🛠️实现过程
第1️⃣步:配置 WXT 权限
首先,我们需要在 WXT 配置中添加必要的权限。
在 wxt.config.ts 文件中添加权限配置:
export default defineConfig({
manifest: {
permissions: [
'webRequest',
'tabs',
'scripting'
],
host_permissions: [
'*://*/*' // 允许所有网站(或指定目标网站)
]
}
});
为什么要配置这些权限呢❓
因为实现网络请求拦截功能,主要依赖 chrome.webRequest API 和 chrome.scripting API。
根据 Chrome 扩展程序的规范,访问大多数扩展 API 和功能时,必须在扩展程序的 清单文件 中声明相应权限。某些权限会触发用户授权提示,用户同意后才能正常使用扩展功能。
关于这些权限的详细作用,小编就不一一展开了,感兴趣的小伙伴可以查阅官方文档:传送门。
这里其实不用太纠结某个 API 具体属于哪个权限,咱们可以把常用权限都配置上,反正呢,影响不大。😋
permissions: [
"activeTab",
"alarms",
"background",
"bookmarks",
"browsingData",
"clipboardRead",
"clipboardWrite",
"contentSettings",
"contextMenus",
"cookies",
"debugger",
"declarativeContent",
"declarativeNetRequest",
"declarativeNetRequestFeedback",
"desktopCapture",
"downloads",
"fontSettings",
"gcm",
"geolocation",
"history",
"identity",
"idle",
"management",
"nativeMessaging",
"notifications",
"pageCapture",
"power",
"printerProvider",
"privacy",
"proxy",
"scripting",
"search",
"sessions",
"storage",
"system.cpu",
"system.display",
"system.memory",
"system.storage",
"tabCapture",
"tabGroups",
"tabs",
"topSites",
"tts",
"ttsEngine",
"unlimitedStorage",
"webNavigation",
"webRequest"
]
第2️⃣步:创建网络拦截器
接下来,我们需要创建一个网络拦截器,用来重写 fetch 和 XMLHttpRequest。
在 utils/injectNetworkInterceptor.ts 文件中创建拦截器:
export function injectNetworkInterceptor() {
const win = window as any;
/**
* @name 统一处理响应结果,可以存储到本地,或者通过postMessage发送到其他地方
*/
function handleResponseData(data: any) {
// 找到url特定接口
if (data.url.includes("juejin") && data.url.includes("user/get")) {
try {
// 解析响应数据
let responseData = data.responseBody;
if (typeof responseData === 'string') {
responseData = JSON.parse(responseData);
}
// 检查是否存在用户信息
if (responseData && responseData.data) {
const userInfo = {
user_id: responseData.data.user_id,
user_name: responseData.data.user_name,
};
// 存储到 sessionStorage ,再其他地方可以通过本地缓存获取到
sessionStorage.setItem('juejin_user_info', JSON.stringify(userInfo));
}
} catch (error) {
console.error('❌ 解析响应数据失败:', error);
}
}
}
// 避免重复注入
if (win.__WEFLY_NETWORK_INTERCEPTOR_INJECTED__) return;
win.__WEFLY_NETWORK_INTERCEPTOR_INJECTED__ = true;
// 保存原始的fetch和XMLHttpRequest
const originalFetch = window.fetch;
const originalXMLHttpRequest = window.XMLHttpRequest;
// ... 后续代码
}
这个拦截器的核心思路是:
- 重写浏览器原生的
fetch和XMLHttpRequest方法。 - 在请求完成后拦截响应数据。
- 根据URL和响应内容进行过滤和处理。
- 将需要的数据存储到本地。
第3️⃣步:重写 fetch API
在拦截器中重写 fetch 方法:
/**
* 重写fetch API
*/
win.fetch = async function (...args: any) {
const [resource, config] = args;
const url = typeof resource === "string" ? resource : resource.url;
const method = config?.method || "GET";
let response = null;
try {
// 调用原始fetch
response = await originalFetch.apply(this, args);
// 克隆响应以便读取响应体
const responseClone = response.clone();
// 尝试获取响应体
let responseBody = null;
try {
const contentType = response.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
responseBody = await responseClone.json();
} else if (contentType.includes("text/")) {
responseBody = await responseClone.text();
} else {
responseBody = "[Binary Data]";
}
} catch (error) {
responseBody = "[Parse Data Fail]";
}
const interceptedData = {
type: "fetch",
url,
method,
requestHeaders: config?.headers || {},
requestBody: config?.body || null,
responseStatus: response.status,
responseStatusText: response.statusText,
responseBody,
timestamp: Date.now(),
};
handleResponseData(interceptedData);
} catch (error) {
if (url.includes("chrome-extension://") || url.includes("127.0.0.1")) {
// 忽略扩展和本地资源的报错
return;
}
console.error("❌ [fetch拦截] 请求失败:", url, error);
} finally {
return response;
}
};
第4️⃣步:重写 XMLHttpRequest
同样需要重写 XMLHttpRequest,因为有些网站可能还在使用这个老的API:
/**
* 重写XMLHttpRequest
*/
win.XMLHttpRequest = function () {
const xhr: any = new originalXMLHttpRequest();
const originalOpen = xhr.open;
const originalSend = xhr.send;
const originalSetRequestHeader = xhr.setRequestHeader;
let requestData: any = {
url: "",
method: "",
headers: {},
body: null,
};
// 重写open方法
xhr.open = function (method: any, url: any, async: any, user: any, password: any) {
requestData.method = method;
requestData.url = url;
return originalOpen.call(this, method, url, async, user, password);
};
// 重写setRequestHeader方法
xhr.setRequestHeader = function (header: any, value: any) {
requestData.headers[header] = value;
return originalSetRequestHeader.call(this, header, value);
};
// 重写send方法
xhr.send = function (body: any) {
requestData.body = body;
// 监听响应
const originalOnReadyStateChange = xhr.onreadystatechange;
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
// 请求完成
const interceptedData = {
type: "xhr",
url: requestData.url,
method: requestData.method,
requestHeaders: requestData.headers,
requestBody: requestData.body,
responseStatus: xhr.status,
responseStatusText: xhr.statusText,
responseBody: xhr.responseText,
timestamp: Date.now(),
};
handleResponseData(interceptedData);
}
// 调用原始的onreadystatechange
if (originalOnReadyStateChange) {
originalOnReadyStateChange.call(this);
}
};
return originalSend.call(this, body);
};
return xhr;
};
// 复制原始XMLHttpRequest的静态属性
Object.setPrototypeOf(win.XMLHttpRequest, originalXMLHttpRequest);
Object.setPrototypeOf(win.XMLHttpRequest.prototype, originalXMLHttpRequest.prototype);
第5️⃣步:在 background 中注入拦截器
最后,我们需要在 background.ts 中监听标签页更新事件,并注入我们的拦截器:
import { injectNetworkInterceptor } from "@/utils/injectNetworkInterceptor"
export default defineBackground(() => {
/**
* @name 初始化JS注入拦截器
* @description 使用chrome.scripting.executeScript API注入JavaScript代码来拦截第三方网站的网络请求
*/
function initJSInjectionInterceptor() {
browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
// 只在页面完全加载后处理,且排除特殊页面和协议
if (changeInfo.status === 'complete' && tab.url && isValidUrl(tab.url)) {
console.log("🎯 插件注入JS代码")
try {
// 使用chrome.scripting.executeScript注入拦截代码
await browser.scripting.executeScript({
target: { tabId: tabId },
func: injectNetworkInterceptor,
world: 'MAIN' // 在主页面上下文中执行,可以访问页面的全局变量
})
} catch (error) {
console.error('❌ 注入拦截代码失败:', error)
}
}
})
}
/**
* @name 检查URL是否有效
* @param {string} url 页面URL
* @returns {boolean} 是否为有效URL
*/
function isValidUrl(url: string) {
// 仅允许配置的域名注入JS
const allowedDomains = [
'juejin.cn',
// 可以添加更多域名
];
const invalidProtocols = [
'chrome://',
'chrome-extension://',
'moz-extension://',
'about:',
'data:',
'file:'
];
// 检查是否为无效协议
if (invalidProtocols.some(protocol => url.startsWith(protocol))) {
return false;
}
// 检查是否为允许的域名
return allowedDomains.some(domain => url.includes(domain));
}
// 启动拦截器
initJSInjectionInterceptor();
});
这里的关键配置:
world: 'MAIN':确保代码在主页面上下文中执行,可以访问页面的全局变量。changeInfo.status === 'complete':确保页面完全加载后再注入。isValidUrl()方法:只在目标网站上注入代码,避免不必要的性能开销。
🎉结果
当我们访问掘金网站时,插件会自动注入拦截代码,只要页面发起用户信息相关的API请求时,咱们就能在拦截到的数据:
至此,本篇文章就写完啦,撒花撒花。
希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。