从 "无中生有" 到 "以假乱真":Mock 是如何 "拦截" 你的请求的?

当你在前端项目中写下 axios.get('/api/list') 并顺利拿到数据时,有没有想过一个问题:如果后端接口压根没开发,这些数据是从哪来的?Mock 工具就像一位隐形的魔术师,在你看不到的地方完成了一场 "偷天换日" 的表演 ------ 拦截你的请求,返回预设的数据,让前端开发彻底摆脱对后端的依赖。

为什么需要请求拦截?

在前后端分离的开发模式中,"接口不同步" 是永远的痛:

  • 产品经理催着要 Demo,但后端接口排期还在下周
  • 前端页面逻辑写完了,只能用假数据 const data = [...] 硬撑
  • 联调时发现接口返回格式和文档不符,大量组件需要返工

Mock 工具的出现,正是通过 "请求拦截" 解决了这些问题。它的核心价值在于:在不修改前端业务代码的前提下,用模拟数据替代真实接口返回,让前后端开发可以并行推进。

举个直观的例子:当你调用 axios.post('/api/login', { username: 'test' }) 时,Mock 会在请求发出前 "截胡",直接返回 { code: 0, token: 'xxx' } 的模拟数据,整个过程中浏览器不会产生真实的网络请求,但前端代码却能正常处理 "登录成功" 的逻辑。

拦截的底层基石:浏览器的请求 API

要理解 Mock 的拦截原理,首先得知道前端请求在浏览器中是如何工作的。目前主流的请求方式有两种:XMLHttpRequest(简称 XHR)和 fetch,Mock 正是通过重写这两个 API 实现拦截的。

1. XMLHttpRequest

几乎所有主流请求库(axios、jQuery.ajax、vue-resource 等)的底层都依赖 XMLHttpRequest。它的工作流程如下:

js 复制代码
// 原生 XHR 请求流程
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data'); // 初始化请求
xhr.addEventListener('readystatechange', function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log('请求结果:', xhr.responseText); // 处理响应
  }
});
xhr.send(); // 发送请求

这个流程中有两个关键节点:open 方法(设置请求信息)和 send 方法(发送请求)。Mock 工具正是通过重写这两个方法,实现了对 XHR 请求的拦截。

Mock 如何拦截 XHR?

我们可以用一段简化的代码模拟 Mock.js 的拦截逻辑:

js 复制代码
  // 保存原生 XHR 构造函数
  const originalXHR = window.XMLHttpRequest;
  const that = this;
  // 重写全局 XHR 构造函数
  window.XMLHttpRequest = function() {
    const xhr = new originalXHR();
    // 保存原生 open和send方法
    const originalOpen = xhr.open;
    const originalSend = xhr.send;
    
    // 重写 open 记录请求信息
    xhr.open = function(method, url) {
      // 记录请求方法和 URL(关键:用于后续匹配 Mock 规则)
      this._method = method.toUpperCase();
      this._url = url;
      // 调用原生 open 方法
      return originalOpen.apply(this, arguments);
    };
    
    // 重写 send 进行拦截
    xhr.send = function(body) {
      // 查找匹配的规则
      const matched = that.rules.find(rule => 
        rule.url === this._url && rule.method === this._method
      );
      
      if (matched) {
        // 模拟异步响应
        setTimeout(() => {
          // 1. 先设置 readyState 为 4
          Object.defineProperty(this, 'readyState', {
            value: 4,
            configurable: true
          });
          
          // 2. 设置响应数据
          Object.defineProperty(this, 'responseText', {
            value: JSON.stringify(matched.response()),
            configurable: true
          });
          
          // 3. 设置状态码
          Object.defineProperty(this, 'status', {
            value: 200,
            configurable: true
          });
          
          // 4. 设置响应头 (axios会检查这个)
          this.getAllResponseHeaders = () => {
            return 'Content-Type: application/json';
          };
          
          // 5. 触发事件
          if (typeof this.onreadystatechange === 'function') {
            this.onreadystatechange();
          }
          
          this.dispatchEvent(new Event('readystatechange'));
          this.dispatchEvent(new Event('load'));
        }, matched.delay || 0);
      } else {
        return originalSend.call(this, body);
      }
    };
    
    return xhr;
  };

这段代码揭示了核心逻辑:Mock 通过重写 XHR 的 open 和 send 方法,在请求发送前进行规则匹配,命中则返回模拟数据,否则走正常请求流程

当你使用 axios 时,它内部创建的 XMLHttpRequest 实例已经是被 Mock 改写过的版本,所以能被无缝拦截。

2. fetch

fetch 是 ES6 新增的请求 API,基于 Promise 设计,语法更简洁,但它并不依赖 XHR,因此需要单独处理。

js 复制代码
// 原生 fetch 请求
fetch('/api/user', { 
  method: 'POST', 
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ id: 1 }) 
})
  .then(res => {
    if (!res.ok) throw new Error('请求失败');
    return res.json();
  })
  .then(data => console.log(data))
  .catch(err => console.error(err));

Mock 拦截 fetch 的思路与 XHR 类似,即重写全局的 fetch 函数:

js 复制代码
// 保存原生fetch
const originalFetch = window.fetch;
const that = this;

window.fetch = async function(url, options = {}) {
  // 提取请求方法 (默认GET)
  const method = options.method?.toUpperCase() || 'GET';
  //匹配规则
  const matched = that.rules.find(rule => 
    rule.url === url && rule.method === method
  );
  
  if (matched) {
    // 命中规则:返回模拟数据的Response 对象
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(new Response(
          JSON.stringify(matched.response()),
          {
            status: 200,
            headers: { 'Content-Type': 'application/json' }
          }
        ));
      }, matched.delay || 0);
    });
  }
  // 未命中:调用原生 fetch
  return originalFetch(url, options);
};

通过这种方式,所有通过 fetch 发起的请求都会经过 Mock 的规则检查,实现无缝拦截。

亲手实现一个简易 Mock 工具

光说不练假把式,我们来实现一个简化版的 Mock 工具,亲身体验拦截的全过程。

步骤 1:创建 Mock 核心逻辑

js 复制代码
// mock.js
export default class SimpleMock {
  constructor() {
    this.rules = []; // 存储 Mock 规则
    this.init(); // 初始化拦截
  }
  
  // 添加 Mock 规则
  addRule(rule) {
    this.rules.push({
      ...rule,
      method: rule.method?.toUpperCase() || 'GET'
    });
  }
  
  // 初始化 XHR 和 fetch 拦截
  init() {
    this.interceptXHR();
    this.interceptFetch();
  }
  
// 拦截 XHR
interceptXHR() {
  const originalXHR = window.XMLHttpRequest;
  const that = this;
  
  window.XMLHttpRequest = function() {
    const xhr = new originalXHR();
    const originalOpen = xhr.open;
    const originalSend = xhr.send;
    
    // 重写 open 记录请求信息
    xhr.open = function(method, url) {
      this._method = method.toUpperCase();
      this._url = url;
      return originalOpen.apply(this, arguments);
    };
    
    // 重写 send 进行拦截
    xhr.send = function(body) {
      // 查找匹配的规则
      const matched = that.rules.find(rule => 
        rule.url === this._url && rule.method === this._method
      );
      
      if (matched) {
        // 模拟异步响应
        setTimeout(() => {
          
          Object.defineProperty(this, 'readyState', {
            value: 4,
            configurable: true
          });
         
          Object.defineProperty(this, 'responseText', {
            value: JSON.stringify(matched.response()),
            configurable: true
          });
          
          Object.defineProperty(this, 'status', {
            value: 200,
            configurable: true
          });
         
          this.getAllResponseHeaders = () => {
            return 'Content-Type: application/json';
          };
          
          if (typeof this.onreadystatechange === 'function') {
            this.onreadystatechange();
          }
          
          this.dispatchEvent(new Event('readystatechange'));
          this.dispatchEvent(new Event('load'));
        }, matched.delay || 0);
      } else {
        return originalSend.call(this, body);
      }
    };
    
    return xhr;
  };
}
  
  // 拦截 fetch
  interceptFetch() {
    const originalFetch = window.fetch;
    const that = this;
    
    window.fetch = async function(url, options = {}) {
      const method = options.method?.toUpperCase() || 'GET';
      const matched = that.rules.find(rule => 
        rule.url === url && rule.method === method
      );
      
      if (matched) {
        return new Promise(resolve => {
          setTimeout(() => {
            resolve(new Response(
              JSON.stringify(matched.response()),
              {
                status: 200,
                headers: { 'Content-Type': 'application/json' }
              }
            ));
          }, matched.delay || 0);
        });
      }
      
      return originalFetch(url, options);
    };
  }
}

步骤 2:在 React 项目中使用

jsx 复制代码
import { useEffect, useState } from 'react';
import axios from 'axios';
import SimpleMock from './mock';

// 创建 Mock 实例
const mock = new SimpleMock();

// 添加规则
mock.addRule({
  url: '/api/todos',
  method: 'GET',
  delay: 500, // 模拟 500ms 延迟
  response: () => ({
    code: 0,
    message: 'success'
    data: [
      { id: 1, title: '学习 Mock 原理', completed: false }
    ]
  })
});

mock.addRule({
  url: '/api/todos',
  method: 'POST',
  response: () => ({
    code: 0,
    message: 'success'
  })
});

function App() {
  const [todos, setTodos] = useState([]);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    // 测试 XHR 拦截
    axios.get('/api/todos')
      .then(res => {
        setTodos(res.data.data);
      })
      .catch(err => {
        setError('加载失败:' + err.message);
      });
    
    // 测试 fetch 拦截
    fetch('/api/todos', { 
      method: 'POST',
      headers: { 'Content-Type': 'application/json' }
    })
      .then(res => {
        if (!res.ok) throw new Error('请求失败');
        return res.json();
      })
      .then(data => console.log(data.message)) // 输出 "添加成功"
      .catch(err => console.error('POST 失败:', err));
  }, []);
  
  if (error) return <div style={{ color: 'red' }}>{error}</div>;
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}
export default App;

步骤 3:验证拦截效果

运行项目后,打开浏览器控制台的 Network 面板:

  1. 你会看到 /api/todos 的 GET 和 POST 请求,但仔细观察会发现:

    • 这些请求没有真实的网络交互(Size 可能显示为 "from memory cache")
    • 响应数据正是我们在规则中定义的模拟数据
    • GET 请求会有 500ms 的延迟(符合我们设置的 delay)
  2. 组件能正常渲染模拟的 todo 列表,证明拦截成功。

通过这个简易工具,我们完美复现了 Mock 拦截请求的核心流程,也验证了底层原理的正确性。

拦截时容易踩的那些坑

在使用 Mock 工具时,很多问题都源于对拦截原理理解不透彻,这里总结几个常见坑点:

1. 部分请求无法拦截?

  • 原因:如果请求库使用了特殊的请求方式(如 WebSocket),Mock 可能无法拦截;另外,部分浏览器插件或安全策略可能会恢复原生的 XHR/fetch 方法。
  • 解决:检查请求库的底层实现,确保基于 XHR 或 fetch;避免在生产环境引入 Mock 代码。

2. 模拟数据与真实接口格式不一致?

  • 原因:Mock 规则定义时没有严格遵循接口文档,导致联调时大量修改。
  • 解决:前后端共同维护接口文档(如 Swagger),Mock 规则严格按照文档生成;联调前用工具对比模拟数据与真实接口的差异。

3. 生产环境意外启用 Mock?

  • 原因:环境判断逻辑错误,导致生产环境仍加载了 Mock 代码。
  • 解决 :通过构建工具(Webpack/Vite)在生产打包时剔除 Mock 代码;使用 process.env.NODE_ENV 严格判断环境。

总结

Mock 拦截请求的技术原理并不复杂,理解拦截原理的价值,不仅在于能更好地使用工具,更在于当遇到问题时能快速定位根源。比如当请求突然不被拦截时,你能想到是规则匹配失败还是 XHR 被意外恢复;当数据格式不符时,你能意识到是 Mock 规则与接口文档不一致。

你在使用 Mock 时遇到过哪些印象深刻的问题?欢迎在评论区分享你的经历和解决方案~

相关推荐
Arvin62733 分钟前
Nginx IP授权页面实现步骤
服务器·前端·nginx
初遇你时动了情1 小时前
react/vue vite ts项目中,自动引入路由文件、 import.meta.glob动态引入路由 无需手动引入
javascript·vue.js·react.js
xw52 小时前
Trae安装指定版本的插件
前端·trae
默默地离开2 小时前
前端开发中的 Mock 实践与接口联调技巧
前端·后端·设计模式
南岸月明2 小时前
做副业,稳住心态,不靠鸡汤!我的实操经验之路
前端
嘗_2 小时前
暑期前端训练day7——有关vue-diff算法的思考
前端·vue.js·算法
伍哥的传说2 小时前
React 英语打地鼠游戏——一个寓教于乐的英语学习游戏
学习·react.js·游戏
MediaTea2 小时前
Python 库手册:html.parser HTML 解析模块
开发语言·前端·python·html
杨荧3 小时前
基于爬虫技术的电影数据可视化系统 Python+Django+Vue.js
开发语言·前端·vue.js·后端·爬虫·python·信息可视化
BD_Marathon3 小时前
IDEA中创建Maven Web项目
前端·maven·intellij-idea