MSW接口Mock实战:规则改写与断点拦截提升前后端联调效率

发布时间:2026/9/16 16:08:05
MSW接口Mock实战:规则改写与断点拦截提升前后端联调效率
如果你也是前端大概率经历过这种场景页面写好了接口文档也看过了但后端说“还差两个接口没写完你先 Mock 一下”。于是你随手在代码里写死一份 JSON或者用 Mock.js 随机生成一批假数据先让页面跑起来。等到联调那天发现字段名不一样、数据结构对不上、分页参数语义完全不同……这一改又是大半天。我以前就是这样后来把 Mock 从“临时凑合”升级成“标准基础设施”联调效率明显改善规则改写和断点拦截这两套能力功不可没。这篇文章不聊大而全的理论我把我自己在真实项目里的一套做法完整展开用 MSWMock Service Worker做接口 Mock重点拆解规则改写怎么做、断点拦截怎么用以及它们如何支撑前后端联调。适合正在做前后端并行开发的前端同学也适合被“接口永远晚两天”折磨的小团队。1. 联调卡壳的根源Mock 不是临时凑合而是接口进度的缓冲带前后端联调之所以总卡在最后几天本质原因是两个团队的产出节奏天然错位。后端要先设计数据库表、写业务逻辑、再暴露接口前端只要拿到接口文档就能先开发页面。等前端搞完界面后端可能还在调第二个接口的边界条件。这个时间差完全可以通过 Mock 数据填补但前提是 Mock 不能只是“写死一份 JSON 交差”。1.1 传统 Mock 方式的三个典型痛点最常见的做法是在 axios 拦截器里做文章判断某个环境变量后就返回假数据。这种方案看着简单实际上把业务代码和 Mock 逻辑搅在了一起。改接口地址、换响应结构的时候你往往要动业务文件多人协作时每个人本地 Mock 的实现还不一样很难对齐。第二种常见做法是用 Mock.js 生成随机数据但随机数据的问题是“不可控”——你想要一个 500 错响应Mock.js 默认永远返回 200你想测试空列表页面它偏给你生成一堆数据。第三种是起一个 json-server单独维护一份 JSON 文件可它脱离了浏览器网络层有些场景下模拟不出真实的请求链路。这三个痛点的背后其实是对“接口 Mock”定位的误解。Mock 的价值不在于“临时伪造数据”而在于给前端提供一条可以独立于后端、但和真实接口行为对齐的开发链路。字段名、请求头、状态码、异常情况都应该按照接口契约来模拟。这样才能在联调期把注意力集中在真正需要两边对齐的问题上。1.2 Mock 做规范了联调周期能缩短多少拿我自己的经验说话。之前做一个订单管理后台接口文档约定了一大套订单列表带分页、筛选、排序订单详情支持状态流转创建订单有一堆必填校验。后端真正把全部接口写完用了两周前端页面五天做好了。以前这种情况就是“页面躺着等接口”后来我花一个下午把 Mock 环境搭好前端同学提前进入开发状态等后端接口就绪后直接切换联调整个周期压缩了将近一周。更关键的是Mock 化之后的前端代码会主动处理异常状态。比如列表页要支持空态、加载态、错误态这些状态在真实接口没就绪时很难触发但在 Mock 环境下想触发就触发点一下规则就能改。2. 方案选择的底层逻辑MSW 为什么更适合现代前端我是从 axios 拦截器方案迁移到 MSW 的。当时项目里已经有一套用 axios 拦截器写的 Mock但每次加业务代码都要小心翼翼绕过 Mock 逻辑。后来换成了 MSWMock Service Worker整个体验一下子正常了——它不侵入业务代码而是在浏览器网络层面做拦截相当于给前端配了一个“本地代理”。2.1 Service Worker 这层“中间人”是怎么工作的MSW 的核心是 Service Worker一个浏览器后台运行的脚本可以拦截页面发出的网络请求。你在页面里照常调用axios.get(/api/orders)这个请求会先经过 Service WorkerMSW 在里面匹配你已经定义好的 handler匹配到了就返回 Mock 数据没匹配到就放行到真实网络。好处有三点。第一业务代码完全不用改不需要判断环境变量也不用给请求路径加分叉。第二Mock 逻辑和业务逻辑天然隔离所有 handler 集中在src/mocks目录下。第三同一套 handler 能复用到 Node 环境跑单元测试、组件测试的时候也能 Mock 请求这比在测试代码里手动 mock 模块干净得多。2.2 和 Mock.js、json-server 的对比网上流行方案不少我列个对比表| 方案 | 拦截层级 | 侵入性 | 动态规则 | 复用性 | | --- | --- | --- | --- | --- | | 手写 JSON if 判断 | 业务代码层 | 高代码里到处是分支 | 弱改数据还要改代码 | 差 | | Mock.js | 数据生成层 | 中通常要改请求封装 | 弱侧重于随机数据 | 一般 | | json-server | HTTP Server 层 | 中真实请求但需独立服务 | 中JSON 文件可改 | 一般 | | axios 拦截器 Mock | 请求封装层 | 中侵入请求库 | 中可写逻辑但和业务耦合 | 一般 | | MSW | 浏览器网络层 | 低业务代码零改动 | 强可完全模拟真实接口 | 强浏览器和 Node 通用 |选型的时候我建议优先关注两点侵入性和动态能力。MSW 在这两点上做得比较平衡这也是它在社区里热度上升的原因。不过它也有学习成本Service Worker 在本地开发时偶尔会遇到缓存不更新的问题这一点后面避坑清单里专门说。3. 环境搭建与目录设计30 分钟跑通一个带 Mock 的项目这里以一个 Vite Vue3 TypeScript 项目为例因为这是当下很多前端团队在用的组合。搭好后你会发现Mock 环境并不复杂核心就四个文件加一条初始化命令。3.1 安装和初始化两条命令搞定npm install msw --save-dev npx msw init public/ --save第二条命令会在public目录下生成一个mockServiceWorker.js文件。这个文件必须放在公共静态目录下因为 Service Worker 脚本要求同源访问。如果你的 Vite 项目配置了base: /admin/这类子路径后面还要手动调整 SW 的注册地址这里先按默认玩法走。3.2 目录设计和最小可运行的 handler我习惯在src下建一个mocks目录专门放 Mock 相关代码src/ mocks/ browser.ts # 浏览器环境启动器 server.ts # Node 环境启动器测试用 handlers/ orders.ts # 订单相关接口 user.ts # 用户相关接口 index.ts # 汇总所有 handler main.ts # 应用入口先写一个简单的 handler 集合模拟订单详情和订单列表// src/mocks/handlers/orders.ts import { http, HttpResponse } from msw const orders [ { id: 1, title: 测试订单A, amount: 99.5, status: pending }, { id: 2, title: 测试订单B, amount: 188, status: paid }, ] export const orderHandlers [ http.get(/api/orders, () { return HttpResponse.json({ code: 0, data: { list: orders, total: orders.length }, }) }), http.get(/api/orders/:id, ({ params }) { const id Number(params.id) const order orders.find((item) item.id id) if (!order) { return HttpResponse.json( { code: 404, message: 订单不存在 }, { status: 404 } ) } return HttpResponse.json({ code: 0, data: order }) }), ]3.3 在应用入口启动 worker注意异步竞态main.ts里的启动逻辑是关键很多第一次用 MSW 的人会在这里踩坑。直接同步注册会有一个竞态应用已经开始发请求了但 Service Worker 还没接管网络导致头几个请求直接打到真实网络。正确的做法是等worker.start()完成后再挂载应用// src/main.ts import { createApp } from vue import App from ./App.vue async function enableMocking() { if (import.meta.env.VITE_USE_MOCK ! true) return const { worker } await import(./mocks/browser) await worker.start({ onUnhandledRequest: bypass }) } enableMocking().then(() { createApp(App).mount(#app) })onUnhandledRequest: bypass这个配置项建议加上。它的意思是Mock 里没定义的请求不要拦直接放行。否则像埋点上报、静态资源这类请求也会被 SW 拦下来轻则警告刷屏重则页面异常。4. 规则改写让同一批接口根据入参吐出不同数据规则改写是 Mock 的灵魂。同一个接口不能永远返回同一份数据得跟着请求参数、请求体、登录状态走。这部分我会写三个最常用的改写维度基本能覆盖日常开发 80% 的场景。4.1 基于查询参数和路径参数的分支处理订单列表页通常要做筛选和分页前端通过 query 参数把条件传给后端。Mock 层读取这些参数再按条件过滤返回。// src/mocks/handlers/orders.ts import { http, HttpResponse } from msw const allOrders Array.from({ length: 35 }, (_, i) ({ id: i 1, title: 订单${i 1}, amount: Math.round(Math.random() * 1000 * 100) / 100, status: [pending, paid, refunded][i % 3], })) export const orderHandlers [ http.get(/api/orders, ({ request }) { const url new URL(request.url) const status url.searchParams.get(status) const keyword url.searchParams.get(keyword) const page Number(url.searchParams.get(page) || 1) const size Number(url.searchParams.get(size) || 10) let result [...allOrders] if (status) { result result.filter((item) item.status status) } if (keyword) { result result.filter((item) item.title.includes(keyword)) } const start (page - 1) * size const list result.slice(start, start size) return HttpResponse.json({ code: 0, data: { list, total: result.length, page, size, }, }) }), ]这里有个小技巧用new URL(request.url)来解析请求地址比用正则或者字符串拆分靠谱得多。searchParams、pathname 这些属性都是标准 URL API不容易出现取参取错的情况。路径参数的处理也差不多订单详情接口用:id占位handler 里通过params.id拿值。上面最小示例里已经写了不再重复。4.2 基于请求体的参数校验和状态流转创建订单、更新状态这类接口Mock 层不应该只是“收到请求然后返回成功”。接口契约里通常有一堆必填字段校验Mock 层可以把这些校验逻辑也实现出来前端在开发阶段就能感知到参数问题。http.post(/api/orders, async ({ request }) { const body await request.json() if (!body.title || !body.amount) { return HttpResponse.json( { code: 422, message: 缺少必填字段title、amount }, { status: 422 } ) } const newOrder { id: allOrders.length 1, ...body, status: pending } allOrders.unshift(newOrder) return HttpResponse.json({ code: 0, data: newOrder }, { status: 201 }) })状态流转接口要注意幂等性。比如支付接口同一笔订单不能支付两次第二次调用应该返回“订单已支付”的提示而不是再次扣款成功。这个语义在 Mock 阶段就应该体现出来。http.post(/api/orders/:id/pay, ({ params }) { const id Number(params.id) const order allOrders.find((item) item.id id) if (!order) { return HttpResponse.json({ code: 404, message: 订单不存在 }, { status: 404 }) } if (order.status paid) { return HttpResponse.json( { code: 20001, message: 订单已支付请勿重复操作 }, { status: 409 } ) } order.status paid return HttpResponse.json({ code: 0, data: order }) })这样前端在 Mock 阶段就把“重复点击”这种边界情况处理好了等真实接口上线代码直接复用。4.3 在 Mock 层维护一份“内存数据库”上面的示例里allOrders就是一份内存数据库用数组实现。它看起来简单但能处理大部分 CRUD 场景。顺序很重要先把数据更新到数组里再返回响应保证下一次读取能拿到最新数据。如果项目里的 Mock 数据量大字段关系复杂可以考虑在 Mock 层引用一个轻量级库比如 faker.js 来生成基础假数据。但我的建议是不要过度设计Mock 的核心是行为一致性不是数据量。数据能支撑分页、筛选、详情、创建这几个操作就够了。4.4 根据请求头模拟登录态联调阶段经常要切换用户权限有时候是管理员有时候是普通用户。前端会在请求头里带 tokenMock 层读取后返回不同角色权限。http.get(/api/user/me, ({ request }) { const token request.headers.get(Authorization) if (!token) { return HttpResponse.json( { code: 401, message: 未登录 }, { status: 401 } ) } const isAdmin token.includes(admin) return HttpResponse.json({ code: 0, data: { name: isAdmin ? 管理员 : 普通用户, role: isAdmin ? admin : user, }, }) })这样前端想测管理员页面就在请求头里带一个包含admin的 token想测普通用户页面就换一个 token。比每次改代码强多了。5. 断点拦截把请求控在手里才能演练各种极端场景规则改写解决的是“同一个接口返回不同数据”的问题断点拦截解决的是“这个请求能不能回来、什么时候回来、回来时是什么状态”的问题。这两个能力合起来才能还原真实联调中的各种意外。5.1 延迟拦截模拟慢网络和接口超时弱网环境是前端最容易忽略的场景。页面在 3G 网络下打开接口响应可能需要 3 秒如果前端没有 loading 状态用户体验会很糟。MSW 的delay()可以模拟这个时延import { delay } from msw http.get(/api/orders, async () { await delay(3000) return HttpResponse.json({ code: 0, data: { list: [], total: 0 } }) })delay(3000)会让这个接口在 3 秒后才返回数据。前端开发时如果发现这个接口没做 loading 处理立刻就能补上请求中状态。更极端的情况是接口超时也就是后端一直不响应。可以返回一个永不结束的 Promisehttp.get(/api/orders, async () { return new Promise(() {}) })请求会一直挂着正好用来验证前端的超时兜底逻辑。一般项目里 axios 会配timeout到了时间触发错误分支前端要给出“网络超时”的提示并支持重试。5.2 错误响应和网络异常让前端处理所有“坏情况”正常 Mock 大家都会写犯错的人少。容易忽略的是 500、502、网络中断这一类异常模拟。之前我犯过一个低级错误接口挂了前端页面直接白屏。后来在 Mock 环境里把所有错误都模拟了一遍逐个修复迭代到真实环境就没再慌过。// 模拟服务端 500 http.get(/api/orders, () { return HttpResponse.json( { code: 500, message: 数据库连接失败 }, { status: 500 } ) }) // 模拟网络层错误请求根本没到达服务器 http.post(/api/orders, () { return HttpResponse.error() })HttpResponse.error()会模拟一次网络错误浏览器捕获到的就是常规的网络异常axios 会走到catch分支。这种模拟比直接返回一个 500 更狠适合测试请求重发机制。5.3 覆盖已有 handler测试一次性场景的好帮手有些场景只在特定页面上出现一次比如“列表页没有数据时展示空态”。你不想全局改掉订单列表接口只想在进某个页面时临时返回空数组。MSW 提供了一个worker.use()的运行时覆盖能力import { http, HttpResponse } from msw worker.use( http.get(/api/orders, () { return HttpResponse.json({ code: 0, data: { list: [], total: 0 } }) }) )调用worker.use()之后之后的请求会优先匹配这个临时 handler相当于给当前页面临时换了份规则。刷新页面后这个覆盖会清除。这个能力和“断点拦截”非常契合——你想在哪个页面拦截什么逻辑就在哪个页面注入对应规则。5.4 多个接口同时拦截用一个场景脚本控制全局有时候前端要测一个完整的流程比如“先创建订单再支付再查看订单列表”。这个过程涉及三个接口而且有先后依赖关系。就按正常操作的顺序写好几个 handler在 Mock 层维护好“内存数据库”的状态流转前端在页面里正常点按钮就能走通全流程不需要额外处理。这一点其实很关键。Mock 做得好前端可以在后端完全没就绪的情况下把完整业务链路跑一遍等到联调时前端已经相当稳定了。6. 联调切换机制从 Mock 数据到真实接口的平滑过渡Mock 做得再好最终还是要切回真实接口。切换这件事如果设计得不好会出现两种情况要么 Mock 代码还残留在生产包里要么切换那天手忙脚乱。合理的做法是提前设计好开关让切换变成配置项的一次修改。6.1 环境变量作为总开关用 Vite 的 env 变量做控制开发环境默认打开 Mock生产环境默认关闭。以.env.development为例# .env.development VITE_USE_MOCKtrue VITE_API_BASE_URL/api联调时只需要把VITE_USE_MOCK改成falseVITE_API_BASE_URL改成后端联调地址或者其他同事的本地地址前端代码就自动走真实接口。如果有多个后端同事并行开发每人起一个本地服务端口还不一样。这时候可以再扩展一层配置比如.env.zhangsan、.env.lisi通过--mode zhangsan启动各自对应的环境文件就不用手动改地址了。6.2 接口契约先行TypeScript 类型和 Mock 共用同一份定义联调阶段最头疼的问题就是“字段名对不上”。后端说字段是orderNo前端按文档写的是no两边各执一词扯半天。要根治这个问题最好在接口定义阶段就把 TypeScript 类型定好然后 Mock 的响应数据也按这个类型写前后端都以类型为准。// src/types/order.ts export interface OrderItem { id: number orderNo: string title: string amount: number status: pending | paid | refunded createdBy: string } export interface OrderListResponse { code: number message?: string data: { list: OrderItem[] total: number page: number size: number } }Mock handler 的返回值类型就写OrderListResponse字段哪里不对编辑器直接标红。真实联调时后端通常会有 Swagger/OpenAPI 文档可以用工具把 OpenAPI 转换成 TypeScript 类型但前提是接口定义先行这个流程要跑起来。6.3 联调当天的实操流程我所在的团队现在有一套固定流程供参考接口评审阶段前后端一起过接口文档确认路径、入参、出参、错误码完成后前端把 TypeScript 类型定义好。前端开发阶段前端打开 Mock按类型定义写 handler开始开发页面。后端开发阶段后端按同一个接口文档开发可以完全不关心前端在干嘛。联调前检查后端接口自测通过后前端关闭 Mock把VITE_API_BASE_URL指到后端环境。联调问题登记联调中发现的问题如果是字段语义不一致回到接口文档改改完类型和 Mock 一起更新。这套流程跑顺之后联调基本不会出现“页面等接口”的等待期两边都是带着相对稳定的版本去对接问题集中在真正需要双方确认的业务逻辑上。6.4 把 Mock 复用到自动化测试除了联调Mock 在自动化测试里也是一员大将。前端项目跑 Vitest 的时候组件里发的请求总是让人头疼现在可以直接用 MSW 的 Node 版// src/test/setup.ts import { setupServer } from msw/node import { handlers } from ../mocks/handlers const server setupServer(...handlers) beforeAll(() server.listen()) afterEach(() server.resetHandlers()) afterAll(() server.close())这样跑测试的时候axios 发出去的请求会被 MSW 在 Node 层面拦截测试环境完全不需要一个真实的后端服务。之前有一段时间我们后端重构数据库接口全断但前端测试照常跑靠的就是这套 Mock 环境。7. 使用 MSW 这段时间踩过的坑最后聊几个实际使用中容易踩的坑都属于“文档不会写但迟早要遇到”的实战经验。7.1 Service Worker 缓存导致改规则不生效最典型的坑改了 handler 代码刷新页面请求结果还是老样子。原因在于浏览器把旧的mockServiceWorker.js缓存住了新代码没有立即生效。处理方式有两个开发时打开 DevTools 的 Network 面板勾选 “Disable cache”或者 Console 里执行一次worker.stop()再重新worker.start()强制刷新 SW 注册。如果发现改了 handler 老不生效先看 Application → Service Workers 面板确认一下有没有多个 SW 在同时工作。有旧 SW 没注销就点 Unregister 清掉再刷新。7.2 v1 到 v2 的 API 迁移老教程的代码别直接抄MSW v2 正式发布了挺久网上还是有一大批 v1 的教程。v1 用rest、res、ctx那套风格v2 全改成http、HttpResponse了。如果你照着老教程写大概率会遇到一堆 API 不存在的报错。最直接的判断方法看 npm 安装的版本号。如果装的是 v1 但你按 v2 写或者反过来报错信息都让人摸不着头脑。迁移时重点改这四处rest.get换成http.getres(ctx.status(200), ctx.json(...))换成HttpResponse.json(..., { status: 200 })req.params.id换成params.idreq.url.searchParams.get()换成new URL(request.url).searchParams.get()。7.3 部署子路径下的 SW 注册问题项目部署在子路径比如https://example.com/admin/下面默认的mockServiceWorker.js路径会注册不上。需要在worker.start()里手动指定 SW 的完整 URLawait worker.start({ onUnhandledRequest: bypass, serviceWorker: { url: /admin/mockServiceWorker.js, }, })记住一个判断原则SW 脚本能通过浏览器直接访问到才能正确注册。访问不到就去查静态目录部署和 base 路径配置。7.4 在 Mock 环境里别忽略真实接口的响应头有一些场景前端要读取响应头里的信息比如分页总数、限流信息。Mock 的时候如果不模拟响应头前端开发时就会直接漏掉这部分的处理逻辑。用 MSW 可以这样设置http.get(/api/orders, () { return HttpResponse.json( { code: 0, data: { list: [], total: 0 } }, { headers: { X-Total-Count: 35, X-Rate-Limit: 5000, }, } ) })前端的 axios 拦截器里如果读取了这些响应头Mock 阶段就能验证取值的逻辑不需要等真实接口。7.5 常备几个“场景化” handler 模板我现在每接到一个需要 Mock 的新模块都会顺手维护几个标准的场景模板正常列表、空列表、单条数据、500 错误、超时不返回、未登录 401。这些模板以注释形式写在 handler 文件里需要哪一个就取消注释不用每次从零开始写。大概是这个体量export const orderHandlers [ // 场景1正常列表 http.get(/api/orders, () { /* ... */ }), // 场景2空列表 // http.get(/api/orders, () { // return HttpResponse.json({ code: 0, data: { list: [], total: 0 } }) // }), // 场景3500错误 // http.get(/api/orders, () { // return HttpResponse.json({ code: 500, message: server error }, { status: 500 }) // }), ]实测下来这个习惯能省很多时间尤其是后面同事接手你的 Mock 代码时看到这些模板立刻就知道怎么扩展。最后再说一句规则改写和断点拦截这两件事最终都是为了让前端在联调前就把更多问题消化在内部。哪怕技术栈不用 MSW这个思路也通用——Mock 不是给测试看的表面功夫它是前端工程化里值得认真对待的一环。你现在愿意花半天搭好的环境后面能在无数个联调日里帮你省回来。