资讯详情

资讯详情

Webpack本地Mock开发模式:从中间件到动态数据完整落地指南

做了多年前端工程化我可以很负责任地说一句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 MOCKfalse webpack serve, dev:mock: cross-env MOCKtrue 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互不干扰。生产构建时因为根本没有执行 devServermock 代码天然不会被打进产物里。但如果你在业务代码里也做了请求拦截式的 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 环境生效?mockDelay2000强制延迟 2 秒?mockTimeout1让接口返回 502?mockCode40002让接口返回业务错误码。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 和错误提示逻辑。我在做某个中后台系统的列表页时就靠?mockCode40002反复验证请求失败后保留旧数据并给出提示的交互比让后端故意断开服务高效多了。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里救出来。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →