1. 为什么前端开发离不开 Mock 数据做过前后端分离项目的人都有一个共同体会后端接口没写好前端页面就得干等着。尤其是现在主流的开发模式前端用 React、Vue、Angular 搭 SPA后端可能还在设计数据库表结构两边进度完全对不上。这时候 Mock 数据就是救命的——它让前端可以在真实接口就绪之前先把页面逻辑、交互流程、异常状态全部跑通。我最早接触 Mock 是在一个后台管理系统的项目里。当时后端同学说接口大概要两周才能联调但产品经理要求三天后看到可点击的原型。没办法只能自己造数据。最开始用的是最土的办法在代码里写死一个 JSON 对象直接 return 回去。但很快问题就来了——分页怎么办搜索过滤怎么办POST 提交后的状态变化怎么模拟写死的 JSON 根本不够用。后来开始用拦截请求的方式做 Mock核心思路就是在前端代码和真实网络层之间插一层“假服务”。当代码发起 GET 或 POST 请求时这层假服务先截住根据请求的 URL、方法、参数返回预先准备好的数据。这样前端代码完全不用改该发请求还是发请求只是请求没有真正发到后端服务器而是被本地拦截并返回了模拟结果。这套机制的价值在于三点第一前后端可以真正并行开发互不阻塞第二前端可以主动构造各种边界情况比如空列表、超长文本、网络超时、服务器 500 错误这些在真实后端上反而不好复现第三自动化测试可以依赖 Mock 数据保证每次运行结果一致不会因为后端数据变动导致测试失败。适合读这篇内容的人包括正在做前后端分离项目的前端开发者、需要写单元测试或集成测试的测试工程师、带学生做课程设计的老师、以及任何需要在不依赖真实服务器的前提下调试 HTTP 请求的人。不管你用的是原生 fetch、axios、jQuery.ajax 还是其他请求库Mock 的核心逻辑都是相通的。2. Mock 的核心机制与方案选型2.1 拦截请求的三种常见方式做 Mock 的本质就是“截胡”——在请求真正发出去之前把它拦住然后返回假数据。根据拦截发生的层次不同常见有三种方案。第一种是在业务代码里直接判断。比如封装一个 request 函数里面写if (isMock) return mockData。这种方式最原始优点是零依赖、零配置缺点是侵入性太强业务代码里到处是 mock 逻辑上线前还得一个个删干净容易漏。第二种是拦截 XMLHttpRequest 或 fetch。这是目前最主流的做法。XMLHttpRequest 是浏览器提供的原生对象所有基于它的请求库axios、jQuery.ajax 等最终都会调用它的 open、send 方法。我们可以在页面加载时把window.XMLHttpRequest替换成一个自定义的类在这个类里判断当前请求是否命中 Mock 规则命中就返回假数据不命中就调用原始的 XMLHttpRequest 发真实请求。fetch 也是类似直接替换window.fetch即可。第三种是在 Service Worker 层拦截。Service Worker 运行在浏览器后台可以拦截页面发出的所有网络请求包括静态资源、图片、API 请求。这种方式最干净业务代码完全无感知但配置相对复杂而且要求 HTTPS 环境localhost 除外。适合对 Mock 要求比较高的中大型项目。对于大多数日常开发场景第二种方式性价比最高。它不需要 HTTPS配置简单而且社区有成熟的库可以直接用。下面重点讲这种方案的实现细节。2.2 GET 与 POST 的 Mock 差异GET 和 POST 虽然都是 HTTP 请求但在 Mock 时的处理逻辑有明显区别。GET 请求的参数通常放在 URL 的查询字符串里比如/api/users?page1size10。Mock 时需要解析 URL提取查询参数然后根据参数决定返回哪一页数据、是否过滤、是否排序。GET 请求一般用于“读取”操作所以 Mock 返回的数据应该是幂等的——同样的参数每次请求都返回同样的结果。POST 请求的参数通常放在请求体里格式可能是 JSON、表单、或者 FormData。Mock 时需要读取请求体解析出提交的数据然后模拟“写入”操作。POST 的 Mock 要复杂一些因为写入操作往往有副作用比如提交成功后列表里应该多一条记录或者某个字段的状态应该改变。如果只是简单返回一个{code: 0, msg: success}虽然能跑通流程但没法验证“提交后列表刷新”这类逻辑。我一般的做法是在 Mock 层维护一个内存中的“数据库”GET 从这个数据库读POST 往这个数据库写。这样增删改查的闭环就能完整模拟出来。当然页面刷新后内存数据会重置但对于开发调试来说已经足够了。如果需要持久化可以把数据存到 localStorage 里刷新后从 localStorage 恢复。2.3 工具选型手写还是用库手写 Mock 拦截器并不难核心代码大概几十行。但实际项目中我更推荐用成熟的库原因有三第一库已经处理了各种边界情况比如 URL 匹配、请求体解析、跨域预检等第二库通常提供了更友好的 API比如用对象描述 Mock 规则可读性更好第三库会持续维护遇到浏览器兼容性问题时不用自己啃。常见的 Mock 库有 Mock.js、msw、json-server 等。Mock.js 偏重数据生成可以随机生成姓名、地址、日期等适合需要大量假数据的场景。msw 基于 Service Worker拦截最彻底适合对真实性要求高的项目。json-server 则是一个独立的命令行工具启动后就是一个完整的 REST API 服务器适合需要模拟完整后端行为的场景。选哪个取决于你的需求如果只是临时糊弄一下页面手写拦截器最快如果需要生成大量随机数据Mock.js 更省事如果项目较大、多人协作msw 或 json-server 更规范。下面我会以手写拦截器为主线讲解因为理解了原理之后用任何库都能快速上手。3. 手写一个可用的 Mock 拦截器3.1 核心代码结构先看整体结构。我们需要一个 Mock 规则表描述“什么请求返回什么数据”一个拦截器负责判断请求是否命中规则一个数据生成器负责根据规则生成响应。// mock-rules.js const mockRules [ { method: GET, url: /api/users, handler: (params) { const page parseInt(params.page) || 1; const size parseInt(params.size) || 10; const allUsers generateUsers(50); const start (page - 1) * size; return { code: 0, data: { list: allUsers.slice(start, start size), total: allUsers.length, page, size } }; } }, { method: POST, url: /api/users, handler: (body) { const newUser { id: Date.now(), ...body }; userDatabase.push(newUser); return { code: 0, msg: 创建成功, data: newUser }; } } ];规则表里每条规则包含三个字段method 表示 HTTP 方法url 表示匹配的路径handler 是一个函数接收解析后的参数返回响应数据。handler 的设计是关键——它把“生成什么数据”的逻辑从拦截器里抽离出来让规则表保持简洁。拦截器的核心逻辑是重写window.XMLHttpRequest在open方法里记录请求方法和 URL在send方法里判断是否命中规则。如果命中就模拟一个响应触发onreadystatechange和onload回调如果不命中就调用原始 XHR 发真实请求。// mock-interceptor.js const OriginalXHR window.XMLHttpRequest; function MockXHR() { this._method ; this._url ; this._async true; this._headers {}; this._body null; this.readyState 0; this.status 0; this.responseText ; this.onreadystatechange null; this.onload null; } MockXHR.prototype.open function(method, url, async) { this._method method.toUpperCase(); this._url url; this._async async ! false; this.readyState 1; if (this.onreadystatechange) this.onreadystatechange(); }; MockXHR.prototype.setRequestHeader function(key, value) { this._headers[key] value; }; MockXHR.prototype.send function(body) { this._body body; const rule findRule(this._method, this._url); if (rule) { // 命中 Mock 规则 const params parseParams(this._url, this._body); const result rule.handler(params); this._respond(200, JSON.stringify(result)); } else { // 未命中走真实请求 this._realXHR new OriginalXHR(); // 把当前对象的回调代理到真实 XHR 上 // ... 省略代理代码 } }; MockXHR.prototype._respond function(status, text) { this.readyState 4; this.status status; this.responseText text; if (this.onreadystatechange) this.onreadystatechange(); if (this.onload) this.onload(); }; window.XMLHttpRequest MockXHR;这段代码是简化版实际使用时还需要处理responseType、timeout、abort、getAllResponseHeaders等方法否则某些请求库会报错。但核心思路已经清楚了用自定义类替换原生类在 send 时判断是否命中规则。3.2 URL 匹配与参数解析URL 匹配不能简单用字符串相等来判断因为实际请求的 URL 可能带查询字符串也可能带路径参数。比如规则里写的是/api/users但实际请求可能是/api/users?page1或/api/users/123。我的做法是分两步先提取路径部分去掉?后面的内容再判断是否匹配。对于路径参数可以用正则表达式来匹配。比如规则/api/users/:id可以转换成正则/api/users/([^/])匹配后把捕获组作为参数传给 handler。function matchUrl(pattern, actualUrl) { const path actualUrl.split(?)[0]; const patternParts pattern.split(/); const pathParts path.split(/); if (patternParts.length ! pathParts.length) return null; const params {}; for (let i 0; i patternParts.length; i) { if (patternParts[i].startsWith(:)) { params[patternParts[i].slice(1)] pathParts[i]; } else if (patternParts[i] ! pathParts[i]) { return null; } } return params; }查询字符串的解析用URLSearchParams最方便一行代码就能把?page1size10转成对象。请求体的解析要看 Content-Type如果是application/json直接JSON.parse如果是application/x-www-form-urlencoded用URLSearchParams解析如果是 FormData需要遍历 entries。注意解析请求体时一定要加 try-catch因为有些请求的 body 可能为空或者格式不符合预期。我遇到过因为 body 解析失败导致整个页面白屏的情况排查了半天才发现是 Mock 拦截器抛了异常。3.3 模拟网络延迟与异常状态真实的网络请求不是瞬间返回的总会有延迟。如果 Mock 数据瞬间返回可能会掩盖一些时序相关的问题。比如页面在请求发出后、响应返回前显示 loading 状态如果 Mock 瞬间返回loading 状态可能一闪而过根本看不到。所以 Mock 时最好加上一个随机延迟比如 200 到 800 毫秒。function delay(min, max) { const ms Math.floor(Math.random() * (max - min 1)) min; return new Promise(resolve setTimeout(resolve, ms)); }异常状态也很重要。真实后端可能返回 400、401、403、404、500 等各种状态码前端需要针对不同状态码做不同处理。Mock 时应该能主动触发这些异常验证前端的错误处理逻辑是否完善。我的做法是在规则表里加一个status字段默认是 200需要测试异常时改成其他值。还有一种情况是网络超时。可以在 Mock 拦截器里模拟一个永不返回的请求或者延迟很久才返回看看前端的超时处理是否生效。这个在测试请求重试逻辑时特别有用。4. 完整实操从零搭建一个 Mock 环境4.1 项目初始化与文件组织假设我们有一个 Vue 或 React 项目目录结构大概是这样的src/ api/ user.js mock/ index.js rules/ user.js utils.js main.jsmock/index.js是入口文件负责安装拦截器、加载所有规则。mock/rules/目录下按模块拆分规则文件比如user.js放用户相关的 Mock 规则order.js放订单相关的规则。mock/utils.js放公共工具函数比如延迟、参数解析、数据生成等。在main.js里根据环境变量决定是否启用 Mockif (process.env.NODE_ENV development) { require(./mock); }这样生产环境打包时Mock 代码会被 tree-shaking 掉不会增加包体积。4.2 编写用户模块的 Mock 规则以用户管理为例常见的接口有获取用户列表GET、获取用户详情GET、创建用户POST、更新用户POST 或 PUT、删除用户POST 或 DELETE。// mock/rules/user.js import { generateUsers } from ../utils; let userDatabase generateUsers(50); export default [ { method: GET, url: /api/users, handler: (params) { const page parseInt(params.page) || 1; const size parseInt(params.size) || 10; const keyword params.keyword || ; let filtered userDatabase; if (keyword) { filtered userDatabase.filter(u u.name.includes(keyword) || u.email.includes(keyword) ); } const start (page - 1) * size; return { code: 0, data: { list: filtered.slice(start, start size), total: filtered.length, page, size } }; } }, { method: GET, url: /api/users/:id, handler: (params) { const user userDatabase.find(u u.id parseInt(params.id)); if (!user) { return { code: 404, msg: 用户不存在 }; } return { code: 0, data: user }; } }, { method: POST, url: /api/users, handler: (body) { const newUser { id: Date.now(), name: body.name, email: body.email, status: active, createdAt: new Date().toISOString() }; userDatabase.unshift(newUser); return { code: 0, msg: 创建成功, data: newUser }; } }, { method: POST, url: /api/users/:id/delete, handler: (params) { const index userDatabase.findIndex(u u.id parseInt(params.id)); if (index -1) { return { code: 404, msg: 用户不存在 }; } userDatabase.splice(index, 1); return { code: 0, msg: 删除成功 }; } } ];注意这里用let userDatabase而不是const因为 POST 操作会修改这个数组。generateUsers函数负责生成初始数据可以写死也可以用 Mock.js 随机生成。4.3 数据生成器的实现数据生成器要保证每次生成的假数据看起来“像真的”。姓名、邮箱、地址、日期这些字段如果全是test1、test2看起来就很假调试时也不容易发现问题。// mock/utils.js const firstNames [张, 李, 王, 刘, 陈, 杨, 赵, 黄]; const lastNames [伟, 芳, 娜, 敏, 静, 磊, 军, 洋]; const domains [example.com, test.org, demo.net]; export function generateUsers(count) { const users []; for (let i 0; i count; i) { const firstName firstNames[Math.floor(Math.random() * firstNames.length)]; const lastName lastNames[Math.floor(Math.random() * lastNames.length)]; const name firstName lastName; const email user${i 1}${domains[i % domains.length]}; users.push({ id: i 1, name, email, status: Math.random() 0.2 ? active : inactive, createdAt: new Date(Date.now() - Math.random() * 365 * 24 * 3600 * 1000).toISOString() }); } return users; }这个生成器有几个细节邮箱用user1example.com这种格式方便识别状态按 80% 概率为 active20% 为 inactive这样列表里既有正常状态也有异常状态创建时间随机分布在过去一年内排序后看起来更自然。4.4 在请求库中接入 Mock如果项目用的是 axiosaxios 默认使用 XMLHttpRequest所以替换window.XMLHttpRequest后 axios 会自动走 Mock 逻辑。但要注意axios 可能会检查XMLHttpRequest的一些属性比如withCredentials、responseType等自定义类需要把这些属性也补上否则会报错。如果项目用的是 fetch需要替换window.fetch。fetch 的拦截比 XHR 简单一些因为 fetch 返回的是 Promise直接返回一个构造好的 Response 对象即可。const originalFetch window.fetch; window.fetch function(url, options {}) { const method (options.method || GET).toUpperCase(); const rule findRule(method, url); if (rule) { const params parseParams(url, options.body); const result rule.handler(params); return Promise.resolve(new Response(JSON.stringify(result), { status: 200, headers: { Content-Type: application/json } })); } return originalFetch.apply(this, arguments); };提示替换 fetch 时要注意保留原始 fetch 的 this 上下文否则某些情况下会报错。用originalFetch.apply(this, arguments)比直接originalFetch(url, options)更安全。5. 常见问题与排查技巧实录5.1 Mock 不生效的几种原因Mock 不生效是最常见的问题表现是请求还是发到了真实后端或者直接报跨域错误。根据我的经验原因通常有这几类第一拦截器安装时机太晚。如果拦截器是在某个组件挂载后才安装的那么组件挂载时发出的请求就不会被拦截。正确的做法是在应用入口文件的最顶部安装拦截器确保在任何业务代码执行之前完成替换。第二请求库没有使用 XMLHttpRequest 或 fetch。有些请求库比如早期的 superagent可能使用了其他底层实现或者在某些环境下使用了 Node.js 的 http 模块。这种情况下替换window.XMLHttpRequest是无效的。解决办法是查看请求库的文档确认它使用的底层 API然后针对性地拦截。第三URL 匹配规则写错了。比如规则里写的是/api/users但实际请求的是/api/user或者带了 baseURL 前缀。我一般会在拦截器里加一行console.log把每个请求的方法和 URL 打印出来对照规则表检查。第四跨域预检请求没有被拦截。对于非简单请求比如带自定义 header 的 POST浏览器会先发一个 OPTIONS 请求做预检。如果 OPTIONS 请求没有被 Mock 拦截就会发到真实后端导致跨域错误。解决办法是在拦截器里也处理 OPTIONS 请求直接返回 204 状态码和必要的 CORS 头。5.2 POST 请求体解析失败POST 请求的 body 格式多种多样解析时容易出错。常见的情况有body 是 JSON 字符串但 Content-Type 没设置对body 是 FormData 但按 JSON 解析了body 为空但代码没有做空值判断。我的做法是写一个健壮的解析函数先看 Content-Type再看 body 的实际类型function parseBody(body, contentType) { if (!body) return {}; if (typeof body string) { if (contentType contentType.includes(application/json)) { try { return JSON.parse(body); } catch (e) { return {}; } } if (contentType contentType.includes(application/x-www-form-urlencoded)) { return Object.fromEntries(new URLSearchParams(body)); } return { raw: body }; } if (body instanceof FormData) { const obj {}; for (const [key, value] of body.entries()) { obj[key] value; } return obj; } return body; }这个函数覆盖了 JSON、表单、FormData 三种常见格式并且对解析失败做了兜底。实际使用时Content-Type 可能带字符集比如application/json; charsetutf-8所以用includes而不是来判断。5.3 内存数据库刷新后丢失前面提到Mock 数据存在内存里页面刷新后会重置。这在开发时通常不是问题但有些场景下需要保持状态比如测试“提交表单后刷新页面数据是否还在”。解决办法是把内存数据库同步到 localStorage。function loadDatabase(key, defaultData) { const stored localStorage.getItem(key); if (stored) { try { return JSON.parse(stored); } catch (e) { return defaultData; } } return defaultData; } function saveDatabase(key, data) { localStorage.setItem(key, JSON.stringify(data)); }每次 POST 操作后调用saveDatabase初始化时调用loadDatabase。这样刷新页面后数据还在更接近真实后端的行为。但要注意localStorage 有大小限制通常 5MB数据量太大时会写入失败需要加 try-catch。5.4 常见问题速查表问题现象可能原因排查方法解决方案请求发到了真实后端拦截器未安装或安装太晚在拦截器入口打印日志移到应用入口最顶部报跨域错误OPTIONS 预检未拦截查看 Network 面板是否有 OPTIONS 请求拦截器处理 OPTIONS 返回 204POST 参数解析为空Content-Type 不匹配打印 Content-Type 和 body用健壮的解析函数页面白屏拦截器抛异常查看控制台报错加 try-catch 兜底刷新后数据丢失内存数据库未持久化检查是否用了 localStorage同步到 localStorage某些请求库不生效底层未用 XHR/fetch查看请求库文档替换对应的底层 API注意Mock 代码千万不要提交到生产环境。我见过有项目把 Mock 规则打包上线了结果用户看到的是假数据排查了半天才发现是 Mock 没关。一定要用环境变量控制并且在生产构建时确保 Mock 代码被完全移除。6. 进阶技巧让 Mock 更接近真实后端6.1 模拟分页、排序、过滤的组合逻辑真实后端的列表接口通常支持分页、排序、过滤三个参数的组合。Mock 时如果只处理分页不处理排序和过滤前端开发时就没法验证这些功能。我的做法是在 handler 里按顺序处理先过滤再排序最后分页。handler: (params) { let result [...userDatabase]; // 过滤 if (params.keyword) { result result.filter(u u.name.includes(params.keyword)); } if (params.status) { result result.filter(u u.status params.status); } // 排序 if (params.sortBy) { const order params.order desc ? -1 : 1; result.sort((a, b) { if (a[params.sortBy] b[params.sortBy]) return -1 * order; if (a[params.sortBy] b[params.sortBy]) return 1 * order; return 0; }); } // 分页 const page parseInt(params.page) || 1; const size parseInt(params.size) || 10; const start (page - 1) * size; return { code: 0, data: { list: result.slice(start, start size), total: result.length, page, size } }; }这个顺序很重要过滤会改变总条数排序会改变顺序分页依赖前两步的结果。如果顺序错了分页数据就会不对。6.2 模拟文件上传与进度文件上传是前端开发中的一个难点Mock 时也需要特殊处理。上传接口通常是 POSTContent-Type 是multipart/form-databody 是 FormData。Mock 拦截器需要能解析 FormData提取出文件信息。if (body instanceof FormData) { const file body.get(file); if (file instanceof File) { // 模拟上传成功返回文件 URL return { code: 0, data: { url: /uploads/${Date.now()}_${file.name}, size: file.size, type: file.type } }; } }如果需要模拟上传进度XHR 的upload.onprogress事件需要被触发。这个在自定义 XHR 里实现起来比较麻烦需要手动构造 ProgressEvent 并按时间间隔触发。如果项目对上传进度要求不高可以跳过这个模拟如果要求高建议直接用 msw它对上传进度的支持更好。6.3 与自动化测试的结合Mock 数据在自动化测试中价值很大。单元测试时可以用 Mock 拦截器模拟各种接口返回测试组件的渲染逻辑。集成测试时可以用 Mock 模拟完整的业务流程比如“登录 - 获取用户信息 - 提交订单”。我的做法是把 Mock 规则导出成一个独立的模块测试文件里按需加载。比如测试登录失败的情况就加载一个返回 401 的规则测试登录成功就加载返回 token 的规则。这样同一套组件代码可以在不同 Mock 规则下测试多种场景。// 测试登录失败 import { setupMock } from ../mock; setupMock([ { method: POST, url: /api/login, handler: () ({ code: 401, msg: 用户名或密码错误 }) } ]);这种按需加载的方式比全局 Mock 更灵活测试用例之间不会互相干扰。6.4 性能优化避免 Mock 拖慢开发服务器Mock 拦截器是在浏览器端运行的对开发服务器的性能没有直接影响。但如果 Mock 规则很多每次请求都要遍历规则表可能会有性能问题。优化方法是用 Map 按 method path 建立索引查找时直接命中不用遍历。const ruleMap new Map(); mockRules.forEach(rule { const key ${rule.method}:${rule.url}; ruleMap.set(key, rule); }); function findRule(method, url) { const path url.split(?)[0]; // 先尝试精确匹配 const exact ruleMap.get(${method}:${path}); if (exact) return exact; // 再尝试路径参数匹配 for (const rule of mockRules) { if (rule.method ! method) continue; if (matchUrl(rule.url, path)) return rule; } return null; }精确匹配用 MapO(1) 复杂度路径参数匹配才遍历但这类规则通常不多。这样即使规则表有几百条查找也很快。7. 我踩过的坑与实操心得第一个坑是拦截器安装顺序。有一次项目里用了某个 UI 库它在初始化时就发了一个请求获取配置。我的 Mock 拦截器是在 UI 库之后安装的结果那个请求没被拦截直接发到了后端报了跨域错误。后来把 Mock 安装代码移到了入口文件的第一行问题解决。这个教训是Mock 拦截器必须尽早安装越早越好。第二个坑是responseType 的处理。有些请求设置了responseType: blob用于下载文件我的自定义 XHR 没有处理这个属性导致下载功能在 Mock 环境下报错。解决办法是在_respond方法里判断responseType如果是 blob 就返回 Blob 对象如果是 json 就返回 JSON 字符串。第三个坑是循环引用。Mock 规则里引用了数据生成器数据生成器又引用了规则里的某些常量导致循环引用打包时报错。解决办法是把公共常量抽到一个独立的文件里规则和生成器都从这个文件导入避免互相引用。第四个坑是Mock 数据太假。最开始我用test1、test2作为用户名结果前端做搜索功能时搜“test”能搜出一堆搜其他关键词一个都搜不到根本没法验证搜索逻辑。后来改成随机生成中文姓名搜索功能才真正能测。所以 Mock 数据一定要尽量接近真实数据的分布。第五个坑是忘记关闭 Mock。有一次打包上线时忘了关 Mock用户看到的全是假数据客服电话被打爆。后来在构建脚本里加了一个检查如果生产环境检测到 Mock 代码直接构建失败。这个检查很简单就是在 Mock 入口文件里加一行if (process.env.NODE_ENV production) throw new Error(Mock 代码不应出现在生产环境);。最后分享一个小技巧在 Mock 拦截器里加一个全局开关通过 URL 参数控制是否启用 Mock。比如?mockoff就关闭 Mock走真实后端。这样在开发时如果怀疑是 Mock 数据的问题可以快速切换验证不用改代码重新编译。const mockEnabled !window.location.search.includes(mockoff); if (mockEnabled) { // 安装拦截器 }这个开关在联调阶段特别有用前端开发时默认开 Mock后端接口好了之后加个参数就能切到真实接口两边对比着调试效率很高。