ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

前端Mock接口数据实战:msw规则改写与断点拦截全解析

前端Mock接口数据实战:msw规则改写与断点拦截全解析 做前端的谁没被联调折磨过。接口还没开发完、文档先行的阶段拿着一份接口文档就得开始排期结果等后端真正提测的时候工期已经耗掉一半。后来我养成了一个习惯不管后端进度如何前端这边先把 Mock 跑起来等联调的时候直接切真实接口换掉 Mock 就行。这个思路听起来简单但真正用好的关键全在两个动作上——规则改写和断点拦截。这篇文章就把这两件事从头到尾拆开讲清楚包括我实际踩过的坑、用过的方案、以及最后的代码怎么写。内容以 mswMock Service Worker为主穿插讲一些和测试工具联动的思路。不管你是刚接触 Mock 的新人还是已经写过不少 mock handler 的老手这篇文章都值得你过一遍。1. Mock 接口数据的核心价值与方案选型1.1 联调时为什么离不开 Mock很多新手对 Mock 的理解停留在“造几条假数据让页面别报错”。真实项目里Mock 的价值远比这个高。联调的核心矛盾是什么是前后端并行开发时接口的不可用。后端可能先给了一个接口文档但代码还在写也可能接口已经能调了但部分返回字段还没接上更常见的是某些异常场景根本没法稳定复现比如超时、返回 500、返回空的列表。这些场景如果都等真实接口测试排期根本排不过来。Mock 把这个问题变成了纯前端可控的事情。接口文档一出来我就能把请求路径、请求方式、请求参数、返回结构全部在本地模拟好。前端页面基于这套 Mock 数据开发等真实接口可用时只需要切换一个开关请求就打到真实服务上。整条链路的开发效率至少提升一倍。更重要的是Mock 数据让前端开发不必等后端“施舍”数据。后端还在设计表结构的时候前端已经可以把页面交互、异常提示、加载状态全部调完。最早做这种事用的是本地 json 文件后来是 json-server再后来是各类构建插件现在我用的是 msw。1.2 方案选型为什么选 msw 而不是 json-server 或 vite-plugin-mock选型这件事我踩过不少坑简单梳理一下。json-server 用起来简单几分钟就能起一个基于 json 文件的 REST API 服务。但它有两个硬伤一是它跑在一个独立的端口上前端代码里要维护 baseURL 切换二是它只能返回静态数据想做规则改写、断点拦截这类动态逻辑需要写一堆自定义中间件非常别扭。vite-plugin-mock 适合 Vite 项目开发环境用中间件拦截请求实现简单。但它的实现依赖 dev server生产构建后就没法用了而且在处理 Service Worker 级别的请求拦截时受限比如没法拦截页面里的静态资源请求也没法模拟真实的网络延迟细节。msw 的原理完全不同。它在浏览器里注册一个 Service Worker真实地拦截网络请求然后把响应返回到页面。前端代码里发的还是正常的 fetch 或 axios 请求浏览器网络面板也能看到请求记录唯一不同的是请求根本没出浏览器就被 Service Worker 接管了。用 msw 有几个直观的好处不需要启动额外服务不需要改 baseURL请求是真实走了 fetch 的所以 axios 拦截器、loading 状态、错误处理统统能测到可以精确到请求方法、URL、请求头做匹配响应延迟、错误码、网络断开都能模拟非常适合做异常场景测试生产环境下可以彻底移除 mock不会存在“上线后还在 mock”这种低级事故。msw 的规则改写和断点拦截就是它最核心的两个能力后面我分别展开。1.3 规则改写和断点拦截是什么关系简单说规则改写解决的是“不同请求返回不同数据”的问题断点拦截解决的是“请求发出前后我能插手改数据”的问题。规则改写是静态层面的。你定义好一个请求匹配规则命中后就返回你预设的响应数据。这里的“改写”体现在同样是/api/user这个路径我可以让 GET 请求返回用户信息让 POST 请求返回创建成功也可以让带?typeadmin的请求返回管理员列表不带参数的返回普通用户列表。断点拦截是动态层面的。它更像调试工具里的断点——请求到某个环节我让它暂停一下这时候我可以修改请求头、修改请求体、模拟延迟、注入错误、甚至直接返回一个自定义响应然后决定是放行还是终止请求。如果说规则改写是静态路由断点拦截就是可插拔的中间件。实际联调里这两个能力经常配合使用先用规则改写把主流程的数据准备好再用断点拦截模拟真实网络环境里会出现的各种鬼问题。接下来我逐个讲实操。2. 规则改写实操让 Mock 数据“活”起来2.1 从写死 JSON 到动态规则的思路转变很多人写 Mock 的第一版是这样的import { http, HttpResponse } from msw export const handlers [ http.get(/api/user, () { return HttpResponse.json({ code: 0, data: { id: 1, name: 张三, }, }) }), ]能跑但问题很大所有用户返回的都是同一个“张三”。真实项目里/api/user/1和/api/user/2返回的内容一定不同。如果 Mock 数据不具备动态性前端的很多逻辑就测不到比如当前用户权限不同、列表页空态、详情页 404。规则改写的核心思路是把请求本身当成输入让响应变成请求的函数。请求路径、查询参数、请求体、请求头都可以参与运算。2.2 按请求参数动态改写响应msw 的路径匹配支持动态参数用冒号声明。比如/api/user/:id就能匹配/api/user/1也能匹配/api/user/2然后在回调里拿到具体的idhttp.get(/api/user/:id, ({ params }) { const { id } params return HttpResponse.json({ code: 0, data: { id: Number(id), name: 用户${id}, avatar: https://example.com/avatar/${id}.png, }, }) })实际项目里这么写还不过瘾因为往往要根据 query 参数做不同返回。比如列表接口http.get(/api/users, ({ request }) { const url new URL(request.url) const page Number(url.searchParams.get(page) ?? 1) const pageSize Number(url.searchParams.get(pageSize) ?? 10) const keyword url.searchParams.get(keyword) ?? const allUsers Array.from({ length: 35 }, (_, i) ({ id: i 1, name: 用户${i 1}, role: i % 3 0 ? admin : user, })) const filtered keyword ? allUsers.filter((user) user.name.includes(keyword)) : allUsers const start (page - 1) * pageSize const pageData filtered.slice(start, start pageSize) return HttpResponse.json({ code: 0, data: { list: pageData, total: filtered.length, page, pageSize, }, }) })这一步做完前端的分页、搜索、筛选逻辑全部可以用 Mock 数据联调。这就是规则改写的精髓——不是伪造数据而是模拟真实接口的行为。2.3 请求方法、请求体层面的规则改写除了路径和参数请求方法也是规则的一部分。同一个路径GET 和 POST 可以有不同的行为这在真实接口中非常常见。比如创建用户的接口简单模拟一个 POST 请求把请求体里的内容回显到响应里http.post(/api/users, async ({ request }) { const body await request.json() as { name: string; email: string } // 校验逻辑也可以在里面做 if (!body.name || !body.email) { return HttpResponse.json( { code: 400, message: name 和 email 不能为空 }, { status: 400 }, ) } return HttpResponse.json( { code: 0, data: { id: Date.now(), ...body }, }, { status: 201 }, ) })这里我补充一个实践细节msw 的HttpResponse.json()第二个参数可以指定 status、headers 等这一点很多教程都没提。真实的接口返回 201、400、500 这些状态码时前端 axios 默认的响应拦截器会走错误分支如果你 Mock 永远返回 200那错误处理代码永远开发不到。2.4 匹配优先级精确规则优先于模糊规则msw 的规则匹配不是“先到先得”而是从精确到模糊的优先顺序。相同优先级下先定义的 handler 生效。这个机制在实际项目中经常踩坑。我举个例子。有这样一个规则http.get(/api/user/:id, () HttpResponse.json({ code: 0, data: { type: normal } })) http.get(/api/user/me, () HttpResponse.json({ code: 0, data: { type: me } }))请求/api/user/me时到底是哪个 handler 生效msw 会优先匹配静态路径/api/user/me所以返回的应该是type: me。这是符合直觉的但如果你把带参数的那个规则写在前面也不会覆盖后面这个精确匹配的规则。这背后的逻辑是msw 对每个 handler 做匹配时会计算匹配的精确度越精确的路径权重越高。所以不用担心“模糊规则把精确规则挡住了”。还有一个要注意的地方query 参数不算匹配权重。/api/users?page1和/api/users?page2命中的是同一个 handler不做区分。如果你确实需要按 query 参数区分只能在 handler 内部自己判断。3. 断点拦截实操把请求“截停”再操作3.1 断点拦截的本质规则改写解决“返回什么”的问题断点拦截解决“在请求过程中能不能插一手”的问题。msw 里的实现方式是通过生命周期事件在请求发出前、响应返回前分别提供钩子。核心是lifecycleEvents或中间件式的写法。msw 2.x 里可以在 handler 内部做逻辑分支也可以用http.all搭配中间件形式把所有请求都接进来import { http } from msw import { delay } from msw export const handlers [ http.all(/api/*, async ({ request }) { // 这里是断点位置请求已经发出但还没到业务逻辑 console.log([mock] 拦截到请求, request.method, request.url) // 模拟网络延迟 await delay(800) // 然后怎么处理呢需要继续往下传递 }), ]有一点要说明http.all的 handler 要求最终返回一个 Response所以它更像一个“全局前置中间件”。但拦截之后要怎么让请求继续走其他 handler 呢这里有个常见的误区。在 msw 中如果多个 handler 都匹配同一个请求并不是所有 handler 都会执行而是按照匹配优先级挑选一个执行。所以如果想把断点拦截做成“先拦截处理再放行到具体规则”官方推荐的做法是通过HttpResponse.json()直接在拦截处返回或者在 handler 内部自行组织逻辑。你不需要在全局中间件里纠结“如何放行”而是应该在具体的规则处先做统一的分支逻辑。实际项目中我一般习惯用一个更清晰的写法把断点逻辑放在一个统一的入口文件里根据条件做分支再在分支里调用具体的响应生成函数。http.all(/api/*, async ({ request }) { const url new URL(request.url) // 模拟所有接口的通用延迟 await delay(300) // 如果是股票行情接口模拟偶发的超时 if (url.pathname.startsWith(/api/stocks) Math.random() 0.8) { await delay(5000) return HttpResponse.json({ code: 504, message: 网关超时 }, { status: 504 }) } // 其余请求按路径继续分发到具体 handler return getMockResponse(request) })这样做的好处是断点拦截逻辑集中管理规则改写逻辑按模块分散互不干扰。新来一个接口只需要在getMockResponse里加一个 case 即可。3.2 主动延迟模拟弱网和慢接口联调中后端接口慢是常态但你不能老真的等后端变慢。用模拟延迟前端可以把 loading 态调得很舒服。msw 提供了delay()方法可以直接返回响应前等待一段时间http.get(/api/slow-report, async () { await delay(3000) return HttpResponse.json({ code: 0, data: { report: ok } }) })更精细的做法是配一个随机延迟模拟真实网络抖动function randomDelay(min 200, max 1500) { return Math.floor(Math.random() * (max - min 1)) min } http.get(/api/random-delay, async () { await delay(randomDelay()) return HttpResponse.json({ code: 0, data: null }) })断点拦截延迟的典型场景有两个。第一个是列表页的骨架屏很多组件的骨架屏效果需要接口在几百毫秒后才返回才能看到。第二个是按钮防重复提交比如提交表单后按钮进入 loading 状态如果 Mock 秒回根本看不出按钮 loading 效果对不对。3.3 错误注入五类常见故障的 Mock 写法联调时最怕的不是失败而是失败得很随机。用断点拦截可以把故障场景稳定复现这是真实接口给不了你的。我整理了几类最常见的错误注入方案直接可以参考// 1. 接口 500 http.get(/api/error-500, () { return HttpResponse.json( { code: 500, message: 服务器内部错误 }, { status: 500 }, ) }) // 2. 接口返回非 JSON 格式比如一段 HTML 错误页 http.get(/api/error-html, () { return new HttpResponse(!DOCTYPE htmlhtmlerror/html, { status: 502, headers: { Content-Type: text/html }, }) }) // 3. 返回空数据模拟列表为空 http.get(/api/empty-list, () { return HttpResponse.json({ code: 0, data: { list: [], total: 0 } }) }) // 4. 网络超时前端会走超时分支 http.get(/api/timeout, async () { await delay(10000) return HttpResponse.json({ code: 0, data: null }) }) // 5. 返回数据结构错误比如缺字段 http.get(/api/bad-structure, () { return HttpResponse.json({ code: 0, data: { name: 没有 id 字段 } }) })错误注入的价值在于它能验证前端各种异常分支的代码是否能正常工作。如果发现某个错误弹窗、错误提示文案没有正确出现多半是前端代码的问题和 Mock 接口没关系。先把前端修好等真实接口出来你就能确认后端的行为是否和 Mock 一致不一致的地方立刻能发现。3.4 请求头的断点修改与登录态模拟联调绕不开登录态。真实场景里前端请求会带上 token后端通过 token 识别用户身份。Mock 阶段你可以模拟这个机制http.get(/api/profile, ({ request }) { const token request.headers.get(Authorization) if (!token || token ! mock-token-123) { return HttpResponse.json( { code: 401, message: 未登录 }, { status: 401 }, ) } return HttpResponse.json({ code: 0, data: { id: 1, name: 管理员, role: admin, }, }) })这样就能模拟登录失效的场景。前端的 axios 拦截器收到 401 后应该跳转登录页、清空本地 token。如果把 token 校验写进 Mock这个链路就能完整联调。断点拦截还能模拟请求头持久化。比如用户切换了主题色后续请求带上这个偏好http.all(/api/*, ({ request }) { const theme request.headers.get(X-Theme) // 可以把 theme 存到全局变量后续响应里带上 return undefined // 这里只是演示拦截读取实际要继续分发 })不过说实话这个功能用得不多通常我会把请求头相关的逻辑集中在专门的 auth mock 里避免全局处理影响其他规则。3.5 和浏览器 DevTools 断点的区别有些刚接触断点拦截的同学会问既然浏览器 DevTools 的 Network 面板也能改响应为什么还要用 msw 做断点拦截这个问题很典型。DevTools 的断点只能手动操作一次适合临时看某个响应而且改不了请求发出前的逻辑。msw 的断点拦截是可编程的、可重复的适合做自动化回归。一次配置多次复用团队每个人拉到代码都能跑出同样的数据。另外DevTools 的响应改写依赖你手动选中某个请求msw 的规则改写在代码层面就定义了场景。比如你写了一个模拟 401 的规则跑测试的时候这个规则自动生效你不需要盯着浏览器手动改。这是两者最大的区别。4. 联调实战vue3 ts 项目落地 Mock4.1 初始化项目与安装依赖实战部分我以一个 vue3 ts 项目为例从零开始把 msw 接进来。假设你已经有了一个 vue3 项目没有的话用npm create vuelatest临时建一个。安装依赖npm install msw --save-dev初始化 Service Workernpx msw init public/这一步会在public/目录下生成mockServiceWorker.js。这个文件的本质是一个 Service Worker 脚本浏览器加载后它负责拦截网络请求并交给 msw 处理。有些同学会疑惑为什么要单独 init 一次因为 Service Worker 必须放在静态资源根目录才能被浏览器访问到public/目录是 Vite 的静态资源目录放这里最合适。安装完成后检查项目中vite.config.ts的 dev 端口。msw 的 worker 默认假设页面在同源环境下如果你的 dev server 跑在不同端口或使用了代理可能需要额外配置worker.start()的选项这个后面讲。4.2 handler 的目录结构与 TS 类型项目一大mock handler 不能全写在一个文件里。我习惯按业务模块拆分src/ mocks/ browser.ts # worker 启动与配置 handlers.ts # 汇总所有 handler handlers/ user.ts # 用户模块 stock.ts # 行情模块 common.ts # 通用异常场景 data/ userDB.ts # 用户相关模拟数据 stockDB.ts # 行情相关模拟数据TS 项目里mock 数据最好也定义类型。这里有一个非常实用的技巧mock 用的类型和真实接口的响应类型应该保持一致。比如真实接口的响应结构是interface ApiResponseT { code: number message: string data: T } interface StockInfo { code: string name: string price: number changePercent: number }mock handler 返回的数据也用这个类型约束import { http, HttpResponse, delay } from msw import type { ApiResponse, StockInfo } from /types/api const stockList: StockInfo[] [ { code: 600519, name: 示例蓝筹, price: 1688.0, changePercent: 2.31 }, { code: 000001, name: 示例银行, price: 12.34, changePercent: -0.5 }, ] export const stockHandlers [ http.get(/api/stocks, async () { await delay(500) const res: ApiResponseStockInfo[] { code: 0, message: ok, data: stockList, } return HttpResponse.json(res) }), http.get(/api/stocks/:code, async ({ params }) { const code params.code as string const stock stockList.find((item) item.code code) const res: ApiResponseStockInfo | null stock ? { code: 0, message: ok, data: stock } : { code: 404, message: 未找到该股票, data: null } return HttpResponse.json(res, stock ? { status: 200 } : { status: 404 }) }), ]这样做的好处是联调阶段如果接口字段变了TS 类型检查会同时提示 mock 数据和页面代码把问题往前端暴露而不是等后端联调时人工去对字段。4.3 worker 启动与浏览器控制台验证在开发环境启动 mock推荐放在入口文件里并且通过环境变量控制避免生产环境影响。// src/main.ts import { createApp } from vue import App from ./App.vue async function bootstrap() { if (import.meta.env.DEV) { const { worker } await import(./mocks/browser) await worker.start({ onUnhandledRequest: bypass, }) } createApp(App).mount(#app) } bootstrap()这里有几个值得注意的点。第一import.meta.env.DEV条件保证生产构建不会打包 mock 代码但也别把 mock 文件漏进生产资源里后续我会讲怎么检查。第二onUnhandledRequest: bypass表示没有定义 handler 的请求直接放行到真实网络不让它打印一堆警告。实际联调时我喜欢把它设置成warn这样能看到有没有漏掉的 mock 请求。在测试环境则用error强制要求所有请求都有 mock。第三worker.start()是异步的需要 await否则会存在首屏前几个请求已经发出去了但 Service Worker 还没注册好的竞态问题。这个坑很隐蔽很多项目首屏偶发几次真实请求就是这个原因。启动后打开浏览器控制台会看到类似这样的输出[MSW] Mocking enabled.然后在 Network 面板里你能看到mockServiceWorker.js的请求记录以及被你 mock 的那些接口请求。注意Network 面板里请求依然存在没有 404只是响应内容是 mock 的。这就是 msw 的好处它不改变前端代码的请求方式只在 Service Worker 层面拦截。4.4 组件联调实践列表、详情与轮询我拿一个实际项目里的案例来讲做一个简单的行情看板页面。页面有三个模块股票列表、搜索框、详情卡片。列表加载后支持点击某只股票查看详情详情接口每 3 秒轮询一次刷新最新价格。页面核心逻辑大概是const stockList refStockInfo[]([]) const currentStock refStockInfo | null(null) async function fetchList(keyword: string) { const { data } await axios.get(/api/stocks, { params: { keyword }, }) stockList.value data.data } async function fetchDetail(code: string) { const { data } await axios.get(/api/stocks/${code}) currentStock.value data.data } let timer: number | undefined function startPolling(code: string) { clearInterval(timer) fetchDetail(code) timer window.setInterval(() fetchDetail(code), 3000) }Mock handler 这边加上列表搜索和详情轮询的支持http.get(/api/stocks, ({ request }) { const url new URL(request.url) const keyword url.searchParams.get(keyword) ?? const filtered keyword ? stockList.filter((item) item.name.includes(keyword)) : stockList return HttpResponse.jsonApiResponseStockInfo[]({ code: 0, message: ok, data: filtered, }) }) http.get(/api/stocks/:code, ({ params }) { const code params.code as string // 模拟价格每分钟小幅波动 const stock stockList.find((item) item.code code) if (stock) { const delta (Math.random() * 2 - 1) * 0.5 stock.price Number((stock.price delta).toFixed(2)) stock.changePercent Number(((stock.price - 1500) / 1500 * 100).toFixed(2)) } return HttpResponse.jsonApiResponseStockInfo | null({ code: 0, message: ok, data: stock ?? null, }) })联调过程中我发现轮询接口如果每次都返回完全一样的数据页面上的价格变化效果就出不来。所以我在 mock 里加入了微小的随机波动这样前端的动画、刷新时间、高亮变化逻辑都能被真实地验证到。另外这个场景可以用断点拦截配合测试弱网。在慢速轮询的情况下上一次请求还没返回下一次轮询又发起了前端需要考虑是否需要取消上一次请求、如何避免竞态。用 msw 的await delay(5000)能稳定复现这种时序问题。4.5 与 jmeter 等接口测试工具联动的思路有些团队不光前端用 Mock测试同学也会用 jmeter 做接口自动化。标题里提到了“jmeter 将 JDBC Request 查询出的数据作为下一个接口的参数”这种场景在联调里也能和 Mock 结合。思路是这样的Mock 数据在前端模拟了接口的行为但 jmeter 测的是真实后端接口。在联调早期后端还没就绪jmeter 可以直接针对 Mock 服务跑接口。不过既然 msw 的 mock 跑在浏览器里jmeter 没法直接访问这个情况我会换一种落地方案。一个可行的做法是在 msw 的 handler 里把关键 Mock 数据暴露成一个本地接口同时用 Node 环境跑一个轻量的 mock 服务比如 msw 的setupServer。这样前端用浏览器里的 mswjmeter 用 Node 环境里的 msw共享同一份 handler 代码保证前端和测试看到的数据行为一致。这里给一个 node 环境复用 handler 的示例// mocks/node.ts import { setupServer } from msw/node import { handlers } from ./handlers export const server setupServer(...handlers)然后测试脚本里import { server } from ./mocks/node beforeAll(() server.listen()) afterEach(() server.resetHandlers()) afterAll(() server.close())这样同一个 handler 既可以用在浏览器也可以用在 Node 环境。jmeter 如果需要对某个接口做依赖关联比如先从/api/stocks拿到列表再用列表里第一只股票的 code 去请求详情那么 mock 这个接口返回的数据结构必须稳定且字段名规范。用共享 handler 的做法能保证你在 jmeter 里解析 JSON 时拿到的字段和前端开发时看到的一致。顺带提一句如果后端用的是真实数据库测试同学想拿数据库查询结果作为下一个接口参数那是另一套路子需要后端配合提供查询接口或测试库通常不属于前端 Mock 的范畴。前端驱动的 Mock 联调重点还是在前端页面和接口测试脚本的配合上。5. 常见问题与排查技巧实录5.1 请求没有被 msw 拦截的排查链路遇到最多的坑就是“为什么请求发出去了但 mock 没生效”。按下面这个链路排查基本能解决九成的问题看浏览器控制台是否有[MSW] Mocking enabled.。没有的话worker 没有启动。看 Network 面板里有没有mockServiceWorker.js这个请求。没有的话说明 Service Worker 没注册成功检查public/mockServiceWorker.js文件是否存在以及路径是否正确。看请求的方法和 URL 是否严格匹配。msw 的路径匹配是区分大小写的/api/Stocks和/api/stocks是两个路径。看请求是否被onUnhandledRequest: bypass放行了。如果 handler 没有匹配上msw 会把它当未处理请求处理。检查worker.start()的 type 配置如果 dev server 和前端页面不同源可能需要设置worker.start({ serviceWorker: { url: /mockServiceWorker.js } })。5.2 TS 类型和 mock 数据不一致的报错很多项目用了 TS 之后会遇到 mock 数据和真实类型对不上的问题。比如真实接口返回的是{ code: 0, data: { user: {...} } }mock 里写成了{ code: 0, data: { name: xxx } }前端拿到data.user时就报错了。解决方案有两个。第一个是 mock 数据也强依赖类型定义用satisfies或as断言约束返回结果避免 mock 结构和真实结构脱节。第二个是写一个“类型一致性校验”函数在 dev 环境把 mock 的响应做一次运行时校验比如用 zod 定义 schemamock 返回的数据先用 schema.parse 校验一遍有问题直接在控制台报错。这个方法我实际用下来很稳。它把联调出错的时机往前推不是你前端拿不到数据的时候才发现问题而是 mock 一启动、数据一返回就发现哪里结构不对。5.3 生产环境忘了关闭 mock 怎么防范msw 只在 DEV 启动代码层面已经规避了大部分风险。但有一种情况容易漏项目里写了一个定时任务或者某个初始化逻辑里直接import { worker } from ./mocks/browser导致生产包也打进了 mock 代码。稳妥的做法是加一个构建检查脚本在 CI 流程里搜索构建产物中是否有mockServiceWorker或Mocking enabled的关键字。另外生产环境可以让worker.start()直接不执行同时在 main.ts 里加一层环境判断if (import.meta.env.DEV) { const { worker } await import(./mocks/browser) await worker.start() }用动态 import 的方式保证生产环境不会执行 mock 相关代码。5.4 缓存导致的“规则改了但不生效”“改了 handler 但页面还是旧数据”这个问题最容易绕晕人。原因有几种第一种是 Service Worker 缓存了旧的 worker 脚本。msw 每次更新版本号npx msw init生成的mockServiceWorker.js里会带一个版本号字段如果你手动改过这个文件或 init 时没覆盖旧文件可能导致浏览器缓存了旧 worker。解决办法是清一下站点数据或者强制刷新页面。第二种是页面用的 dev server 缓存了模块。Vite 对 ES Module 有缓存修改 handler 文件后需要等 HMR 或刷新页面。如果 HMR 失效就重启一下 dev server。第三种很隐蔽是你在 Node 环境跑了setupServer然后浏览器环境也同时跑了一个 worker两个环境各有一个 handler 实例。你改了文件 A 的 handler但页面请求命中文件 B 的 handler自然不生效。排查时先看控制台的 handler 日志确认命中了哪条规则。5.5 问题排查速查表症状可能原因排查动作控制台没有 Mocking enabledworker 没启动检查 main.ts 的 worker.start()请求照常发到真实接口没被拦截handler 路径不匹配检查 URL 大小写、参数格式偶发几个请求没 mock 住worker 启动时存在竞态在入口处 await worker.start()改了 handler 不生效Service Worker 缓存强制刷新或清站点数据生产环境出现 mock 数据环境判断没生效检查 import.meta.env.DEV 条件TS 类型报错mock 数据和类型不一致用 zod 做运行时校验请求 404msw 版本不兼容检查 msw 版本和初始化命令没有网络请求记录被其他代理拦截检查 dev server 代理配置这个速查表是我在多个项目里碰过的所有问题的集合基本覆盖了 95% 的 msw 日常问题。最后再分享一个小技巧。做规则改写和断点拦截时我习惯在 console 里把命中情况打出来http.get(/api/stocks/:code, async ({ request, params }) { console.debug([mock] stocks detail, params.code, request.url) // ... })这个方法看似简单实际联调排查时比任何工具都管用。你能立刻看到哪些请求被 mock 了哪些没有响应数据长什么样。等真实接口切换完成后这些 debug 日志不用删因为 dev 环境才跑 mock生产不会输出留着反而方便后续排查。Mock 接口数据这件事做得好了前端开发速度和稳定性都能上一个台阶。规则改写和断点拦截这两个核心能力本身不复杂但要用得熟练还是得多在真实项目里练。希望这篇文章能帮你把 Mock 这块彻底整明白少走点我走过的弯路。
返回列表