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?page=1&size=10。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?page=1或/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最方便,一行代码就能把?page=1&size=10转成对象。请求体的解析要看 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里,根据环境变量决定是否启用 Mock:
if (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; }这个生成器有几个细节:邮箱用user1@example.com这种格式,方便识别;状态按 80% 概率为 active,20% 为 inactive,这样列表里既有正常状态也有异常状态;创建时间随机分布在过去一年内,排序后看起来更自然。
4.4 在请求库中接入 Mock
如果项目用的是 axios,axios 默认使用 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; charset=utf-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 返回 204 |
| POST 参数解析为空 | 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 时也需要特殊处理。上传接口通常是 POST,Content-Type 是multipart/form-data,body 是 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; }精确匹配用 Map,O(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。比如?mock=off就关闭 Mock,走真实后端。这样在开发时如果怀疑是 Mock 数据的问题,可以快速切换验证,不用改代码重新编译。
const mockEnabled = !window.location.search.includes('mock=off'); if (mockEnabled) { // 安装拦截器 }这个开关在联调阶段特别有用:前端开发时默认开 Mock,后端接口好了之后,加个参数就能切到真实接口,两边对比着调试,效率很高。