
做了这么多年前端Mock劫持Ajax这件事几乎每个项目都躲不掉但我们团队最近一次重构就在这上面翻了个大跟头。测试环境接口数据全部乱套登录接口居然返回了一条商品列表排查了大半天才定位到是Mock的正则写得太宽把业务接口全劫持了。今天就把这些年玩Mock劫持Ajax积累的经验、踩过的坑和排查方法整理成文希望能帮各位前端老铁少走点弯路。不管你是刚入行的新人还是带了几年项目的老手只要你的项目里用过Mock.js、MSW或者vite-plugin-mock这类工具这篇内容都值得花十分钟看完。1. 为什么要用Mock劫持Ajax从联调痛点说起1.1 前后端联调的真实困境先聊一个场景。你在做前端页面后端接口还没开发完后端老哥甩给你一份Swagger文档说“你先按这个联调”。你一看文档倒是挺全但接口一个个调不通要么404要么返回一串看不懂的报错。这时候前端只能干等吗当然不是Mock就是干这个的。以前很多人的做法是在后端代码里写假数据接口或者前端代码里写死一堆if条件返回假数据。这俩方案都很别扭动不动就要改业务代码改完还得记得删。后来大家开始用Mock.js这类库直接在前端拦截Ajax请求把请求“劫持”到本地模拟数据上响应速度立刻上来前端再也不用看后端脸色了。我见过不少团队Mock用得挺早但用法还停留在“给按钮加个点击事件弹个假弹窗”的层面。真正到了复杂项目里比如多个模块并行开发、接口版本频繁变动、权限系统还没落地的时候简单的静态Mock根本撑不住。这时候就需要一套能灵活拦截不同URL、不同请求方法、不同参数的“劫持方案”。1.2 Mock劫持与传统Mock的本质区别很多人容易把Mock和劫持搞混。传统Mock比如手写个JSON文件丢在public目录里然后页面用相对路径去请求这种本质上是“绕过”网络前端直接读取静态文件。而Mock劫持是让请求照常发出去但在网络层或代码层把请求“截住”换成我们想返回的数据整个过程对业务代码来说是无感的。这么说吧传统Mock像你去餐厅点菜厨师没上班你直接走进后厨自己拿了份预制菜。而Mock劫持呢是你照样点菜、照样等上菜流程但服务员在半路把你的菜单给换了端上来的菜是提前准备的道具你根本不知道后厨有没有真做这道菜。这就是关键差异劫持方案更接近真实网络环境能模拟延迟、状态码、请求头、各种异常情况联调时测出来的问题更有参考价值。不过也正因为它是“把请求截住”一旦没控制好边界条件就容易翻车这也是后面要重点聊的部分。2. Mock劫持Ajax的底层原理拆解2.1 XHR层面的拦截思路先看最经典的一类方案以Mock.js为代表它的核心原理是在浏览器环境里重写XMLHttpRequest对象。具体来说Mock.js会在你调用Mock.mock()方法时把匹配规则注册进一个拦截器列表然后替换全局的XMLHttpRequest构造函数。当页面发起Ajax请求时走的是被替换过的XHR这个XHR内部会先检查请求地址和参数对不对得上拦截器列表里的规则匹配上了就直接在本地生成数据返回压根不经过网络栈。这个方案优势很明显兼容性好原生XHR、jQuery的$.ajax、axios默认适配器就是XHR都能覆盖到。不过它也有软肋对fetch API无能为力因为fetch是浏览器另一套网络请求体系不走XMLHttpRequest。现在很多新项目尤其是用了比较新版本的前端框架和工具链之后fetch的使用率越来越高了Mock.js这套就有点力不从心。另外重写XHR还有一个隐性问题就是它对请求的拦截是“全局”的一旦规则写得不严谨很容易把不该拦的请求也拦下来。我见过有人用一个宽泛的正则去匹配所有/api/开头的地址结果把一个走真实接口的第三方登录请求也给截了后果可想而知。2.2 Fetch与Axios的拦截差异说完XHR再说说fetch。如果你拦截的是fetch请求XHR层面的劫持方案完全不生效。那怎么办比较直接的做法是在代码里包一层比如定义一个自己的request函数内部先判断当前是否处于Mock模式如果是就直接返回模拟数据。但这么干的问题在于业务代码里到处都是import { request } from /utils/request一旦哪天你不用这个工具函数了换成了原生fetch或者别的库Mock逻辑就全失效了。另一个思路是拦截器方案比如axios的interceptors。axios本身就支持请求拦截器和响应拦截器你可以在请求拦截器里判断当前URL是不是Mock的URL如果是构造一条假的响应返回直接短路掉。这种方案的好处是逻辑集中不散落在各个业务文件里而且能精确控制到单个请求。不过拦截器方案有个体验上的坑因为请求在代码层面被“短路”了Network面板里看不到这条请求排查问题的时候容易懵总以为请求没发出去。我建议在做这种劫持时手动在console里打印一条mock日志标明“mock intercepted: GET /api/user/info”否则出了问题连从哪查起都不知道。2.3 Service Worker方案的原理再来看MSW这类方案的原理。MSWMock Service Worker走的是浏览器Service Worker通道。Service Worker本身是一个独立于页面线程的脚本可以拦截页面发出的所有网络请求包括XHR、fetch、图片、CSS几乎覆盖一切。MSW在这个基础之上封装了一套接口匹配规则你可以像写路由一样配置哪些请求要被拦截、返回什么数据。它的优势在于请求层面看起来最“真实”Network面板里能看到请求记录只是响应内容被替换成了Mock数据这在调试时非常友好。而且它不侵入业务代码也没有全局替换XMLHttpRequest这种危险操作。但Service Worker方案也有它的坑。第一需要在页面加载前启动Worker启动过程是异步的如果时序没处理好页面先发了请求Worker还没来得及拦截请求就打到真实后端了。第二Service Worker在本地开发环境好使但在某些特殊网络环境、或者用了某些安全策略比较严格的浏览器插件时兼容性会说翻车就翻车。第三它只能拦截浏览器发出的请求对Node环境里跑的单测、脚本请求是拦不到的。3. 主流Mock劫持方案选型与落地3.1 从业务场景出发选方案选哪种Mock劫持方案不能拍脑袋得看你的业务场景和团队情况。我根据实际经验整理了一张对比表供大家参考方案拦截层面XHRFetch学习成本生产隐患适用场景Mock.js重写XHR支持不支持低易误伤中小项目、快速原型Axios拦截器代码层支持不支持低侵入代码已统一封装请求的项目MSWService Worker支持支持中时序坑中大型项目、规范化团队vite-plugin-mockVite中间件支持支持低依赖构建环境Vite Vue3/React项目如果是Vue3 TS的项目我个人比较推荐vite-plugin-mock。它是在Vite开发服务器的中间件层做劫持请求发到开发服务器时就被拦住了根本不走浏览器那套网络栈所以XHR和fetch都能覆盖。更关键的是它天然支持TSmock数据能写类型定义对TS项目非常友好。下面我详细讲讲怎么配置。3.2 vue3 ts的mock使用教程先说安装。项目根目录执行npm install vite-plugin-mock mockjs -D这里把mockjs也装上是因为vite-plugin-mock很多时候是和mockjs配合生成随机数据的。接着在vite.config.ts里配置import { defineConfig } from vite import vue from vitejs/plugin-vue import { viteMockServe } from vite-plugin-mock export default defineConfig({ plugins: [ vue(), viteMockServe({ mockPath: ./mock, enable: process.env.NODE_ENV development, logger: true, }), ], })mockPath指定mock文件所在目录enable用环境变量控制只有开发环境才开启logger打开可以在控制台打印Mock日志。这里有个细节很多人会忽略enable一定要显式判断环境而不是直接写true。很多人图省事直接写true结果打包上线的时候mock的代码被打了进去万一里面的接口路径和生产环境路径撞了线上数据就被劫持了这种翻车我见过不止一次。然后在mock目录下建一个user.tsimport { MockMethod } from vite-plugin-mock export default [ { url: /api/user/info, method: get, response: () { return { code: 200, data: { id: 1, name: 张小明, role: admin, }, message: success, } }, }, ] as MockMethod[]这里有几个关键点。第一url要写完整路径不要用模糊匹配除非你真的需要匹配一组接口。第二response可以是一个函数函数里你可以根据请求参数动态返回数据比如从query里取id返回对应的用户信息。第三返回的数据结构要和后端约定好的结构完全一致比如code、data、message三层结构不要为了图省事只返回一个裸对象否则前端拿到的数据和真实接口对不上等于白Mock。如果你需要模拟延迟vite-plugin-mock也支持在response函数里包一层setTimeout或者直接返回一个Promise{ url: /api/user/list, method: get, response: () { return new Promise((resolve) { setTimeout(() { resolve({ code: 200, data: [], message: success, }) }, 500) }) }, }这对模拟弱网环境、测试Loading状态特别有用。我建议平时就开着500ms左右的延迟逼着自己在开发阶段就把Loading、空态、Error态都做了别等联调时才发现一堆状态没处理。4. 翻车实录那些年我踩过的Mock劫持坑4.1 生产环境没关Mock线上数据被劫持这个坑我真的不想再踩了。有一次项目上线前产品经理发现有用户反馈线上页面数据莫名其妙不对有的页面显示的是测试数据有的直接空白。我们当时第一反应是后端挂了后端排查了半天说接口正常。后来我看了一下Network面板发现请求返回的数据格式和真实接口完全不一样再一看代码好家伙Mock逻辑居然在production环境也被执行了。问题出在哪当时那个项目用的是Mock.js配置写在main.ts里直接无条件启动没有任何环境判断。最离谱的是当时项目里有一个接口的Mock规则是/api/goods而生产环境的真实接口是/api/goods/detailMock.js的正则匹配把detail请求也劫持了返回了一堆乱写的数据。后来我们的规范是凡是Mock相关代码必须有一个独立配置文件通过环境变量控制开关而且开关默认是关闭状态。上线前还要跑一个检查脚本扫描所有入口文件里有没有不受环境控制的Mock初始化代码。这个检查脚本虽然简单但真的救过我好几次。4.2 正则匹配过宽误伤兄弟业务线另一个典型翻车场景是多团队共用一个前端工程的时候。有次我们在做一个中后台系统订单模块和用户模块并行开发。负责订单模块的同事写Mock规则时为了省事直接写了一个/api/.*的正则。这玩意儿一挂上去整个系统的请求全被Mock拦截了用户模块的同事打开页面发现数据全是假的还以为是后端联调环境出了问题。排查的过程也很痛苦因为Mock.js劫持请求之后Network面板里其实看不出异常请求显示200响应也正常返回。后来是逐条注释Mock规则才定位到的。从那以后我定了一个铁律Mock的URL规则一律精确到接口路径不搞通配。实在要通配必须加上模块前缀比如/api/order/.*而且要在注释里写清楚这条规则覆盖了哪些接口为什么这么写。另外还有一点如果你用的是Mock.js正则匹配玩不转的话可以直接用字符串匹配。字符串匹配是精确匹配不会误伤。比如Mock.mock(/api/user/info, get, { ... })这种写法的匹配优先级高于正则而且更安全。能用字符串就用字符串别动不动就上正则在真的。4.3 异步时序问题首次请求逃出拦截网这个坑主要出在MSW和Service Worker方案上。MSW需要先调用worker.start()这个函数返回一个Promise。如果你在调用start()之前就发出了业务请求那么这些请求不会被Worker拦截会直接打到真实服务器结果就是CORS报错或者404页面一片红。我当时的处理方式比较粗暴在入口文件里直接await worker.start()然后再挂载Vue应用async function bootstrap() { if (import.meta.env.DEV) { const { worker } await import(./mocks/browser) await worker.start() } const app createApp(App) app.mount(#app) } bootstrap()这里用了动态import开发环境才加载Mocks代码生产环境完全不会打入包体。另外用await确保Worker启动完毕再渲染页面从根上避开了时序问题。后来我还遇到一个变种问题就是页面里有第三方脚本很早就发起了请求比如埋点SDK这类请求在Worker启动前就发出了虽然不影响业务但会在控制台刷一堆报错。这个不致命但我建议Mock的拦截规则只匹配业务接口第三方SDK的请求就别拦了。4.4 参数匹配不一致Mock静默失效还有一类坑特别隐蔽就是Mock规则看着没问题但它就是不生效。我见过有人写Mock.mock(/api/user/list?id1, ...)本意是根据query参数匹配但Mock.js在正则匹配的时候是把整个URL包括query串起来做正则测试的。如果你接口地址里query的顺序变了比如变成了/api/user/list?page1id1这个正则就匹配不上了请求穿透到真实后端去了。更隐蔽的是POST请求的body参数匹配。Mock.js的Mock.mock(url, method, template)只支持URL和method匹配不能根据POST body里的字段去匹配。如果你要实现“同一个URL参数不同返回不同数据”就得在response函数里手动解析bodyMock.mock(/api/user/update, post, (options) { const body JSON.parse(options.body || {}) if (body.type add) { return { code: 200, data: { id: Date.now() } } } if (body.type delete) { return { code: 200, data: true } } })所以我后来给自己定了个规矩看到Mock不生效这类问题第一反应不要怀疑代码写错先去看请求URL的query排序、body格式是不是和Mock规则一致。很多时候就是一个小细节没对上。5. 常见问题与排查技巧实录5.1 Mock劫持不生效到底卡在哪先整理一份我平时排查问题必看的速查表按症状定位原因效率会高很多症状可能原因快速排查方法请求返回真实数据而非Mock数据URL匹配不上、Mock未初始化、环境开关关闭console里搜mock日志Network里看请求状态请求返回404/500请求穿透到了真实后端且该接口不存在查看Network请求域名确认是否走了MockNetwork面板看不到请求代码层拦截axios拦截器短路了请求查找代码里是否有interceptors直接返回假数据页面数据是旧的验证数据生产环境执行了Mock代码检查入口文件环境判断搜isMock、mock等关键词部分请求被拦截了正则匹配过宽逐个检查Mock规则的URL匹配范围Mock不生效且只发生在首次加载Service Worker启动时序问题在入口文件await worker.start()后再渲染页面5.2 我常用的调试三板斧排查Mock劫持问题我一般就三招。第一招开console日志。不管用哪个Mock方案我都会确保能看到“哪条请求被Mock拦截了”的日志。vite-plugin-mock开启logger选项MSW默认会在console打印请求信息Mock.js没有现成的日志那就自己在response函数里手动打印。第二招看Network面板的请求详情。重点看两样东西请求的URL完整路径以及响应内容的数据结构。一旦发现响应内容的字段名和预期不一致基本可以断定请求是被Mock劫持了或者没被劫持走了真实接口但拿到的数据结构跟前端预期不匹配。这时对比一下Mock模板和真实接口的返回差异问题往往就浮出水面了。第三招临时绕过。怀疑某条规则有问题的时候不要急着改代码直接把Mock的初始化注释掉或者把拦截规则改成一个绝对不可能匹配到的URL然后刷新页面看请求走真实后端的表现。对比一下走Mock和不走Mock两种响应的差异很快就能定位到到底是Mock本身的问题还是Mock数据和真实数据不对齐的问题。5.3 独家避坑经验加一层白名单机制最后分享一个我自己的独家做法白名单机制。具体来说就是在Mock的配置文件里维护一个数组专门记录当前允许被Mock劫持的接口路径白名单const MOCK_WHITELIST [ /api/user/info, /api/user/list, /api/order/detail, ]然后在所有Mock规则的匹配函数里先做一个判断URL不在白名单里就直接返回null不执行Mock逻辑。这样就算有人误加了一条宽泛的正则白名单也能兜底保证只有明确列出的接口才会被劫持。白名单机制增加了一点维护成本但对于中大型项目、多人协作的场景这点成本换来的安全性非常值得。另外一个和Mock劫持看似无关、但实际很相关的建议是Mock数据一定要用类型定义约束。在TS项目里给每条Mock数据的响应写一份interface这个interface尽量从后端Swagger文档里生成保证字段名和类型完全对齐。这样前端在开发阶段拿到的数据结构和联调时真实接口返回的结构就是一致的不会出现Mock跑得好好的一接真实接口就报“undefined is not a function”这种事。6. 对Mock劫持定位的思考与个人体会做了这么多年前端我最大的感悟是Mock劫持Ajax这件事工具本身并不复杂难的是“边界控制”。什么该劫持什么不该劫持什么时候开什么时候关这些边界如果控制不好工具反而会变成事故现场。我见过太多项目Mock代码写得到处都是业务文件里一个if工具函数里一个switch配置文件里一个正则最后上线前排查Mock遗漏代码就跟扫雷一样没人敢保证扫干净了。所以我的建议是Mock劫持方案一定要集中管理最好所有Mock逻辑都收拢到一个目录下通过环境变量统一控制开关。哪怕项目不大也值得为Mock单独建一个目录而不是把Mock代码散落在业务组件里。另一个体会是Mock数据不能“太完美”。我早年写Mock数据都是规规矩矩的字段一个不缺状态码永远是200。这样的Mock数据用起来倒是顺手但联调的时候就傻眼了真实接口返回的字段可能为空、可能多出几个字段、可能格式和文档不完全一致。所以我建议Mock数据里故意造一些“脏数据”——空数组、null字段、异常状态码、超长文本。尽早让前端代码去处理这些边界情况比联调时再被现实拷打要舒服得多。最后再分享一个小技巧。如果你用的是vite-plugin-mock可以把Mock开关绑定到localStorage上而不是只靠环境变量。这样同一个开发环境里不同开发者可以按需开启或关闭Mock互不影响。比如你正在调支付模块的真实接口而同事在调用户模块的Mock数据你们共用一个开发服务器时localStorage级别的开关就能完美兼容两种需求。这个小技巧是我在一次多模块并行开发中摸索出来的实测下来救了不少次急。Mock劫持Ajax这条路说深不深说浅不浅。希望这篇翻车实录和避坑指南能帮你把工具用好、把坑躲开。如果你也有过类似的Mock翻车经历欢迎在评论区聊聊大家互相补补课。