大家好,我是 前端架构师 - 大卫。
更多优质内容请关注微信公众号 @程序员大卫。
初心为助前端人🚀,进阶路上共星辰✨,
您的点赞👍与关注❤️,是我笔耕不辍的灯💡。
背景
当你想写一个油猴脚本(Tampermonkey)来注入网页,并拦截页面原有的 XMLHttpRequest 和 fetch 请求时,并返回自定义的 response 数据,实现对页面内容的"随心所欲"地改造和渲染。
这篇文章将一步步介绍如何实现这个过程。
实现步骤
1. 准备工作
1.首先,在 Chrome 商店安装 Tampermonkey 插件(谷歌商店需要科学上网):
chromewebstore.google.com/detail/tamp...
2.安装完成后,点击浏览器插件图标,选择"添加新脚本",就可以开始编写你的第一个脚本:

2. 编写代码
1.油猴脚本的前置
⚠️注意:@run-at
字段必须设置为 document-start
,这样可以尽可能早地拦截 XMLHttpRequest
和 fetch
请求,也能避免页面里的 console.log
被其他脚本覆盖。
perl
// ==UserScript==
// @name API - 拦截
// @namespace http://tampermonkey.net/
// @version 0.1
// @description 根据不同请求 URL 自定义不同的响应数据
// @match https://www.xxx.com/*
// @run-at document-start
// @grant none
// ==/UserScript==
不过需要注意,document-start
实际的执行时机并不是页面加载最开始的时候,而是 DOM Ready 之后。这时因为油猴本质上是通过 Chrome 的内容脚本机制注入代码的,因此仍然受到一些限制。
可以通过以下示例来验证脚本的实际执行时机。
油猴脚本代码:
js
// ==UserScript==
// @name 测试油猴脚本执行
// @namespace http://tampermonkey.net/
// @version 2025-04-10
// @description try to take over the world!
// @author You
// @match http://*/*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @run-at document-start
// @grant none
// ==/UserScript==
(function() {
'use strict';
console.log('油猴脚本执行');
})();
HTML 页面代码:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>测试油猴脚本执行时机</title>
</head>
<body>
<script>
console.log("Script excution started");
document.addEventListener("DOMContentLoaded", function () {
console.log("Dom ready");
});
window.addEventListener("load", () => {
console.log("Page loaded");
});
</script>
</body>
</html>
控制台输出结果:
arduino
Script execution started
Dom ready
油猴脚本执行
Page loaded
可以看到,油猴脚本在 DOM 完成之后才运行的。
2.拦截 Fetch 请求
几点关键说明:
- 原始请求建议仍然发出:这样可以在开发工具中看到真实的返回值,便于调试。
- 返回的
mock
数据必须是window.Response
的实例。 - Response 的 body 是一次性读取的流,如果需要打印内容,请先使用
.clone()
,否则会报错:TypeError: body stream already read.
拦截代码如下:
js
const tryJSONParse = (str) => {
try {
return JSON.parse(str);
} catch (e) {
return str;
}
};
const log = console.log;
function setupFetchInterceptor(interceptRules = []) {
const originalFetch = window.fetch;
window.fetch = function (input, init) {
const url = typeof input === "string" ? input : input.url;
const matchedRule = interceptRules.find((rule) => rule.match(url));
const orgResponse = originalFetch(input, init);
if (matchedRule) {
const mockResponse = createMockResponse(matchedRule);
// 需要 clone 一下然后再打印日志,因为 Response 的 body 是一个一次性可读流(ReadableStream),
// 第二次尝试读取时会抛出错误 `TypeError: body stream already read。`
mockResponse
.clone()
.text()
.then((data) => {
log(
`🔥 拦截 Fetch 响应:
🌐 InterceptedUrl: ${url}
📦 FakeResponse:`,
tryJSONParse(data),
);
});
return mockResponse;
} else {
return orgResponse;
}
};
function createMockResponse(rule) {
const { body, ...init } = {
status: 200,
headers: { "content-type": "application/json" },
...rule.response,
};
let responseBody = body;
const contentType = init.headers?.["content-type"];
if (
typeof responseBody === "object" &&
contentType?.includes("application/json")
) {
responseBody = JSON.stringify(responseBody);
}
return new window.Response(responseBody, init);
}
}
setupFetchInterceptor(interceptRules);
3. 拦截 XMLHttpRequest 请求
需要注意以下几点:
- Axios 内部默认也是通过 XMLHttpRequest 发送请求。
2.不能单纯使用 Reflect.set 来代理 Web API
提供的原生宿主对象(如 XMLHttpRequest
)
因为这些对象的属性 setter
往往依赖内部上下文绑定(this
)。使用 Reflect.set 时,如果上下文绑定不正确,会抛出如 TypeError: Illegal invocation
这样的错误。
以下是一个常见的错误示例:
js
const xhr = new window.XMLHttpRequest();
const proxy = new Proxy(xhr, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
return typeof value === "function" ? value.bind(target) : value;
},
set(target, prop, value, receiver) {
return Reflect.set(target, prop, value, receiver);
},
});
proxy.onreadystatechange = ()=> {};
3.另外为了和原型保持一致,需要加 CustomXHR.prototype = OriginalXHR.prototype
最终 XMLHttpRequest
拦截代码如下:
js
function setupXHRInterceptor(interceptRules = []) {
const OriginalXHR = window.XMLHttpRequest;
function CustomXHR() {
const xhr = new OriginalXHR();
let interceptedUrl = null;
let matchedRule = null;
// 自定义数据注入(供 Proxy 读取)
let mockData = {
readyState: undefined,
status: undefined,
responseText: undefined,
};
// open 拦截,记录请求 URL 和匹配规则
const originalOpen = xhr.open;
xhr.open = function (method, url, ...args) {
interceptedUrl = url;
matchedRule = interceptRules.find((rule) => rule.match(url));
return originalOpen.call(this, method, url, ...args);
};
// send 拦截:匹配则生成伪造响应
const originalSend = xhr.send;
xhr.send = function (...args) {
if (matchedRule) {
const { body, status = 200 } = matchedRule.response;
const fakeResponseText =
typeof body === "object"
? JSON.stringify(body)
: String(body ?? "");
// 设置 mock 数据
mockData = {
readyState: 4,
status,
responseText: fakeResponseText,
};
// 打印日志(保留原有)
log(
`🔥 拦截 XHR 响应:
🌐 InterceptedUrl: ${interceptedUrl}
📦 FakeResponse:`,
tryJSONParse(fakeResponseText),
);
}
return originalSend.apply(this, args);
};
// Proxy 包装,拦截只读属性的读取
return new Proxy(xhr, {
get(target, prop) {
if (prop === "readyState" && mockData.readyState) {
return mockData.readyState;
}
if (prop === "status" && mockData.status) {
return mockData.status;
}
if (
(prop === "responseText" || prop === "response") &&
mockData.responseText
) {
return mockData.responseText;
}
const value = target[prop];
return typeof value === "function" ? value.bind(target) : value;
},
set(target, prop, value) {
target[prop] = value;
return true;
},
});
}
// 原型保持一致
CustomXHR.prototype = OriginalXHR.prototype;
// 替换全局 XMLHttpRequest
window.XMLHttpRequest = CustomXHR;
}
setupXHRInterceptor(interceptRules);
4. 编写 interceptRules
interceptRules 用于定义需要拦截的 API 以及其返回的数据格式:
js
const interceptRules = [{
match: url => url.includes('/api/xxx'),
response: {
body: {
userId: 1111
}
}
}]
实战
假设我们要修改掘金首页的"作者版"
版块,如下图所示:

首先打开 Chrome 开发者工具,可以看到该版块的请求地址为:
bash
https://api.juejin.cn/user_api/v1/quality_user/rank
接口返回数据如下图:

并且这个请求是通过 fetch 发起的:

接下来我们编写完整的 fetch 拦截代码:
js
// ==UserScript==
// @name 掘金首页拦截接口
// @namespace http://tampermonkey.net/
// @version 2025-04-10
// @description try to take over the world!
// @author You
// @match https://juejin.cn/
// @icon https://www.google.com/s2/favicons?sz=64&domain=juejin.cn
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
"use strict";
const interceptRules = [
{
match: (url) =>
url.includes("https://api.juejin.cn/user_api/v1/quality_user/rank"),
response: {
body: {
err_no: 0,
err_msg: "success",
data: {
user_rank_list: [
{
user_info: {
user_id: "4416102247436824",
user_name: "🔥马云🔥",
},
},
],
},
},
},
},
];
const tryJSONParse = (str) => {
try {
return JSON.parse(str);
} catch (e) {
return str;
}
};
const log = console.log;
function setupFetchInterceptor(interceptRules = []) {
const originalFetch = window.fetch;
window.fetch = function (input, init) {
const url = typeof input === "string" ? input : input.url;
const matchedRule = interceptRules.find((rule) => rule.match(url));
const orgResponse = originalFetch(input, init);
if (matchedRule) {
const mockResponse = createMockResponse(matchedRule);
// 需要 clone 一下然后再打印日志,因为 Response 的 body 是一个一次性可读流(ReadableStream),
// 第二次尝试读取时会抛出错误 `TypeError: body stream already read。`
mockResponse
.clone()
.text()
.then((data) => {
log(
`🔥 拦截 Fetch 响应:
🌐 InterceptedUrl: ${url}
📦 FakeResponse:`,
tryJSONParse(data)
);
});
return mockResponse;
} else {
return orgResponse;
}
};
function createMockResponse(rule) {
const { body, ...init } = {
status: 200,
headers: { "content-type": "application/json" },
...rule.response,
};
let responseBody = body;
const contentType = init.headers?.["content-type"];
if (
typeof responseBody === "object" &&
contentType?.includes("application/json")
) {
responseBody = JSON.stringify(responseBody);
}
return new window.Response(responseBody, init);
}
}
setupFetchInterceptor(interceptRules);
})();
思考
有人可能会问:为什么不直接使用 Charles 或 Proxyman 这类抓包工具?
首先,这些工具需要你每次都手动启动,而且如果你电脑中已经在使用如 ClashX 这类网络代理工具,当你同时打开抓包工具时可能会发生冲突,导致代理不可用。
相比之下,油猴脚本更轻便。虽然它在执行时机上有一定限制(不能在最开始执行),并且脚本的启用/禁用并不是实时生效,需要刷新页面才能应用,但整体来说仍然是一个非常灵活的解决方案。
总结
本文介绍了如何用油猴脚本拦截网页中的 fetch
和 XMLHttpRequest
请求,并返回自定义数据。相比抓包工具,这种方式更轻量、灵活,适合快速调试和页面定制。不过也有局限,比如脚本执行时机受限,启用需刷新页面等。整体来说,是前端开发中非常实用的一种技巧。