🔥 如何“为所欲为”地渲染页面:优雅拦截 Fetch 和 XMLHttpRequest!

大家好,我是 前端架构师 - 大卫

更多优质内容请关注微信公众号 @程序员大卫

初心为助前端人🚀,进阶路上共星辰✨,

您的点赞👍与关注❤️,是我笔耕不辍的灯💡。

背景

当你想写一个油猴脚本(Tampermonkey)来注入网页,并拦截页面原有的 XMLHttpRequest 和 fetch 请求时,并返回自定义的 response 数据,实现对页面内容的"随心所欲"地改造和渲染。

这篇文章将一步步介绍如何实现这个过程。

实现步骤

1. 准备工作

1.首先,在 Chrome 商店安装 Tampermonkey 插件(谷歌商店需要科学上网):

chromewebstore.google.com/detail/tamp...

2.安装完成后,点击浏览器插件图标,选择"添加新脚本",就可以开始编写你的第一个脚本:

2. 编写代码

1.油猴脚本的前置

⚠️注意:@run-at 字段必须设置为 document-start,这样可以尽可能早地拦截 XMLHttpRequestfetch 请求,也能避免页面里的 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 请求

需要注意以下几点:

  1. 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 这类网络代理工具,当你同时打开抓包工具时可能会发生冲突,导致代理不可用。

相比之下,油猴脚本更轻便。虽然它在执行时机上有一定限制(不能在最开始执行),并且脚本的启用/禁用并不是实时生效,需要刷新页面才能应用,但整体来说仍然是一个非常灵活的解决方案。

总结

本文介绍了如何用油猴脚本拦截网页中的 fetchXMLHttpRequest 请求,并返回自定义数据。相比抓包工具,这种方式更轻量、灵活,适合快速调试和页面定制。不过也有局限,比如脚本执行时机受限,启用需刷新页面等。整体来说,是前端开发中非常实用的一种技巧。

相关推荐
吃杠碰小鸡9 分钟前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone15 分钟前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_090134 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农1 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝2 小时前
RBAC前端架构-01:项目初始化
前端·架构