Vue项目里的Mock实践:从写死数据到MSW工程化接入
如果你是做Vue前端的应该对这样一幕不陌生后端接口还没开发完前端只能先写好组件然后在代码里临时写死一个对象等联调时再一点点替换成真实接口。一旦接口字段有变前端代码跟着改好几处出了问题还要争论“是后端返回结构不对还是前端解析错了”。我做了几年Vue项目早期也是这么过来的直到有一次把一个列表页反复改了四遍才下决心把Mock这件事系统地做起来。这篇文章不是单纯介绍Mock.js怎么用也不是丢给你一个“一键生成假数据”的工具而是从Vue项目真实协作场景出发讲清楚Mock解决什么问题、当前主流方案怎么选、如何在ViteVue3TS项目里接入可维护的Mock体系以及我自己在落地过程中踩过哪些坑。1. 前端Mock到底在解决什么问题1.1 联调等接口的日常逼我重新认识Mock先说我自己的经历。几年前我在做一个后台管理系统产品需求排期很紧前端和后端同时开工。我负责的用户管理页面涉及列表查询、角色分配、状态开关三个接口后端排期晚我三天。那三天我干了一件事在Vuex的action里直接return一个写死的数组。页面看起来是“通”的但一旦涉及到分页参数、搜索条件、权限状态切换这些写死的逻辑根本走不通。等后端接口真正给出来我又返工改了整整一天。这个经历让我意识到前端开发时数据依赖不是一个“有没有数据”的问题而是一个“数据结构能不能稳定下来”的问题。Mock的核心价值就是让前端在真实接口出现之前先有一个稳定的、可交互的、能覆盖各种状态的数据来源。它把“前后端必须同时准备好才能联调”变成了“双方只要提前约定好接口格式就能并行推进”。1.2 Mock的本质是接口契约的提前落地很多人一听到Mock就会想到“造假数据”觉得这只是开发阶段的临时手段。我现在的理解更深入一点Mock的本质是接口契约的提前落地。一个接口契约包含哪些东西请求路径、请求方法、请求参数、响应结构、错误码、字段类型。后端没开发完成之前这些信息其实已经可以通过接口文档或者团队讨论确定下来。Mock数据就是把这些尚未编码的契约以代码的形式固定下来。前端拿Mock数据开发等于在验证这份契约是否合理字段够不够用嵌套结构是不是太深分页格式是否方便组件消费这些疑问如果等到联调阶段才暴露修改成本会高很多。所以我在团队里一直强调一个观点Mock数据写得好不好直接反映出一个团队对接口契约的重视程度。Mock不是用来“骗过页面”的而是用来“提前验证设计”的。1.3 哪些场景该Mock哪些场景别乱Mock虽然Mock很实用但它不是万能的。根据使用场景不同我会把Mock分为三类边界很清晰。第一类是接口联调前的开发阶段。这个阶段后端接口不存在或者不稳定前端需要独立开发页面Mock是必须的。第二类是自动化测试场景。跑单元测试或者E2E测试时不能真的依赖后端环境因为测试环境可能不稳定而且请求外部接口会让用例变得不可控这时候Mock也是必须的。第三类是接口出问题时专门模拟异常返回比如超时、500、空数据用来验证前端的异常处理逻辑。但有些场景我不会用Mock。比如做性能排查时如果Mock数据和真实接口在响应耗时、数据体量上差异很大得出的性能结论就是错的。再比如后端已经完成、正在联调的过程中不应该再用Mock数据去掩盖真实接口的报错否则问题会被推迟到上线前才暴露。场景是否该用Mock原因前端独立开发页面应该用接口未就绪需要稳定数据源单元测试/E2E测试应该用避免外部依赖保证用例稳定模拟异常数据与边界场景应该用真实接口难以稳定触发异常性能测试与线上问题排查不应该用Mock与真实环境差异大结论失真接口联调阶段谨慎用应优先暴露真实接口问题而非掩盖2. Vue项目里的Mock方案哪种更适合你Vue项目里可选的Mock方案不少但很多人并不知道它们的边界在哪里。我把主流方案拉出来对比一下你会发现每个方案都有自己的脾气。2.1 在Axios拦截器里造假简单但不彻底这是最简单也最容易想到的做法。在axios封装层判断一个标志位如果是Mock模式直接返回一个构造好的Promise。代码写起来很短适合小项目里临时顶一下。// api.js const isMock import.meta.env.VITE_USE_MOCK true export function getUserList(params) { if (isMock) { return Promise.resolve({ code: 0, data: [] }) } return request.get(/api/user/list, { params }) }这个方案的缺点是侵入性太强。业务代码里到处都要写if分支Mock逻辑和真实请求逻辑混在一起。一旦接口字段调整要同时改Mock分支和真实逻辑非常容易漏。项目一旦大起来这种代码就是灾难。2.2 Mock.js搭配线上拦截随机数据能力强Mock.js是老牌框架了它的核心能力是拦截XMLHttpRequest请求并按照模板生成随机数据。用起来很直观import Mock from mockjs Mock.mock(/api/user/list, get, { code: 0, data|1-10: [ { id|1: 1, name: cname, email: email, }, ], })Mock.js的优势在于生成随机数据的语法很成熟一行模板就能控制数组长度、字符串格式、自增ID。但它的问题也不少它拦截的是XMLHttpRequest如果项目使用fetch发请求默认情况下Mock.js是管不到的而且它内部对XHR的hack方式比较重偶尔会和项目的其他请求库产生冲突。还有一个隐患Mock.js生成的数据有时候“太随机”比如生成了一个超长字符串前端UI没做好长度适配页面直接错乱反而影响开发效率。2.3 json-server短平快起一个REST APIjson-server并不是一个前端库而是一个Node服务。它可以把一个JSON文件快速变成一个支持增删改查的RESTful API特别适合前端需要一个“真实后端”来联调的场景。npm install -g json-server json-server --watch db.json --port 3001它的思路是准备好db.json里面放好users、orders这样的数组json-server自动生成列表、详情、新增、修改、删除接口。配合Vite的proxy配置前端开发环境里请求/api/usersVite代理到http://localhost:3001/api/users整个链路和真实后端几乎没有区别。json-server的问题在于它是一条完整的HTTP服务无法便捷地编写复杂的业务逻辑。比如你需要根据用户角色返回不同的数据或者需要模拟登录后token鉴权就要写不少中间件。作为一个快速原型工具它很好用但在大型项目里很难承担复杂的Mock场景。2.4 MSW从浏览器网络层拦截请求MSW全称Mock Service Worker是目前我觉得最适合在Vue项目里长期维护的Mock方案。它利用Service Worker在浏览器层面的请求拦截能力把Mock逻辑从业务代码里彻底剥离出来。应用发出的请求照常走fetch或者axios但是到达网络之前被Service Worker“截胡”直接返回Mock数据。这种做法的好处非常明显业务代码完全不用关心是不是Mock环境请求怎么写Mock模式就怎么写。支持fetch和XMLHttpRequest两种请求方式。拦截逻辑放在独立的handler文件里与业务代码完全解耦。开发环境和测试环境可以复用同一套handler。请求路径、方法、参数匹配写起来非常直观。2.5 我的选型结论如果你只是在做一个临时DemoAxios拦截器或者Mock.js都够用。如果你要在一个正式的中大型Vue项目里搭建一套可持续维护的Mock体系我会优先推荐MSW。它在代码侵入性、可维护性、测试复用性上的优势是其他方案无法替代的。下面我整理的对比表格可以直观看出差异。Mock方案请求层拦截侵入业务代码支持fetch可维护性适合场景Axios拦截器Axios层高需自己适配低临时DemoMock.jsXHR层中不支持中快速原型json-server独立服务低支持中前后端联调MSWService Worker低支持高中大型项目3. 在ViteVue3TS项目里接入MSW的完整过程我现在的项目技术栈是ViteVue3TypeScript下面我把MSW从初始化到跑通的过程完整写出来每一步都附上代码和解释。3.1 安装MSW并初始化Worker脚本首先安装依赖。MSW在开发环境使用所以装到devDependencies里。npm install -D msw装完后执行初始化命令让MSW在Vite的public目录下生成Service Worker脚本。npx msw init public/ --save这条命令会生成public/mockServiceWorker.js。注意Vite默认会把public目录下的所有文件原样复制到打包产物根目录所以mockServiceWorker.js会在开发服务器根路径下被正确访问到。这一步很容易被忽略如果漏了执行后面会一直遇到“请求没有被拦截”的问题。3.2 按业务模块组织handler我习惯在src下建一个mocks目录和views、components同级结构如下src/mocks/ browser.ts server.ts handlers.ts data/ user.ts order.tshandlers.ts负责汇总所有业务模块的handler我这里的示例是用户模块和订单模块// src/mocks/handlers.ts import { http, HttpResponse } from msw import { getUserList, getUserDetail } from ./data/user import { getOrderList } from ./data/order export const handlers [ http.get(/api/user/list, ({ request }) { const url new URL(request.url) const page Number(url.searchParams.get(page) || 1) const pageSize Number(url.searchParams.get(pageSize) || 10) return HttpResponse.json(getUserList(page, pageSize)) }), http.get(/api/user/:id, ({ params }) { return HttpResponse.json(getUserDetail(Number(params.id))) }), http.get(/api/order/list, () { return HttpResponse.json(getOrderList()) }), ]browser.ts是浏览器环境下的启动入口主要负责用handlers去初始化Service Worker// src/mocks/browser.ts import { setupWorker } from msw/browser import { handlers } from ./handlers export const worker setupWorker(...handlers)3.3 在main.ts里按环境启动MockVue项目的main.ts通常是同步挂载App的但要启用MSW必须等Worker启动完成后再挂载否则页面一开始发出的请求可能不会被拦截。// src/main.ts import { createApp } from vue import App from ./App.vue async function enableMocking() { if (import.meta.env.DEV) { const { worker } await import(./mocks/browser) await worker.start({ onUnhandledRequest: bypass, }) } } enableMocking().then(() { createApp(App).mount(#app) })这里的核心是动态import(./mocks/browser)。这样生产环境构建时Mock相关的代码根本不会打进主包。import.meta.env.DEV是Vite内置的环境变量只在开发模式下为true。onUnhandledRequest: bypass的意思是没有命中handler的请求直接放行到真实网络这样即使有接口没有Mock也不会把整个调试环境卡死。3.4 在Vue组件里正常发请求验证Mock是否生效Mock接入完成后Vue组件里不需要做任何特殊处理。还是用平时正常的请求方式// src/views/UserList.vue import { ref, onMounted } from vue import axios from axios interface User { id: number name: string email: string } const users refUser[]([]) onMounted(async () { const { data } await axios.get(/api/user/list, { params: { page: 1, pageSize: 10 }, }) users.value data.data })跑起来之后打开浏览器开发者工具的Network面板你会发现/api/user/list这个请求是正常发出的并且返回了刚才在handler里定义的数据状态码是200。但它并不是真的发到了后端服务器而是在Service Worker里就被接住了。如果你在Network面板里看到请求是(from service worker)说明MSW已经在正常工作。这个细节是验证整套体系是否打通的最直观标志。4. Mock数据设计的三个层次与随机尺度很多人把Mock数据写成了“盘古开天辟地”式的随机数据一个string(50)就往前端塞结果页面样式直接崩掉。Mock数据设计同样需要分层我在项目里一般会分三个层次来处理。4.1 第一层固定JSON适合联调早期最基础的情况是接口结构刚定下来字段还没有完全稳定。这时候适合直接返回一个固定JSON尽可能保证字段和真实接口一致。比如登录接口// src/mocks/data/auth.ts export function getLoginResponse() { return { code: 0, data: { token: mock-token-abc123, userInfo: { id: 1, name: 张三, role: admin, }, }, } }固定JSON没有任何随机逻辑前端拿到的数据是完全可预测的。这样做的好处是排查问题时方便坏处是数据太“死”页面只能看到一种状态。如果列表只有一个固定项分页和加载更多的逻辑就没法验证。所以它适合接口刚起步、页面还在搭框架的阶段。4.2 第二层Factory函数生成结构化数据当页面开始联调具体功能时就不能只有一条固定数据了。我会把Mock数据改成Factory函数通过参数控制生成多少条、什么状态的数据。// src/mocks/data/user.ts function createUser(id: number, role: admin | user) { return { id, name: 用户${id}, email: user${id}example.com, role, createdAt: 2024-01-${String((id % 28) 1).padStart(2, 0)}, status: id % 3 0 ? disabled : active, } } export function getUserList(page: number, pageSize: number) { const total 35 const start (page - 1) * pageSize const list Array.from({ length: pageSize }, (_, i) { return createUser(start i 1, i % 2 0 ? admin : user) }) return { code: 0, data: { list, total, page, pageSize, }, } }思路是根据页码和每页条数动态生成对应数量且ID不重复的用户数组。状态字段status通过取余的方式在active和disabled之间交替方便前端验证不同状态的样式。这里使用user${id}而不是直接写死名字可以让每条数据在浏览器里看起来有区分度又不至于出现乱码级别的随机字符串。4.3 第三层随机数据的边界控制有些场景确实需要随机数据比如图表页需要大量点位数据或者需要生成很长的文本考察布局。这时候可以用随机函数但一定控制好边界。// src/mocks/data/chart.ts function randomNumber(min: number, max: number) { return Math.floor(Math.random() * (max - min 1)) min } export function getChartData() { const points Array.from({ length: 30 }, (_, i) ({ month: 2024-${String(i 1).padStart(2, 0)}, value: randomNumber(50, 300), growth: randomNumber(-20, 30), })) return { code: 0, data: points } }我建议所有随机数据都明确设定最小值和最大值不要让数据出现极端的负数、超长的字符串、超大整数。因为Mock数据的目的是辅助开发不是考验页面的鲁棒性。你要测试边界情况时可以单独写一个专门返回边界值的handler而不是让随机数据“蒙”出边界情况。比如空列表的handler、超长文本的handler单独管理起来比依赖随机更可靠。4.4 别忘了错误分支和异常状态Mock数据最容易忽略的就是接口错误的分支。前端做异常提示、错误重试、空数据占位的时候都需要特定返回。我会在handler里额外保留几个“错误接口”的Mock地址比如http.get(/api/user/list/error, () { return HttpResponse.json( { code: 500, message: 服务器开小差了 }, { status: 500 } ) })你可能会问真实项目里不会有人故意去请求一个带/error后缀的地址吧对但这个接口不是给业务请求用的是给开发者调试用的。当你在处理请求错误的UI时临时把组件里的请求地址改成这个/error地址就能立刻触发错误状态验证完再改回来。这种做法虽然朴素但在实际开发中特别顺手。5. Mock响应不生效的排查链路以及我踩过的真实坑接入MSW只是开始真正让人头疼的是“明明配好了数据却不生效”。我把自己用MSW以来遇到过的几个典型问题整理成排查链路按顺序查下去大部分问题都能解决。5.1 排查链路一Worker是否注册成功打开浏览器开发者工具在Application面板的Service Workers标签里看如果列表里没有mockServiceWorker.js说明Worker没有注册成功。常见原因是初始化命令没有执行或者public/mockServiceWorker.js被删了。重新执行一次npx msw init public/ --save然后刷新页面。注意开发服务器如果开着可能需要重启一次Vite进程因为public目录文件的改动不一定能被完整热更新。还有一个让人迷惑的点你需要在Application面板看到Worker处于activated状态。有时候面板显示的是activating页面刷新几次后才稳定。遇到这种情况耐心等一下再刷新通常能解决。5.2 排查链路二Vite对Worker脚本的处理如果你把项目部署到了某个子路径比如/admin/那么mockServiceWorker.js的路径也要调整。MSW的worker.start()支持通过serviceWorker配置项指定URLawait worker.start({ serviceWorker: { url: /admin/mockServiceWorker.js, }, onUnhandledRequest: bypass, })我之前在一个后台项目里就遇到过这种问题现象是MSW的handler配置完全正确但页面请求全部走真实网络。查了很久才发现是子路径部署导致Worker脚本URL不对。这个坑特别隐蔽因为页面不报错只是不拦截请求。5.3 排查链路三请求方法、路径与handler是否完全匹配确认Worker正常后再看handler。MSW的路径匹配是严格区分请求方法的http.get(/api/user/list)不会响应POST /api/user/list。前后端联调时我踩过一个坑后端给的接口文档写的是POST但开发时我按GET写了handler前端请求也用的GET看起来一切正常。后来后端接口改回POST前端请求改成POSTMSW直接不拦截了因为handler里还是GET。排查了半天才意识到方法不匹配。另外MSW v2的路径参数写法也需要注意。路径里的参数要用:开头比如/api/user/:id取参数时用params.id。如果你在handler里写正则或者{*}通配符要确保语法和当前MSW版本一致不同版本差异挺大。5.4 从Fiddler到MSW不要把代理工具思维带进来其实我最想单独说的是这个思维方式上的坑。团队里有些前端同学以前用Fiddler改响应数据习惯是在中途改掉某个字段、让浏览器看到一份被“篡改”的数据。但MSW的逻辑完全不同它是在请求发出前就拦截不依赖代理、不依赖本地网络配置。也就是说Fiddler改的是“服务端返回之后、浏览器收到之前”的数据而MSW改的是“浏览器发出去之后、网络到达之前”的数据。这个区别带来两个结论MSW只在开发环境里天然生效而Fiddler对任何环境都生效所以MSW更适合作为团队开发环境的标准配置。用Fiddler去Mock数据时数据是临时改的不会沉淀到代码仓库里用MSW时Mock逻辑在仓库里换一台电脑、拉一个新同事环境都是一致的。我发现很多团队Mock效率不高的原因不是工具不好用而是把Mock工具当成了调试工具在用。调试工具是“我在本地临时改一改”Mock工具是“整个团队共享的一套模拟环境”。想明白这一点你会更愿意把时间花在维护handler上而不是每次手工去改响应。5.5 几个实际落地时的建议最后分享几个我实际用下来觉得有帮助的经验。第一Mock handlers要跟着接口文档走不要自己发明接口结构。我见过有同学为了省事Mock数据里的字段跟后端文档对不上等联调时再返工。正确做法是Mock数据命名和接口文档保持一致后端文档里是userNameMock里就不要写成name。第二尽量让Mock数据有区分度。数组数据不要每一条都长一样至少ID要递增名称要能看出来变化这样你才能快速判断列表渲染是否真的生效。第三把MSW的启动条件严格限定在import.meta.env.DEV生产环境绝对不要让Mock代码参与进来避免出现“上线后页面还在走Mock数据”的尴尬事故。这一点我在最初几个项目里没有严格把控某次打包后忘了去掉Mock开关结果真实用户看到的还是假数据教训很深。把Mock从“临时方案”升级成“工程化基础设施”之后我再遇到后端接口延期的情况就不会慌了。反过头来想前端开发的很多焦虑其实不是来自技术难度而是来自对依赖项不可控的等待。Mock解决不了所有等待但能把等待变成有价值的并行开发。现在我在团队里推行的流程是接口定义先评审评审通过后前端立刻为这个接口编写Mock handler然后所有前端研发都在这套Mock环境上开发。后端接口完成后我们只需要把代理切到真实服务再做一轮联调回归。整个过程顺滑很多也不再有“前端等后端、后端等前端”的死锁。如果你所在的项目还停留在“手工写死数据”的阶段我建议你从这个周末开始花半天时间把MSW接进去你会回来感谢自己的。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →