做了多年前端工程化,我可以很负责任地说一句:mock 数据这件事,看起来简单,真想做到好用、不坑人、能长期维护,远比大多数人想象得更讲究。尤其是把 mock 集成进 Webpack 构建流程时,藏在配置背后的坑一个接一个。今天这篇就聊聊我落地过的 Webpack 本地 mock 开发模式,从方案选型、具体配置、动态数据模拟,到切换生产联调时的注意事项,尽量把操作细节和踩坑过程都讲透。这套模式适合谁?很直接:凡是前后端并行开发、接口文档先行但后端还没实现的团队,或者你想在本地开发时可控地模拟各种接口场景,都可以直接把文章里的思路拿过去用。无论是小项目快速起步,还是老项目做增量改造,这套做法都能接得住。1. 为什么要在本地开发里引入 mock 模式1.1 前后端并行开发时,前端不能干等很多团队的前后端联调节奏是这样的:后端先给接口文档,前端按文档开发页面,等后端环境就绪后再联调。但现实是,页面开发到一半,接口还没好,前端总不能对着真实接口干等。读接口文档猜数据结构,自己写死一段 JSON 先渲染,这是最原始的做法。写死的 JSON 放在组件里,等到联调那天又要翻代码去替换,改来改去,出 Bug 的概率极高。mock 模式要解决的第一个问题,就是把"临时写死的 JSON"变成"统一管理、可切换、靠近真实网络行为的模拟接口"。它不仅能让你在接口没实现时把页面跑起来,还能提前暴露请求方案、参数、返回结构之间的不一致,等到真联调时,改动面最小。我见过太多项目,前端在页面上硬编码了一个data = [...],后端的接口字段名只要改一个,前端得全局搜。引入 mock 之后,至少契约是被固定在 mock 文件里,后端改字段时前端拿到 mock 文件就知道哪里要动。1.2 mock 的真正边界:只模拟契约,不模拟复杂逻辑不过要提前说清楚,mock 不等于"把整个后端逻辑做一遍"。它的目标是模拟契约和边界条件,不是模拟业务算法。比如一个下单接口,你 mock 它返回{ code: 0, data: { orderId: 'xxx' } }就够了,不需要真的去算库存、算价格、调支付。把 mock 做复杂,维护成本会迅速超过它带来的收益。边界划分我给得比较明确:涉及第三方回调、异步任务结果、强业务规则计算的接口,不要放进 mock 里;状态简单的读接口、基础的增删改、带分页和筛选的列表接口,非常适合 mock。这样既保证了本地开发的效率,又不会让 mock 层变成一个庞大的模拟系统。一句话总结:mock 是给前端自己用的测试替身,不是给后端写的影子实现,别让它背上不属于它的责任。2. 方案选型:mock 在 Webpack 里有几条路可以走2.1 静态 JSON + 静态目录:最省事,但限制很明显最早我试过在项目里放一个mock目录,里面全是静态 JSON 文件,然后通过devServer.static或 Webpack 的copy-webpack-plugin让这些文件能以/mock/user.json的形式直接访问。这种方式的好处是零依赖、上手快,坏处也特别直接:只能响应 GET 请求,不能根据参数返回不同结果,不能模拟延迟,更不能模拟 500、超时之类的异常。假如你的接口大部分是"返回固定数据看页面效果",静态 JSON 勉强够用。可但凡牵扯到分页、搜索、登录态,静态 JSON 根本接不住。它们只能算 mock 的起步形态,不太适合作为一套长期维护的开发模式。现在再让我选,除非只是临时验证一个页面,否则我不会碰这个方案。2.2 devServer 中间件:最贴近真实网络的 mock 方式Webpack devServer 本质是一个 Express 服务,它允许在响应静态资源和代理之前插入自定义中间件。我们可以在中间件里注册一堆本地路由,请求打到/api/xxx时直接返回本地数据,不经过网络层。这比静态 JSON 强太多:可以读req.query和req.body区分参数、可以设置延迟、可以返回任意状态码,甚至能挂 cookie。我从这套方案里获得的最大收益是:页面里的请求代码和真实环境完全一致,不封装拦截器、不改baseURL。因为 mock 直接发生在 devServer 这一层,浏览器发出的请求路径和最终联调环境保持一致,天然没有跨域问题。如果后端联调环境需要跨域,devServer 本身的proxy也能解决掉,前端代码可以做到零改动切换模式。2.3 独立 mock 服务与代理转发:功能最强,但多一个进程还有一种做法是把 mock 做成一个独立服务(比如 json-server、或自建 Express mock 服务),然后在 Webpack 的devServer.proxy里把/api前缀代理到http://localhost:3000。好处是 mock 服务独立演进、甚至可以在团队内共享;坏处是要额外维护一个进程,启动脚本、端口占用、环境变量切换都会多一些成本。如果团队规模大、接口数量多,独立服务往往更合适。如果只是单人开发或小组协作,我建议优先用 devServer 中间件,省掉一整套进程管理。下面这套实战配置就以中间件方案为主,但它同样能平滑迁移到独立服务方案——把路由注册函数单独导出来,用 Express 包一层就能变成独立服务,迁移成本并不高。下表是我个人对三种方案的评价,供你按项目情况参考:方案上手成本支持动态参数模拟异常能力适合场景静态 JSON + 静态目录低弱弱临时看效果、原型验证devServer 中间件中较强较强小团队、单人开发、快速迭代独立 mock 服务 + 代理较高强强大团队、接口量大、需要共享 mock3. 实战配置:搭一套可维护的 mock 开发模式3.1 目录结构:mock 文件也要按领域模块拆分mock 一旦多起来,堆在一个文件里肯定不行。我常用的目录结构是这样:mock/ index.js # 统一加载所有路由模块 routes/ user.js # 用户相关接口 order.js # 订单相关接口 common.js # 公共配置、字典等接口 utils/ response.js # 统一响应格式、延迟处理工具index.js只做一件事:把 routes 目录下的模块收集起来,导出成统一的路由数组。每个路由模块导出一个配置对象,比如:// mock/routes/user.js module.exports = [ { url: '/api/user/info', method: 'get', response: { code: 0, data: { id: 1, name: 'Tom' } } }, { url: '/api/user/login', method: 'post', response: { code: 0, data: { token: 'fake_token_123456' } } } ];这样做的核心好处是:接口按领域拆开,负责用户模块的人不会和负责订单模块的人互相冲突;同时路由注册逻辑集中在index.js,改起来不分散。后端每修改一个接口字段,前端可以快速定位到对应的 mock 文件改掉。等到接口数量上了规模,这个目录结构还能无缝对接自动生成路由文档的工具。3.2 注册中间件:before 钩子与 setupMiddlewares 的版本差异很多朋友卡在配置这步,是因为 Webpack 版本不一样,devServer 的钩子名也变了。在 webpack-dev-server 4.x 之前的版本,配置里可以直接写before(app):// webpack.config.js module.exports = { devServer: { before(app) { app.get('/api/user/info', (req, res) => { res.json({ code: 0, data: { id: 1, name: 'Tom' } }); }); } } };但 webpack-dev-server 4.x 开始,before被标记弃用,改成了setupMiddlewares。新版写法要注意:默认的中间件数组middlewares必须原样返回,否则 devServer 的静态资源服务和代理会失效。// webpack.config.js 配合 webpack-dev-server 4.x+ const mockRoutes = require('./mock'); // 返回路由配置数组 module.exports = { devServer: { setupMiddlewares(middlewares, devServer) { mockRoutes.forEach((item) => { devServer.app[item.method](item.url, (req, res) => { res.json(item.response); }); }); return middlewares; } } };这里有个非常容易踩的坑:如果你写过middlewares.unshift(...)或者直接忘了 return,后面接的静态文件和代理规则全都白配。我第一次升级版本时就因为没返回middlewares,页面直接 404 了十分钟才反应过来。如果项目里既有老版本又有新版本,可以用一个自定义函数做兼容封装,根据setupMiddlewares是否存在来判断写法,避免团队里不同成员用不同版本时配置对不上。3.3 动态数据:让 mock 接口"活"起来静态返回固定 JSON 只能算入门,真正有价值的 mock 要能根据请求参数返回不同数据。这里我推荐两个层面结合起来做。第一层,在响应函数里读取请求参数。比如分页接口:module.exports = [ { url: '/api/order/list', method: 'get', handler(req, res) { const page = Number(req.query.page) || 1; const pageSize = Number(req.query.pageSize) || 10; // 假设 total 为 57,按 page/pageSize 动态切片 const allOrders = generateOrders(57); const list = allOrders.slice((page - 1) * pageSize, page * pageSize); res.json({ code: 0, data: { list, total: allOrders.length } }); } } ];第二层,引入 mockjs 生成随机但结构稳定的数据。mockjs 可以配合第一层直接使用:const Mock = require('mockjs'); function generateOrders(total) { return Mock.mock({ [`list|${total}`]: [ { 'id|+1': 1, 'orderNo': '@string("number", 10)', 'status|1': ['pending', 'paid', 'shipped', 'completed'], 'amount|100-9999.2': 100, 'createdAt': '@datetime' } ] }).list; }用 mockjs 时要注意:@string、@datetime这类随机值虽然看着真实,但有些字段是给排序、筛选用的,随机出来不一定符合业务预期。我给出的经验是:对参与计算或条件判断的字段,用可枚举值或固定规则模拟;对纯展示字段,随便随机没关系。比如订单状态要参与筛选,就固定从四个枚举里选,别让 mockjs 随机生成一堆前端没处理过的枚举值,否则页面状态展示必乱。延迟和异常也需要模拟。统一处理方式是在注册路由时包一层:function applyMockMiddleware(app, routes) { routes.forEach((route) => { app[route.method](route.url, (req, res) => { // 统一延迟 300~800ms,模拟真实网络 setTimeout(() => { if (req.query.mockError === '500') { res.status(500).json({ code: 500, message: 'server error' }); return; } res.json(route.response); }, 300 + Math.random() * 500); }); }); }延迟范围我一般控制在 300 到 800 毫秒。太短看不出加载态效果,太长会影响日常开发效率。这些都应该是可以通过配置项开关的,别写死在代码里。比如某个页面要专门测 Loading 状态时,可以用 parseMockOptions 里约定的 mockDelay 参数强制拉长延迟,而不是每次改代码重启。3.4 环境切换:开发、联调、生产三套环境的落地方式mock 模式最怕的事情是联调时忘了关、生产包还带着 mock 逻辑。我在工程里一般这样管理。用环境变量区分模式。在执行脚本里定义:{ "scripts": { "dev": "cross-env MOCK=false webpack serve", "dev:mock": "cross-env MOCK=true webpack serve" } }在 webpack.config.js 里读取:const isMock = process.env.MOCK === 'true'; module.exports = { devServer: { ...(isMock ? { setupMiddlewares(middlewares, devServer) { mockRoutes.forEach((item) => { /* 注册路由 */ }); return middlewares; } } : {}) } };这样做的好处是:dev跑的是真实后端代理,dev:mock跑的是本地 mock,互不干扰。生产构建时因为根本没有执行 devServer,mock 代码天然不会被打进产物里。但如果你在业务代码里也做了请求拦截式的 mock,那就必须靠DefinePlugin注入一个全局变量,在打包阶段把 mock 分支彻底摇掉。这个坑我在后面第五节单独展开,因为它太容易踩了。4. 把 mock 模式做得更贴近真实场景4.1 登录态与权限模拟很多页面的开发都要先过登录这一关。mock 登录态我一般用两个手段:一种是接口返回一个固定的 token,同时模拟一个/api/user/info接口,前端拿到 token 后再请求用户信息;另一种是直接在 mock 中间件里判断请求头是否带某个约定 token,没带就返回 401,方便前端测试跳登录逻辑。实际操作中,建议在 mock 路由里把"未登录返回 401"的校验抽出来做成公共中间件,而不是每个接口里都写一遍。比如:function authMiddleware(req, res, next) { const token = req.headers.authorization; if (!token) { res.status(401).json({ code: 401, message: 'unauthorized' }); return; } next(); }在注册路由时,对需要鉴权的接口先经过这个中间件。这样前端就能比较真实地验证登录失效、token 过期这类场景,而不至于把"没登录"和"接口失败"混在一起调。4.2 延迟、超时与错误码模拟本地开发时,网络波动基本感知不到,到了生产环境,接口偶发超时就会让页面出现大量 Loading 态。为了让前端提前处理这些边界情况,mock 接口需要能注入不同强度的异常。我的做法是约定一套查询参数,只在 mock 环境生效:?mockDelay=2000强制延迟 2 秒;?mockTimeout=1让接口返回 502;?mockCode=40002让接口返回业务错误码。function parseMockOptions(req) { const opts = { delay: 0, timeout: false, bizCode: null }; if (req.query.mockDelay) opts.delay = Number(req.query.mockDelay); if (req.query.mockTimeout) opts.timeout = true; if (req.query.mockCode) opts.bizCode = String(req.query.mockCode); return opts; }这个思路特别适合排查前端 Loading 和错误提示逻辑。我在做某个中后台系统的列表页时,就靠?mockCode=40002反复验证"请求失败后保留旧数据并给出提示"的交互,比让后端故意断开服务高效多了。4.3 列表、分页、筛选:最容易写坏的高频场景列表接口是 mock 里最常用也最容易写坏的部分。写坏通常体现在三个地方:总数和分页对不上、筛选条件不生效、排序字段不稳定。我的建议是维护一份"全量数据",所有列表接口都从这份数据里切片。筛选、排序先在内存里做一遍,再从结果里算分页,这样返回的total、list始终是可信的。代码如下:function getPagedData(allData, { page, pageSize, keyword, status }) { let filtered = allData; if (keyword) { filtered = filtered.filter((item) => item.name.includes(keyword)); } if (status) { filtered = filtered.filter((item) => item.status === status); } const start = (page - 1) * pageSize; return { list: filtered.slice(start, start + pageSize), total: filtered.length }; }前端分页组件多多少少会依赖返回的total来渲染总页数。如果 mock 里total是瞎写的,页面上的分页器一定会出现"最后一页没数据"或者"页数跳不对"的怪问题,排查时又很难怀疑到 mock 头上。提前写好这个工具函数能省下不少时间。5. 常见问题与排查技巧实录5.1 改完 mock 文件不生效,热更新是假的Webpack devServer 默认监听源码文件变化并触发重编译,但 mock 文件如果是被require进配置的,改动它并不会触发 devServer 重启。经常出现的情况是:你在mock/routes/user.js里改了返回字段,浏览器刷新以后接口返回还是老数据。解决方案通常是改完 mock 后手动重启 devServer。如果嫌麻烦,可以把 mock 文件纳入 watch 范围,在 webpack-dev-server 里配置watchFiles让它监听 mock 目录。不过我个人更习惯的做法是写一个 npm script,让 nodemon 在 mock 目录变化时自动帮你重启,省去手动操作的麻烦。核心逻辑就是:mock 文件和业务源码的变化触发机制要分开,别指望热更新帮你处理配置文件的变更。5.2 mock 路由和代理规则撞车如果你同时配置了 devServer 的proxy和 mock 中间件,并且它们都拦截/api,这时候出问题的概率很高。webpack-dev-server 4.x 的中间件执行顺序是:先执行setupMiddlewares里你注册的中间件,再执行代理。所以理论上 mock 优先于代理。但在 webpack-dev-server 3.x 里,顺序可能相反,尤其是before钩子和 proxy 的优先级关系在不同版本之间并不一致。我的建议是:不要在同一路径上同时开启 mock 和 proxy,用环境变量二选一。非要同时存在的话,在 mock 路由里统一加一个/mock前缀的前缀,比如提供'/mock/api/user/info',前端在 mock 模式下把请求前缀指过去,避免撞车。最怕的情况是代理指向真实后端,mock 中间件也拦截同一地址,结果线上页面连的是本地 mock、开发环境连的是真实后端,两边行为不一致,查起来特别头痛。5.3 mock 逻辑被打进生产包这个问题最常见于在业务代码里直接写if (process.env.NODE_ENV === 'development')并引入 mockjs 的做法。如果你把这个判断写在入口代码里,打包时会把整个 mockjs 库和所有 mock 数据都打进 chunk。要避免这一点,我建议:第一,mock 只能出现在 webpack 配置里,不要出现在业务代码里。第二,万一业务代码里必须保留一个开关,用DefinePlugin注入一个名为__MOCK_ENABLED__的全局变量,并在生产构建里设为false。new webpack.DefinePlugin({ __MOCK_ENABLED__: JSON.stringify(process.env.MOCK === 'true') });在业务代码里:if (__MOCK_ENABLED__) { // 仅在 mock 模式生效的拦截逻辑 }配合 Webpack 的 dead code elimination,生产构建会把这段永远为 false 的分支整个删掉。但如果你直接写process.env判断,某些构建工具只会保留判断而不会完全移除 mockjs 的引用,这才是体积膨胀的根源。我见过一个项目,业务代码里用了 mockjs 做拦截,构建后 bundle 体积大了 200KB,就是因为这个原因。5.4 后端接口字段改了,mock 文件没同步mock 做到后期,最大的敌人往往不是技术问题,而是契约变更的同步成本。后端接口文档改了一个字段名,前端 mock 文件忘了改,页面在 mock 模式下表现一切正常,等联调一开,渲染全崩。我目前比较推荐的做法是:在 mock 接口里加一层"契约字段校验",用一个轻量的工具对返回数据结构做断言。比如约定必填字段、字段类型、字段枚举值,注册路由时先校验一遍,不符合就打印红色警告。这样后端文档变更时,你至少能在 mock 模式下提前发现"示例返回"和"真实接口文档"之间的不一致。另一个实用技巧是把契约快照固化在 git 变更里。每轮接口调整,后端会更新文档,前端就在 mock 目录里建一个CHANGELOG.md记录每个接口的字段变更。这看起来土,但真的能让"联调时措手不及"的概率大幅下降。如果接口文档本身就存在 YAPI 或 apifox 这类平台,可以把 mock 文件生成脚本和文档平台打通,后端改完文档,前端 mock 文件自动生成,但这属于进阶玩法,小项目不必强求。6. 写到最后:mock 模式需要反复打磨的几个细节关于 mock 开发模式的工程化,我自己踩过不少坑,最后沉淀下来的核心体会是:mock 的本质不是让你"跳过联调",而是让前端在受控环境里把逻辑边界摸清楚,等真正联调时专注于双方协作问题,而不是把时间耗在页面渲染、Loading、报错这些本可以提前发现的细节上。最后再分享一个小技巧:给所有 mock 接口的响应体里加一个mock: true的标志位。联调切换真实环境后,如果某个请求的返回里还带着mock: true,说明切换没生效,浏览器控制台一眼就能看到。这个标志位成本几乎为零,但排查问题时特别直观。再说说持久维护的问题。mock 路由会随着接口数量增长到几十甚至上百条,这时候一定要把"路由注册"和"数据处理"分离,别在一个文件里又写路由又写随机数据函数。我通常把数据处理函数放到 mock/utils 下,路由文件只保留路由描述和响应逻辑,这样当某个接口逻辑复杂到必须独立测试时,数据处理函数可以被单元测试直接引用,而不会被迫启动整个 devServer。这套模式并不适合所有项目,如果你们后端环境长期可用、接口文档稳定到一个月不变,那 mock 投入产出比确实不高。但反过来看,只要接口还在并行开发期,或者你想让前端所有边界情况都有固定复现手段,本地 mock 开发模式都是值得认真做的一件事。希望这篇能把大家从"临时写死 JSON"里救出来。
做了多年前端工程化,我可以很负责任地说一句:mock 数据这件事,看起来简单,真想做到好用、不坑人、能长期维护,远比大多数人想象得更讲究。尤其是把 mock 集成进 Webpack 构建流程时,藏在配置背后的坑一个接一个。今天这篇就聊聊我落地过的 Webpack 本地 mock 开发模式,从方案选型、具体配置、动态数据模拟,到切换生产联调时的注意事项,尽量把操作细节和踩坑过程都讲透。这套模式适合谁?很直接:凡是前后端并行开发、接口文档先行但后端还没实现的团队,或者你想在本地开发时可控地模拟各种接口场景,都可以直接把文章里的思路拿过去用。无论是小项目快速起步,还是老项目做增量改造,这套做法都能接得住。1. 为什么要在本地开发里引入 mock 模式1.1 前后端并行开发时,前端不能干等很多团队的前后端联调节奏是这样的:后端先给接口文档,前端按文档开发页面,等后端环境就绪后再联调。但现实是,页面开发到一半,接口还没好,前端总不能对着真实接口干等。读接口文档猜数据结构,自己写死一段 JSON 先渲染,这是最原始的做法。写死的 JSON 放在组件里,等到联调那天又要翻代码去替换,改来改去,出 Bug 的概率极高。mock 模式要解决的第一个问题,就是把"临时写死的 JSON"变成"统一管理、可切换、靠近真实网络行为的模拟接口"。它不仅能让你在接口没实现时把页面跑起来,还能提前暴露请求方案、参数、返回结构之间的不一致,等到真联调时,改动面最小。我见过太多项目,前端在页面上硬编码了一个data = [...],后端的接口字段名只要改一个,前端得全局搜。引入 mock 之后,至少契约是被固定在 mock 文件里,后端改字段时前端拿到 mock 文件就知道哪里要动。1.2 mock 的真正边界:只模拟契约,不模拟复杂逻辑不过要提前说清楚,mock 不等于"把整个后端逻辑做一遍"。它的目标是模拟契约和边界条件,不是模拟业务算法。比如一个下单接口,你 mock 它返回{ code: 0, data: { orderId: 'xxx' } }就够了,不需要真的去算库存、算价格、调支付。把 mock 做复杂,维护成本会迅速超过它带来的收益。边界划分我给得比较明确:涉及第三方回调、异步任务结果、强业务规则计算的接口,不要放进 mock 里;状态简单的读接口、基础的增删改、带分页和筛选的列表接口,非常适合 mock。这样既保证了本地开发的效率,又不会让 mock 层变成一个庞大的模拟系统。一句话总结:mock 是给前端自己用的测试替身,不是给后端写的影子实现,别让它背上不属于它的责任。2. 方案选型:mock 在 Webpack 里有几条路可以走2.1 静态 JSON + 静态目录:最省事,但限制很明显最早我试过在项目里放一个mock目录,里面全是静态 JSON 文件,然后通过devServer.static或 Webpack 的copy-webpack-plugin让这些文件能以/mock/user.json的形式直接访问。这种方式的好处是零依赖、上手快,坏处也特别直接:只能响应 GET 请求,不能根据参数返回不同结果,不能模拟延迟,更不能模拟 500、超时之类的异常。假如你的接口大部分是"返回固定数据看页面效果",静态 JSON 勉强够用。可但凡牵扯到分页、搜索、登录态,静态 JSON 根本接不住。它们只能算 mock 的起步形态,不太适合作为一套长期维护的开发模式。现在再让我选,除非只是临时验证一个页面,否则我不会碰这个方案。2.2 devServer 中间件:最贴近真实网络的 mock 方式Webpack devServer 本质是一个 Express 服务,它允许在响应静态资源和代理之前插入自定义中间件。我们可以在中间件里注册一堆本地路由,请求打到/api/xxx时直接返回本地数据,不经过网络层。这比静态 JSON 强太多:可以读req.query和req.body区分参数、可以设置延迟、可以返回任意状态码,甚至能挂 cookie。我从这套方案里获得的最大收益是:页面里的请求代码和真实环境完全一致,不封装拦截器、不改baseURL。因为 mock 直接发生在 devServer 这一层,浏览器发出的请求路径和最终联调环境保持一致,天然没有跨域问题。如果后端联调环境需要跨域,devServer 本身的proxy也能解决掉,前端代码可以做到零改动切换模式。2.3 独立 mock 服务与代理转发:功能最强,但多一个进程还有一种做法是把 mock 做成一个独立服务(比如 json-server、或自建 Express mock 服务),然后在 Webpack 的devServer.proxy里把/api前缀代理到http://localhost:3000。好处是 mock 服务独立演进、甚至可以在团队内共享;坏处是要额外维护一个进程,启动脚本、端口占用、环境变量切换都会多一些成本。如果团队规模大、接口数量多,独立服务往往更合适。如果只是单人开发或小组协作,我建议优先用 devServer 中间件,省掉一整套进程管理。下面这套实战配置就以中间件方案为主,但它同样能平滑迁移到独立服务方案——把路由注册函数单独导出来,用 Express 包一层就能变成独立服务,迁移成本并不高。下表是我个人对三种方案的评价,供你按项目情况参考:方案上手成本支持动态参数模拟异常能力适合场景静态 JSON + 静态目录低弱弱临时看效果、原型验证devServer 中间件中较强较强小团队、单人开发、快速迭代独立 mock 服务 + 代理较高强强大团队、接口量大、需要共享 mock3. 实战配置:搭一套可维护的 mock 开发模式3.1 目录结构:mock 文件也要按领域模块拆分mock 一旦多起来,堆在一个文件里肯定不行。我常用的目录结构是这样:mock/ index.js # 统一加载所有路由模块 routes/ user.js # 用户相关接口 order.js # 订单相关接口 common.js # 公共配置、字典等接口 utils/ response.js # 统一响应格式、延迟处理工具index.js只做一件事:把 routes 目录下的模块收集起来,导出成统一的路由数组。每个路由模块导出一个配置对象,比如:// mock/routes/user.js module.exports = [ { url: '/api/user/info', method: 'get', response: { code: 0, data: { id: 1, name: 'Tom' } } }, { url: '/api/user/login', method: 'post', response: { code: 0, data: { token: 'fake_token_123456' } } } ];这样做的核心好处是:接口按领域拆开,负责用户模块的人不会和负责订单模块的人互相冲突;同时路由注册逻辑集中在index.js,改起来不分散。后端每修改一个接口字段,前端可以快速定位到对应的 mock 文件改掉。等到接口数量上了规模,这个目录结构还能无缝对接自动生成路由文档的工具。3.2 注册中间件:before 钩子与 setupMiddlewares 的版本差异很多朋友卡在配置这步,是因为 Webpack 版本不一样,devServer 的钩子名也变了。在 webpack-dev-server 4.x 之前的版本,配置里可以直接写before(app):// webpack.config.js module.exports = { devServer: { before(app) { app.get('/api/user/info', (req, res) => { res.json({ code: 0, data: { id: 1, name: 'Tom' } }); }); } } };但 webpack-dev-server 4.x 开始,before被标记弃用,改成了setupMiddlewares。新版写法要注意:默认的中间件数组middlewares必须原样返回,否则 devServer 的静态资源服务和代理会失效。// webpack.config.js 配合 webpack-dev-server 4.x+ const mockRoutes = require('./mock'); // 返回路由配置数组 module.exports = { devServer: { setupMiddlewares(middlewares, devServer) { mockRoutes.forEach((item) => { devServer.app[item.method](item.url, (req, res) => { res.json(item.response); }); }); return middlewares; } } };这里有个非常容易踩的坑:如果你写过middlewares.unshift(...)或者直接忘了 return,后面接的静态文件和代理规则全都白配。我第一次升级版本时就因为没返回middlewares,页面直接 404 了十分钟才反应过来。如果项目里既有老版本又有新版本,可以用一个自定义函数做兼容封装,根据setupMiddlewares是否存在来判断写法,避免团队里不同成员用不同版本时配置对不上。3.3 动态数据:让 mock 接口"活"起来静态返回固定 JSON 只能算入门,真正有价值的 mock 要能根据请求参数返回不同数据。这里我推荐两个层面结合起来做。第一层,在响应函数里读取请求参数。比如分页接口:module.exports = [ { url: '/api/order/list', method: 'get', handler(req, res) { const page = Number(req.query.page) || 1; const pageSize = Number(req.query.pageSize) || 10; // 假设 total 为 57,按 page/pageSize 动态切片 const allOrders = generateOrders(57); const list = allOrders.slice((page - 1) * pageSize, page * pageSize); res.json({ code: 0, data: { list, total: allOrders.length } }); } } ];第二层,引入 mockjs 生成随机但结构稳定的数据。mockjs 可以配合第一层直接使用:const Mock = require('mockjs'); function generateOrders(total) { return Mock.mock({ [`list|${total}`]: [ { 'id|+1': 1, 'orderNo': '@string("number", 10)', 'status|1': ['pending', 'paid', 'shipped', 'completed'], 'amount|100-9999.2': 100, 'createdAt': '@datetime' } ] }).list; }用 mockjs 时要注意:@string、@datetime这类随机值虽然看着真实,但有些字段是给排序、筛选用的,随机出来不一定符合业务预期。我给出的经验是:对参与计算或条件判断的字段,用可枚举值或固定规则模拟;对纯展示字段,随便随机没关系。比如订单状态要参与筛选,就固定从四个枚举里选,别让 mockjs 随机生成一堆前端没处理过的枚举值,否则页面状态展示必乱。延迟和异常也需要模拟。统一处理方式是在注册路由时包一层:function applyMockMiddleware(app, routes) { routes.forEach((route) => { app[route.method](route.url, (req, res) => { // 统一延迟 300~800ms,模拟真实网络 setTimeout(() => { if (req.query.mockError === '500') { res.status(500).json({ code: 500, message: 'server error' }); return; } res.json(route.response); }, 300 + Math.random() * 500); }); }); }延迟范围我一般控制在 300 到 800 毫秒。太短看不出加载态效果,太长会影响日常开发效率。这些都应该是可以通过配置项开关的,别写死在代码里。比如某个页面要专门测 Loading 状态时,可以用 parseMockOptions 里约定的 mockDelay 参数强制拉长延迟,而不是每次改代码重启。3.4 环境切换:开发、联调、生产三套环境的落地方式mock 模式最怕的事情是联调时忘了关、生产包还带着 mock 逻辑。我在工程里一般这样管理。用环境变量区分模式。在执行脚本里定义:{ "scripts": { "dev": "cross-env MOCK=false webpack serve", "dev:mock": "cross-env MOCK=true webpack serve" } }在 webpack.config.js 里读取:const isMock = process.env.MOCK === 'true'; module.exports = { devServer: { ...(isMock ? { setupMiddlewares(middlewares, devServer) { mockRoutes.forEach((item) => { /* 注册路由 */ }); return middlewares; } } : {}) } };这样做的好处是:dev跑的是真实后端代理,dev:mock跑的是本地 mock,互不干扰。生产构建时因为根本没有执行 devServer,mock 代码天然不会被打进产物里。但如果你在业务代码里也做了请求拦截式的 mock,那就必须靠DefinePlugin注入一个全局变量,在打包阶段把 mock 分支彻底摇掉。这个坑我在后面第五节单独展开,因为它太容易踩了。4. 把 mock 模式做得更贴近真实场景4.1 登录态与权限模拟很多页面的开发都要先过登录这一关。mock 登录态我一般用两个手段:一种是接口返回一个固定的 token,同时模拟一个/api/user/info接口,前端拿到 token 后再请求用户信息;另一种是直接在 mock 中间件里判断请求头是否带某个约定 token,没带就返回 401,方便前端测试跳登录逻辑。实际操作中,建议在 mock 路由里把"未登录返回 401"的校验抽出来做成公共中间件,而不是每个接口里都写一遍。比如:function authMiddleware(req, res, next) { const token = req.headers.authorization; if (!token) { res.status(401).json({ code: 401, message: 'unauthorized' }); return; } next(); }在注册路由时,对需要鉴权的接口先经过这个中间件。这样前端就能比较真实地验证登录失效、token 过期这类场景,而不至于把"没登录"和"接口失败"混在一起调。4.2 延迟、超时与错误码模拟本地开发时,网络波动基本感知不到,到了生产环境,接口偶发超时就会让页面出现大量 Loading 态。为了让前端提前处理这些边界情况,mock 接口需要能注入不同强度的异常。我的做法是约定一套查询参数,只在 mock 环境生效:?mockDelay=2000强制延迟 2 秒;?mockTimeout=1让接口返回 502;?mockCode=40002让接口返回业务错误码。function parseMockOptions(req) { const opts = { delay: 0, timeout: false, bizCode: null }; if (req.query.mockDelay) opts.delay = Number(req.query.mockDelay); if (req.query.mockTimeout) opts.timeout = true; if (req.query.mockCode) opts.bizCode = String(req.query.mockCode); return opts; }这个思路特别适合排查前端 Loading 和错误提示逻辑。我在做某个中后台系统的列表页时,就靠?mockCode=40002反复验证"请求失败后保留旧数据并给出提示"的交互,比让后端故意断开服务高效多了。4.3 列表、分页、筛选:最容易写坏的高频场景列表接口是 mock 里最常用也最容易写坏的部分。写坏通常体现在三个地方:总数和分页对不上、筛选条件不生效、排序字段不稳定。我的建议是维护一份"全量数据",所有列表接口都从这份数据里切片。筛选、排序先在内存里做一遍,再从结果里算分页,这样返回的total、list始终是可信的。代码如下:function getPagedData(allData, { page, pageSize, keyword, status }) { let filtered = allData; if (keyword) { filtered = filtered.filter((item) => item.name.includes(keyword)); } if (status) { filtered = filtered.filter((item) => item.status === status); } const start = (page - 1) * pageSize; return { list: filtered.slice(start, start + pageSize), total: filtered.length }; }前端分页组件多多少少会依赖返回的total来渲染总页数。如果 mock 里total是瞎写的,页面上的分页器一定会出现"最后一页没数据"或者"页数跳不对"的怪问题,排查时又很难怀疑到 mock 头上。提前写好这个工具函数能省下不少时间。5. 常见问题与排查技巧实录5.1 改完 mock 文件不生效,热更新是假的Webpack devServer 默认监听源码文件变化并触发重编译,但 mock 文件如果是被require进配置的,改动它并不会触发 devServer 重启。经常出现的情况是:你在mock/routes/user.js里改了返回字段,浏览器刷新以后接口返回还是老数据。解决方案通常是改完 mock 后手动重启 devServer。如果嫌麻烦,可以把 mock 文件纳入 watch 范围,在 webpack-dev-server 里配置watchFiles让它监听 mock 目录。不过我个人更习惯的做法是写一个 npm script,让 nodemon 在 mock 目录变化时自动帮你重启,省去手动操作的麻烦。核心逻辑就是:mock 文件和业务源码的变化触发机制要分开,别指望热更新帮你处理配置文件的变更。5.2 mock 路由和代理规则撞车如果你同时配置了 devServer 的proxy和 mock 中间件,并且它们都拦截/api,这时候出问题的概率很高。webpack-dev-server 4.x 的中间件执行顺序是:先执行setupMiddlewares里你注册的中间件,再执行代理。所以理论上 mock 优先于代理。但在 webpack-dev-server 3.x 里,顺序可能相反,尤其是before钩子和 proxy 的优先级关系在不同版本之间并不一致。我的建议是:不要在同一路径上同时开启 mock 和 proxy,用环境变量二选一。非要同时存在的话,在 mock 路由里统一加一个/mock前缀的前缀,比如提供'/mock/api/user/info',前端在 mock 模式下把请求前缀指过去,避免撞车。最怕的情况是代理指向真实后端,mock 中间件也拦截同一地址,结果线上页面连的是本地 mock、开发环境连的是真实后端,两边行为不一致,查起来特别头痛。5.3 mock 逻辑被打进生产包这个问题最常见于在业务代码里直接写if (process.env.NODE_ENV === 'development')并引入 mockjs 的做法。如果你把这个判断写在入口代码里,打包时会把整个 mockjs 库和所有 mock 数据都打进 chunk。要避免这一点,我建议:第一,mock 只能出现在 webpack 配置里,不要出现在业务代码里。第二,万一业务代码里必须保留一个开关,用DefinePlugin注入一个名为__MOCK_ENABLED__的全局变量,并在生产构建里设为false。new webpack.DefinePlugin({ __MOCK_ENABLED__: JSON.stringify(process.env.MOCK === 'true') });在业务代码里:if (__MOCK_ENABLED__) { // 仅在 mock 模式生效的拦截逻辑 }配合 Webpack 的 dead code elimination,生产构建会把这段永远为 false 的分支整个删掉。但如果你直接写process.env判断,某些构建工具只会保留判断而不会完全移除 mockjs 的引用,这才是体积膨胀的根源。我见过一个项目,业务代码里用了 mockjs 做拦截,构建后 bundle 体积大了 200KB,就是因为这个原因。5.4 后端接口字段改了,mock 文件没同步mock 做到后期,最大的敌人往往不是技术问题,而是契约变更的同步成本。后端接口文档改了一个字段名,前端 mock 文件忘了改,页面在 mock 模式下表现一切正常,等联调一开,渲染全崩。我目前比较推荐的做法是:在 mock 接口里加一层"契约字段校验",用一个轻量的工具对返回数据结构做断言。比如约定必填字段、字段类型、字段枚举值,注册路由时先校验一遍,不符合就打印红色警告。这样后端文档变更时,你至少能在 mock 模式下提前发现"示例返回"和"真实接口文档"之间的不一致。另一个实用技巧是把契约快照固化在 git 变更里。每轮接口调整,后端会更新文档,前端就在 mock 目录里建一个CHANGELOG.md记录每个接口的字段变更。这看起来土,但真的能让"联调时措手不及"的概率大幅下降。如果接口文档本身就存在 YAPI 或 apifox 这类平台,可以把 mock 文件生成脚本和文档平台打通,后端改完文档,前端 mock 文件自动生成,但这属于进阶玩法,小项目不必强求。6. 写到最后:mock 模式需要反复打磨的几个细节关于 mock 开发模式的工程化,我自己踩过不少坑,最后沉淀下来的核心体会是:mock 的本质不是让你"跳过联调",而是让前端在受控环境里把逻辑边界摸清楚,等真正联调时专注于双方协作问题,而不是把时间耗在页面渲染、Loading、报错这些本可以提前发现的细节上。最后再分享一个小技巧:给所有 mock 接口的响应体里加一个mock: true的标志位。联调切换真实环境后,如果某个请求的返回里还带着mock: true,说明切换没生效,浏览器控制台一眼就能看到。这个标志位成本几乎为零,但排查问题时特别直观。再说说持久维护的问题。mock 路由会随着接口数量增长到几十甚至上百条,这时候一定要把"路由注册"和"数据处理"分离,别在一个文件里又写路由又写随机数据函数。我通常把数据处理函数放到 mock/utils 下,路由文件只保留路由描述和响应逻辑,这样当某个接口逻辑复杂到必须独立测试时,数据处理函数可以被单元测试直接引用,而不会被迫启动整个 devServer。这套模式并不适合所有项目,如果你们后端环境长期可用、接口文档稳定到一个月不变,那 mock 投入产出比确实不高。但反过来看,只要接口还在并行开发期,或者你想让前端所有边界情况都有固定复现手段,本地 mock 开发模式都是值得认真做的一件事。希望这篇能把大家从"临时写死 JSON"里救出来。