首页 / 资讯中心 / 文章详情

前端接口模拟实战:从 mock 数据到契约驱动开发

前端接口模拟实战:从 mock 数据到契约驱动开发 ★ FEATURED ARTICLE
1. 为什么前端要自己“造”后端数据这事儿比你想象的更日常“前端模拟后端接口返回数据”——听起来像一句技术黑话但其实它每天都在真实项目里发生。我带过的几个团队里几乎每个新成员入职第一周都会被导师拉到电脑前说“先别连真实后端咱们先把 mock 数据跑通。”这不是偷懒而是工程现实后端接口还没写完、联调环境不稳定、测试数据难构造、甚至只是想快速验证一个 UI 交互逻辑是否合理……这时候等就是最大的成本。核心关键词就三个前端、模拟、接口返回数据。它不涉及服务器部署不依赖数据库也不需要后端同学配合——所有动作都发生在浏览器或本地开发服务中。本质是用前端代码伪造出符合真实 API 契约URL、Method、Request/Response 结构、状态码的响应体。它不是“假数据”而是“契约级真数据”字段名、嵌套层级、类型、枚举值、分页结构全都和线上接口一模一样。我见过最典型的场景是UI 团队在设计评审通过后当天就用 mock 数据把整套列表页详情页弹窗交互跑通了而此时后端接口文档才刚定稿接口代码还没提交第一行。适合谁看三类人最常需要一是刚接手老项目的新人面对几十个未文档化的接口靠 mock 先“摸清脉络”二是独立开发者一个人包揽前后端mock 就是你的临时后端三是测试工程师需要稳定复现特定错误码比如 401 未登录、503 服务降级、边界数据空数组、超长字符串、时间戳为 null来验证前端容错逻辑。它不解决生产问题但能极大压缩开发闭环周期——从“等后端”变成“我自己先跑起来”。实测下来一个中等复杂度的管理后台用好 mock 方案前期开发效率能提升 40% 以上而且上线前回归测试的稳定性明显更高因为所有测试用例都基于同一份契约数据。2. 四种主流方案深度对比选哪一种取决于你卡在哪一关选 mock 方案从来不是“哪个最新潮就用哪个”而是看当前项目卡点在哪。我踩过太多坑有团队盲目上 Webpack DevServer 的 proxy结果发现跨域能解但 POST 请求体解析失败也有团队硬上 Mock.js结果发现它生成的随机数据根本不符合业务规则比如用户 ID 居然生成负数。下面这四种方案是我过去十年在不同规模项目里反复验证过的按适用场景从轻量到重载排列2.1 浏览器控制台临时打桩快得像呼吸但只适合“秒级验证”这是最原始也最有效的方法直接在 Chrome 控制台里覆盖fetch或XMLHttpRequest。比如你要验证一个用户列表接口/api/users可以这样写// 临时覆盖 fetch const originalFetch window.fetch; window.fetch function(input, init) { if (input /api/users init?.method GET) { return Promise.resolve({ ok: true, status: 200, json: () Promise.resolve({ code: 0, data: [ { id: 1, name: 张三, role: admin }, { id: 2, name: 李四, role: user } ] }) }); } return originalFetch(input, init); };提示这种方式零配置、零依赖改完立刻生效特别适合调试某个按钮点击后触发的单次请求。但它有个致命缺陷刷新页面就失效且无法处理请求头校验、POST 参数解析等复杂逻辑。我一般只用它来“探路”——确认这个接口调用路径是否正确或者快速看一眼 UI 渲染效果。2.2 本地 JSON 文件 开发服务器代理稳如老狗适合中小项目主力方案这是目前我推荐给 80% 中小团队的首选。原理极简把模拟数据写成标准 JSON 文件如mock/users.json然后让本地开发服务器Vite/Webpack/Next.js 等把/api/*路径的请求代理到这些静态文件上。以 Vite 为例在vite.config.ts中加几行export default defineConfig({ server: { proxy: { /api: { target: http://localhost:5173, // 指向自己 rewrite: (path) path.replace(/^\/api/, /mock), // 把 /api/users 变成 /mock/users changeOrigin: true, } } } });然后在项目根目录建mock/文件夹放users.json、orders.json等。访问/api/users时Vite 会自动读取mock/users.json并返回。优势在于数据结构完全可控你手写不是随机生成支持完整的 HTTP 状态码、响应头设置比如加Content-Type: application/json还能用fs.readFileSync在代理层做简单逻辑如根据 query 参数返回不同数据。我带的一个电商后台项目初期 30 个接口全靠这套方案支撑了两个月直到后端交付。2.3 Mock Service WorkerMSW前端测试的黄金标准渐进式增强首选MSW 是目前最接近“生产级 mock”的方案。它不代理请求而是用 Service Worker 拦截浏览器发出的所有网络请求并按预设规则返回响应。这意味着它能在真实浏览器环境、真实网络条件下工作连navigator.onLine都能精准模拟离线状态。配置方式非常声明式// mock/handlers.ts import { rest } from msw; export const handlers [ rest.get(/api/users, (req, res, ctx) { return res( ctx.status(200), ctx.json({ code: 0, data: [{ id: 1, name: 王五, email: ww.com }] }) ); }), rest.post(/api/login, (req, res, ctx) { const { username } await req.json(); return username admin ? res(ctx.status(200), ctx.json({ token: xxx })) : res(ctx.status(401), ctx.json({ message: 密码错误 })); }) ];注意MSW 必须在开发环境和测试环境分别启动setupWorker用于浏览器setupServer用于 Jest。它的学习曲线略陡但一旦掌握就能实现“一套 mock 规则三处复用”开发时拦截、单元测试时复用、E2E 测试时复用。我们团队用 MSW 后前端测试覆盖率从 35% 直接干到 72%因为所有异步请求都能被精准控制。2.4 专用 Mock 服务器如 MirageJS、JSON Server当你要“假装自己是后端”这类方案本质是启动一个微型 Node.js 服务它监听某个端口如http://localhost:3001并提供 RESTful 接口。以 JSON Server 为例只需一个db.json{ users: [ { id: 1, name: 赵六, email: zz.com } ], posts: [ { id: 1, title: Hello World, userId: 1 } ] }执行npx json-server --watch db.json --port 3001它就自动生成/users、/posts等接口还支持_limit、_page分页参数。优势是完全独立于前端框架后端同学也能直接访问调试支持 CRUD 全操作甚至能模拟数据库外键关联。但代价是多启一个进程开发环境变复杂数据变更需重启服务无法精细控制响应头和错误码。我只在两类场景用它一是需要多人协作调试比如前端、测试、产品一起看一个 mock 接口二是要做压力测试前的数据准备。3. 实操细节拆解从写第一行 mock 到覆盖 90% 场景光知道方案不够真正卡住人的永远是细节。下面这些都是我在项目里手把手教新人时他们问得最多、也最容易翻车的地方。3.1 数据结构怎么写才“像真的”别再用随机字符串糊弄自己很多新手 mock 数据喜欢用Mock.js的Random.cname()生成“张三”“李四”看起来很热闹但一到真实业务就露馅。比如一个订单列表接口后端返回的status字段是数字枚举1-待支付2-已发货3-已完成你 mock 时如果写成status: Random.integer(1, 5)就可能生成非法值 4 或 5导致前端 switch-case 报错。正确做法是严格对照后端 Swagger 文档或接口契约用 TypeScript Interface 定义数据结构再手写符合规则的实例。以用户信息为例先定义类型// types/api.ts export interface User { id: number; name: string; email: string; role: admin | editor | user; // 枚举值必须明确 lastLoginAt: string; // ISO 8601 格式不是 Date 对象 avatar?: string; // 可选字段要标 ? }再写 mock 数据mock/users.json{ code: 0, message: success, data: { list: [ { id: 1001, name: 陈七, email: ccompany.com, role: admin, lastLoginAt: 2024-03-15T08:22:33Z, avatar: https://ui-avatars.com/api/?nameChenQi } ], total: 1 } }实操心得我要求团队所有 mock 数据必须通过JSON Schema校验。用ajv库写个简单脚本每次提交前自动校验mock/*.json是否符合schema/user.json。这招让我们在早期就发现了 7 次字段类型错误比如把number写成123字符串避免了联调时的扯皮。3.2 如何模拟“不那么顺利”的情况错误码和边界值才是试金石一个只返回200 OK的 mock等于没 mock。真实世界里网络会断、token 会过期、参数会错、数据库会挂。我总结了前端必须覆盖的 5 类异常场景以及对应 mock 写法异常类型HTTP 状态码mock 关键点实际价值认证失败401响应体含{ code: 401, message: 登录已过期 }且不设Authorization响应头验证登录态跳转逻辑权限不足403返回403但响应体结构与正常一致code: 403前端据此隐藏按钮测试 RBAC 权限控制是否生效参数错误400响应体含errors: [{ field: email, message: 邮箱格式不正确 }]驱动表单校验提示显示服务不可用503响应头加Retry-After: 60模拟服务降级验证前端 loading 状态和重试机制空数据200data.list: []total: 0而非data: null确保列表页“暂无数据”提示正常渲染以 401 为例用 MSW 写法rest.get(/api/profile, (req, res, ctx) { // 检查请求头是否有有效 token const auth req.headers.get(Authorization); if (!auth || !auth.startsWith(Bearer )) { return res( ctx.status(401), ctx.json({ code: 401, message: 登录已过期请重新登录 }) ); } return res(ctx.json({ /* 正常数据 */ })); });注意事项千万别在 mock 里写if (Math.random() 0.9) return errorRes这种随机错误。测试需要可重现性错误必须由可控条件触发如特定 URL 参数、特定请求头、特定 cookie。3.3 动态响应怎么做URL 参数、请求体、Header 都是你的开关静态 JSON 文件只能应付 GET 请求。一旦遇到 POST 提交表单、PUT 更新资源、带分页参数的列表就必须动态生成响应。这里的关键是把请求的“输入特征”作为 mock 的决策依据。URL 参数比如/api/orders?page2limit10需要根据page和limit计算返回哪一段数据。用 Vite 代理时可在vite.config.ts的 proxy 里加函数proxy: { /api: { target: http://localhost:5173, rewrite: (path) { const url new URL(path, http://a.com); const page Number(url.searchParams.get(page)) || 1; const limit Number(url.searchParams.get(limit)) || 10; // 把参数存到全局供后续读取 globalThis.mockParams { page, limit }; return path.replace(/^\/api/, /mock); } } }然后在mock/orders.json的读取逻辑里用globalThis.mockParams拿到参数切片返回。请求体BodyPOST/PUT 的 JSON 数据是最大难点。MSW 天然支持req.json()解析但 Vite 代理默认不解析 body。解决方案是用connect中间件在代理层提前解析import { json } from body-parser; export default defineConfig({ server: { middlewareMode: true, proxy: { /api: { target: http://localhost:5173, configure: (app) { app.use(json()); // 解析 JSON body } } } } });请求头Header这是模拟多租户、灰度发布、A/B 测试的利器。比如后端通过X-Tenant-ID头区分客户数据mock 时就可以rest.get(/api/dashboard, (req, res, ctx) { const tenantId req.headers.get(X-Tenant-ID); if (tenantId tenant-a) { return res(ctx.json({ revenue: 120000 })); } else { return res(ctx.json({ revenue: 85000 })); } });4. 避坑指南那些没人告诉你、但会让你加班到凌晨的细节以下这些全是血泪教训。有些坑我踩过两次第三次就写进团队规范里了。4.1 “跨域”不是万能解药CORS 头缺失会导致 Fetch API 直接报错很多人以为只要开了代理跨域就解决了。错。代理只是把请求转发到另一个地址但最终响应头里如果没有Access-Control-Allow-Origin: *浏览器的 Fetch API 依然会抛出TypeError: Failed to fetch。这是因为 Fetch 的错误处理机制网络层错误如 DNS 失败和 CORS 错误都统一抛TypeError但后者没有详细信息极难排查。解决方案有两个简单粗暴在代理配置里强制添加响应头。Vite 示例proxy: { /api: { target: http://localhost:3001, changeOrigin: true, configure: (proxy, options) { proxy.on(proxyRes, (proxyRes, req, res) { res.setHeader(Access-Control-Allow-Origin, *); res.setHeader(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS); res.setHeader(Access-Control-Allow-Headers, Content-Type, Authorization, X-Requested-With); }); } } }一劳永逸用 MSW。它运行在 Service Worker 层完全绕过浏览器的 CORS 检查因为请求根本没发到网络层。实操心得我现在的习惯是只要项目用 TypeScript就在src/mock/index.ts里写一个enableMock()函数里面检查import.meta.env.DEV和一个全局开关如window.__ENABLE_MOCK__ true只有两者都满足才启动 MSW。这样既能保证开发时可用又杜绝了误打包到生产环境的风险。4.2 时间戳和日期格式一个new Date().toISOString()救你全家后端返回的时间字段99% 是 ISO 8601 格式字符串如2024-03-15T08:22:33Z而不是 JavaScript 的Date对象。但很多新手 mock 时直接写lastLoginAt: new Date()结果前端moment(lastLoginAt).format(YYYY-MM-DD)报错因为new Date()是对象不是字符串。正确写法永远是服务端风格lastLoginAt: new Date().toISOString()生成 UTC 时间字符串本地时区风格lastLoginAt: new Date().toLocaleString(zh-CN)但不推荐因格式不统一更稳妥的做法是写一个工具函数专门生成符合后端契约的日期// utils/date.ts export const mockDate (offsetDays: number 0): string { const d new Date(); d.setDate(d.getDate() offsetDays); return d.toISOString(); // 保证格式绝对正确 }; // mock/users.json 里调用 { lastLoginAt: 2024-03-15T08:22:33Z, createdAt: 2024-01-01T00:00:00Z }4.3 Mock 数据的生命周期管理别让“测试数据”污染你的“开发数据”这是高级坑。比如你在 mock 里写了一个创建用户的 POST 接口每次调用都往内存数组里 push 一条新用户。开发几天后列表页显示几百条“张三”而真实后端数据库里只有 3 条。这会导致两个问题一是前端分页逻辑错乱mock 总数 vs 真实总数二是团队成员互相干扰A 同学 mock 了 100 条数据B 同学看到后以为这是真实数据量。我的解决方案是所有可变数据增删改必须基于“种子数据” “确定性算法”生成。比如用户列表种子数据是固定的 10 条// mock/seeds/users.json [ { id: 1, name: 种子用户1 }, { id: 2, name: 种子用户2 } ]而 POST/api/users的 mock 逻辑是读取种子数据取最后一条的id加 1 生成新id名字用种子用户${id}这样每次重启 mock 服务数据都是一致的。MSW 的setupServer在测试中就天然支持这种模式因为它是纯内存的每次 test case 执行前重置。4.4 生产环境“幽灵开关”如何确保 mock 绝对不进线上最恐怖的事某天早上产品经理说“首页白屏了”你一看控制台满屏MSW: Mocking is enabled。这是因为有人把worker.start()写在了main.ts里没加环境判断。终极保险方案是三层防护编译时剔除用import.meta.env.PROD包裹启动逻辑运行时开关加一个 URL 参数?mockon只在带此参数时启动构建产物扫描在 CI 流程里加一步用grep -r msw dist/如果命中则失败。我们团队的main.ts是这样写的if (import.meta.env.DEV location.search.includes(mockon)) { const { worker } await import(./mock/browser); worker.start({ onUnhandledRequest: bypass }); }这样只有开发环境 显式开启mock 才生效。上线构建时import.meta.env.DEV为false整个代码块被 Tree Shaking 掉连字节都不会进生产包。5. 进阶技巧让 mock 成为你项目的“活文档”和“质量守门员”做到上面你已经是个合格的 mock 使用者。但真正的高手会把 mock 升级为项目资产。5.1 用 mock 自动生成接口文档Swagger 不是后端的专利我们团队现在有一个约定所有新增接口必须先写 mock 数据和 MSW handler再写后端代码。因为 mock 文件本身就是最精确的接口契约。我们用swagger-jsdoc工具从 MSW 的handlers.ts里自动提取注释生成 Swagger JSON/** * openapi * /api/users: * get: * summary: 获取用户列表 * responses: * 200: * description: 成功 * content: * application/json: * schema: * $ref: #/components/schemas/UserListResponse * components: * schemas: * UserListResponse: * type: object * properties: * code: { type: integer } * data: { $ref: #/components/schemas/UserListData } */ rest.get(/api/users, ...);执行npx swagger-jsdoc -d swagger-config.js -o docs/swagger.json就生成了标准 OpenAPI 文档。前端、测试、产品所有人看的都是同一份实时更新的文档。这比“后端写完再补文档”高效十倍。5.2 Mock 驱动的自动化测试让 80% 的接口逻辑在 CI 里跑通我们把 MSW 的setupServer和 Jest 深度集成。每个 API 调用的测试用例都包含三部分Arrange用server.use(handler)设置本次测试的 mock 行为Act调用真实的业务函数如await api.getUser(123)Assert断言返回值、调用次数、甚至请求参数。test(getUser 返回用户详情, async () { server.use( rest.get(/api/users/123, (req, res, ctx) { expect(req.url.searchParams.get(include)).toBe(profile); // 断言请求参数 return res(ctx.json({ id: 123, name: 测试用户 })); }) ); const user await api.getUser(123, { include: profile }); expect(user.name).toBe(测试用户); });这套方案让我们的接口层单元测试覆盖率常年保持在 95%而且每次 PR 提交CI 都会自动跑一遍任何破坏接口契约的修改比如改了字段名、删了必填字段都会立刻失败。5.3 Mock 数据版本化当你的项目需要支持多个后端版本大型项目常面临一个问题前端要同时对接 v1旧版和 v2新版后端。这时mock 不能只有一套。我们的做法是按后端版本号组织 mock 目录。mock/ ├── v1/ │ ├── users.json │ └── orders.json ├── v2/ │ ├── users.json // 字段更多结构更深 │ └── orders.json └── index.ts // 根据 import.meta.env.VUE_APP_API_VERSION 加载对应版本index.ts里const version import.meta.env.VUE_APP_API_VERSION || v1; const handlers await import(./${version}/handlers.ts).then(m m.handlers); worker.start({ serviceWorkerUrl: /mockServiceWorker-${version}.js });这样切换后端版本只需改一个环境变量mock 数据自动适配。我们曾用这套方案支撑了为期三个月的 v1/v2 并行上线期零接口兼容性事故。6. 我的个人体会mock 不是“将就”而是“掌控力”的体现写完这篇我翻了下自己最近三年的项目笔记发现一个规律凡是 mock 做得扎实的项目前端同学的主动性都特别强。他们不会说“这个功能得等后端”而是说“我先 mock 一个你看效果行不行”不会抱怨“接口文档太简陋”而是直接从 mock 数据里反推业务规则更不会在联调时手忙脚乱因为所有异常分支早已在 mock 里验证过三遍。mock 的本质不是逃避后端而是把“等待”变成“主动构建”。它逼着你去读接口文档、理解业务语义、思考边界条件、验证错误处理——这些恰恰是优秀前端工程师的核心能力。我见过最厉害的一位同事他写的 mock 数据后来直接被后端团队拿去当单元测试的 fixture 用了因为“比我们自己写的还全”。所以下次当你又要花半天等一个接口时不妨停下来花 20 分钟搭个 mock。那 20 分钟换来的不只是当天的进度更是你对整个系统越来越清晰的掌控感。这种感觉是任何框架、任何工具都给不了的它只属于真正动手的人。
阅读完成 · 觉得有帮助?
咨询建站