首页 / 资讯中心 / 文章详情

Webpack 实战:基于 Dev Server 搭建本地 Mock 服务

Webpack 实战:基于 Dev Server 搭建本地 Mock 服务 ★ FEATURED ARTICLE
1. 本地 mock 的开发价值与方案选型1.1 为什么前端要折腾本地 mock这些年我先后参与过几个中后台项目有一个共通的痛点非常典型后端接口永远比前端页面晚一步。你也别指望后端能按时交付因为需求本身就在不断调整。如果前端老老实实等接口那项目排期基本可以按两倍来估。所以前端自己造一套本地 mock 数据不是为了偷懒而是为了把「依赖后端」变成「依赖自己」让页面开发、联调、自测完全不被阻塞。还有一类场景前后端已经联调完了但是测试环境不稳定动不动 502。你总不能因为环境挂了就停下手里的活吧。这时候本地 mock 的价值又出来了前端页面、交互逻辑、边界状态全部能在本地跑通哪怕后端服务彻底宕机照样能开发而且开发效率反而更高。说白了mock 的本质就是给前端创造一套可控的数据服务让数据的制造权掌握在开发者自己手里。这个标题里提到“Webpack 实战”说明不是简单用静态 json 文件模拟一下而是要让 mock 能力深度嵌进 Webpack 的 dev server 里做到启动项目即有接口、改接口即热更新、接口挂了有日志。有人可能会问单独开一个 node 服务不也能做吗确实能但这些额外的进程、端口、维护成本对于一个前端项目来说负担太重。把 mock 做进 Webpack 开发服务器里才是真正贴合前端日常开发节奏的做法。1.2 方案对比三种本地 mock 路径怎么选本地 mock 大体有三种路线我先帮你把水搅浑再一个个踩实。第一种是静态 JSON 文件。在项目的 mock 目录里放若干 JSON前端通过 loader 或者 devServer 静态资源映射直接请求。优点是极其简单缺点是只能处理 GET 请求无法模拟 POST PUT DELETE也无法根据请求参数动态返回不同数据。你做一个分页列表希望 page1 和 page2 返回不同内容静态文件就派不上用场了。第二种是独立的 mock 服务比如用 mockjs 在 node 层搭一个 express 服务单独跑在另一个端口前端通过代理把 /api 转发过去。这种方式很灵活功能也强大但缺点是项目里多了一套进程你启动 Webpack 之前得先启动 mock 服务部署到 CI 时还得想办法管理这个服务团队新人上手成本也会高一些。第三种就是我标题里说的“Webpack 本地 mock 模式”直接在 webpack-dev-server 基础上扩展中间件把接口拦截和响应放到 dev server 内部完成。这个方案能做到「一个命令启动、无额外进程、代码热重载、完全可控」而且在你需要代理真实后端时mock 和 proxy 可以平滑切换。我个人强烈推荐第三种后面的内容全部按这个思路展开。提示如果你的项目已经迁到了 Vite 或者其它构建工具本文的“通过 dev server 中间件做 mock”思路同样成立只是挂载方式不同。核心方法论是通用的。2. 搭建 mock 基础设施2.1 规划目录结构与配置入口动手写代码之前先把目录设计好。mock 代码要相对独立不要散落在各种业务代码里不然哪天清理起来非常痛苦。我常用的结构长这样project-root/ ├── mock/ │ ├── index.js # mock 入口统一注册所有接口 │ ├── utils/ │ │ ├── response.js # 统一响应包装 │ │ └── mock-data.js # mockjs 封装 │ └── modules/ │ ├── user.js # 用户模块接口 │ ├── order.js # 订单模块接口 │ └── dashboard.js # 看板模块接口 ├── webpack.config.js └── src/把所有 mock 逻辑收拢到一个独立目录里好处很明显项目里哪些代码是 mock 用的一目了然后续想整体移除直接删目录就行而且不会出现“某个接口的假数据散落在 5 个组件里”这种尴尬局面。入口文件mock/index.js要做的事情就是把所有模块的接口配置收集起来导出成一个中间件函数。模块文件则按业务域划分各自维护自己负责的那几个接口。职责清晰多人并行开发时冲突也能降到最低。2.2 webpack-dev-server 中间件的加载机制老一点的 webpack 版本里常用 devServer.before 来做接口拦截webpack 5 之后这套东西改成了setupMiddlewares。很多教程还在写 before/after但那些在新版本里已经不生效了。先看清楚机制再写代码。devServer.setupMiddlewares接收两个参数middlewares是 devServer 现有的中间件数组devServer是 dev server 实例。你需要返回一个新的中间件数组。也就是说我们可以在数组前面插入自己的 mock 中间件让请求先经过 mock再走 devServer 的静态服务和代理逻辑。顺便强调一下开发模式和生产构建的配置要分开。mock 代码只进 devServer绝不能打进生产包里。你在webpack.prod.js里根本就不挂这个中间件同时入口文件里也不要引用 mock 任何模块避免打包时把 mock 代码带进线上 bundle。2.3 写一个最基本的 mock 中间件直接看代码。这个中间件做的事情其实很简单拦截以/api/开头的请求在内存里查找匹配的接口配置匹配到就直接返回 JSON匹配不到就调用next()交给后面的代理中间件处理这样不会妨碍你调试真实环境。// mock/index.js const modules [ require(./modules/user), require(./modules/order), require(./modules/dashboard), ]; const routes modules.flatMap((mod) mod); function mockMiddleware(req, res, next) { const fullPath req.originalUrl.split(?)[0]; const route routes.find( (item) item.method req.method.toLowerCase() item.path fullPath ); if (!route) { return next(); } const delay route.delay ?? 200; setTimeout(() { const data typeof route.handler function ? route.handler(req) : route.handler; res.setHeader(Content-Type, application/json; charsetutf-8); res.end(JSON.stringify(data)); }, delay); } module.exports mockMiddleware;然后在 webpack 配置里注册// webpack.dev.js const mockMiddleware require(../mock/index); module.exports { // ...其他配置 devServer: { setupMiddlewares(middlewares, devServer) { middlewares.unshift(mockMiddleware); return middlewares; }, }, };这就是最小可行方案。够用但是不够好。你会发现几个问题接口路由全凭手写 path容易写错返回的数据没有统一 envelope比如{ code: 0, data: ... }结构也没有考虑请求体解析POST 请求的 body 拿不到。这些坑我在第三节逐步补上。3. mock 接口的编写与动态数据生成3.1 统一响应格式与模块化接口写法后端接口再千奇百怪大部分公司内部也有一套约定俗成的响应格式最常见的就是{ code, message, data }。mock 数据和真实后端保持一致意义特别大前端写的响应拦截器、错误提示、数据解构在任何环境下都一样不会出现“mock 跑得好好的一接真实接口就报错”的情况。我通常会在mock/utils/response.js里封装几个方法// mock/utils/response.js function success(data, message ok) { return { code: 0, message, data, }; } function error(code 500, message server error) { return { code, message, data: null, }; } module.exports { success, error };模块文件写起来就很清爽// mock/modules/user.js const { success, error } require(../utils/response); module.exports [ { method: get, path: /api/user/profile, handler(req) { const userId req.query.id; // 演示不同 id 返回不同名称 const nameMap { 1: 张三, 2: 李四 }; return success({ id: Number(userId), name: nameMap[userId] || 未知用户, avatar: https://example.com/avatar.png, }); }, }, { method: post, path: /api/user/update, handler(req) { if (!req.body || !req.body.name) { return error(400, name 不能为空); } return success(null, 更新成功); }, }, ];这里有个很关键的细节req.body能不能用取决于你是否引入了 body-parser 类的中间件。webpack-dev-server 默认不会帮你解析请求体。所以还要在setupMiddlewares里先挂一个 JSON 解析中间件或者用 devServer 自带的请求体解析能力。我在 3.3 小节补上这部分的坑。3.2 用 mockjs 生成动态随机数据写死的数据虽然简单但页面上的列表、图表不可能永远只有三四条。要模拟真实场景就需要随机数据。mockjs 是个非常成熟的工具它的语法很直观name生成姓名、integer(1, 100)生成随机整数、image生成图片占位地址。我封装了一个mock-data.js// mock/utils/mock-data.js const Mock require(mockjs); function listTemplate(count 20) { return Mock.mock({ [list|${count}]: [ { id|1: 1, name: cname, age|18-60: 1, email: email, status|1-3: 1, createTime: datetime(yyyy-MM-dd HH:mm:ss), }, ], }); } module.exports { listTemplate };然后在接口 handler 里直接调用// mock/modules/order.js const { listTemplate } require(../utils/mock-data); const { success } require(../utils/response); module.exports [ { method: get, path: /api/order/list, handler(req) { const page Number(req.query.page) || 1; const pageSize Number(req.query.pageSize) || 10; const temp listTemplate(pageSize); return success({ list: temp.list, total: 100, page, pageSize, }); }, }, ];用 mockjs 有几个小要领。第一cname生成中文名name是英文名别搞混第二|1属性是自增字段你在列表里经常要一个连续自增的 id这个写法最方便第三datetime默认返回的是yyyy-MM-dd HH:mm:ss格式如果想带毫秒得自己调格式第四mockjs 生成的图片用的外部占位服务国内环境不一定稳定建议在本地配置默认图片 URL。还有一点容易忽略mockjs 的随机占位符只在字符串里生效。如果你写成name: cname注意是字符串形式它才会被替换如果直接写函数返回则不会。这点在搭建 mock 数据时很多人都会踩我刚开始也被坑了一次。3.3 处理 GET 与 POST 等不同请求方式的差异不同 HTTP 方法最核心的差异就是参数位置不同。GET 在req.queryPOST 在req.bodyPUT 跟 POST 类似。但 webpack dev server 默认没有 body 解析能力所以就算你在 handler 里写req.body拿到的也可能直接是 undefined。解决办法有两个。一个是在 mock 中间件里自己解析原始请求体完全不依赖外部库另一个是在setupMiddlewares里显式挂载 express 的 json 解析中间件然后再挂 mock 中间件这样 mock 中间件里就能用req.body了。我推荐第二种因为代码更干净。在开发依赖里加一个express或者body-parser然后在配置里// webpack.dev.js const express require(express); const mockMiddleware require(../mock/index); module.exports { devServer: { setupMiddlewares(middlewares, devServer) { // 解析 JSON body 后再走 mock middlewares.unshift( express.json(), mockMiddleware ); return middlewares; }, }, };这里有个执行顺序的讲究unshift是把元素插到数组最前面所以后 unshift 的mockMiddleware反而在express.json()前面执行。如果你希望 mock 里能用 body就得把 json 解析放在 mock 前面。更稳妥的做法是用middlewares.unshift(express.json()); middlewares.unshift(mockMiddleware);或者直接用middlewares [mockMiddleware, ...middlewares]这种数组展开方式去控制顺序。实际调试中你会发现POST 接口处理逻辑大多和 GET 一样只是数据来源从 query 换成了 body把这两条路径统一好接口开发效率会提升很多。另外DELETE 请求既可能把参数放 query/api/order/delete?id1也可能放 RESTful path 里/api/order/1我们的 mock 中间件目前只会精确匹配 path所以遇到 RESTful 路径时需要在路由解析上做一点 pattern 匹配或者规定 mock 路径里就写带占位符的字符串。这个我在第四节展开。4. 进阶配置路径匹配、延迟与异常场景模拟4.1 支持 RESTful 风格路径与模糊匹配精确匹配在GET /api/user/profile这种场景下好用但你很快会遇到几个现实需求。第一个DELETE /api/order/123这种 RESTful 风格路径123 是动态的你没法穷举所有 id。第二个同一个路径可能需要根据 query 参数决定返回不同结构的数据。第三个有些接口返回数据本身就应该支持自定义参数匹配。我自己的做法是在 mock 入口的查找阶段引入一个简单的正则匹配逻辑。比如 path 里含:id占位符就先编译成正则再匹配// mock/utils/router.js function compilePath(path) { if (!path.includes(:)) return path; const segments path.split(/); const regexStr segments .map((seg) (seg.startsWith(:) ? ([^/]) : seg)) .join(/); return new RegExp(^${regexStr}$); } function matchRoute(route, req) { const fullPath req.originalUrl.split(?)[0]; if (typeof route.path string !route.path.includes(:)) { return route.method req.method.toLowerCase() route.path fullPath; } const regex compilePath(route.path); const match regex fullPath.match(regex); if (!match || route.method ! req.method.toLowerCase()) return null; const params {}; const keys route.path.split(/).filter((seg) seg.startsWith(:)); keys.forEach((key, index) { params[key.slice(1)] match[index 1]; }); return params; }在 handler 里就能直接拿到路径参数// mock/modules/order.js { method: delete, path: /api/order/:id, handler(req, params) { return success(params.id, 删除成功); }, }这样写的好处是mock 代码的表达能力和真实后端路由一致遇到 RESTful 接口能直接对应不需要靠字符串拼接来“骗”匹配。唯一要注意的是正则匹配对性能略有一点消耗但在开发环境下几十条路由完全无所谓别过度优化。4.2 模拟网络延迟与 loading 展示真实接口永远不会像本地 mock 那样瞬间返回为了让前端 loading 状态在联调时也有意义必须给 mock 接口加延迟。我在每个接口配置项里都支持一个 delay 字段{ method: get, path: /api/dashboard/overview, delay: 1000, handler(req) { return success({ ... }); }, }然后在 mock 中间件里用 setTimeout 包裹响应即可。这个字段可以按需设置列表查询 200ms详情查询 500ms上报接口 800ms甚至特意设置 2000ms 来验证 loading 文案、骨架屏和超时逻辑。对前端来说延迟模拟不是可有可无的装饰而是测试交互体验的必需手段。有一点值得注意如果你把延迟放得太高比如 3 秒而页面又在同一时间发起了 5 个请求页面体验会非常卡。这不是 mock 的问题而是你接口并发设计的问题。你在开发阶段就应该能感知到这个现象等接真实接口时心里才有预期。mock 延迟实际上帮你在早期暴露性能隐患。4.3 模拟异常分支超时、500、限流我见过太多项目前端把成功路径写得金光闪闪但异常路径却只在联调时靠后端的“偶发故障”去测试。mock 的价值之一就是把异常场景变成可控的“故意为之”。我在 mock 配置里支持了status和MockError两种方式。第一种直接返回 HTTP 状态码和非 0 业务码{ method: get, path: /api/order/list, handler(req) { const page Number(req.query.page) || 1; if (page 3) { return { code: 500, message: 服务器开小差了, data: null, }; } return success(...); }, }第二种用通用工具构造超时场景// mock/utils/response.js function timeout(time 10000) { return new Promise((resolve) { setTimeout(() resolve({ code: 408, message: 请求超时, data: null, }), time); }); }注意这里有个异步处理的细节mock 中间件里最好统一支持 handler 返回 Promise 的能力否则timeout()这种异步结果不会被正确等待。改造也很简单在 setTimeout 里判断结果是不是 Promise// mock/index.js 中响应部分 const result typeof route.handler function ? route.handler(req, params) : route.handler; if (result typeof result.then function) { result.then((data) sendResponse(data)); } else { sendResponse(result); }这样你的 mock 能力就基本追上真实后端了可以模拟超时、参数校验错误、权限不足、服务内部异常等。前端在处理这些分支时的代码在 mock 阶段就能完整跑通。4.4 按环境变量动态切换 mock 与实际代理开发到中后期后端接口陆续就绪时你不可能一个接口一个接口地手动切换 mock。更聪明的做法是给 mock 加一个总开关并且支持接口级别的覆盖。比如用环境变量MOCK_ENVall表示全部 mockMOCK_ENVnone表示全部走真实代理MOCK_ENVpartial时可以配合一个数组变量仅 mock 指定的接口。我常用的一种方式是在 devServer 的 proxy 配置和 mock 中间件之间做优先级控制。mock 中间件里如果发现当前请求路径命中了 mockRoutes 且开关状态为 mock则直接返回否则next()交给代理。也就是说mock 和 proxy 不是互斥的而是“先 mock、后代理”的链式关系。// mock/index.js const enabled process.env.MOCK_ENV ! none; // mockRoutes 里的接口若命中就返回 mock 数据否则走代理前端请求库依然用相对路径/api/xxxproxy 把未命中的请求转发到目标服务器。这样切环境只需要重启 dev server 时改一下环境变量不用动任何业务代码。我还试过更细粒度的一个配置在 mock 模块文件里给每个路由加一个enabled字段这个字段支持传入布尔值或者函数函数可以根据当前环境变量和请求参数决定是否启用 mock。这样你在联调某个具体接口时可以单独关掉它的 mock而其它接口继续 mock灵活性很高。5. 前端请求层的联动与开关控制5.1 请求封装与环境变量约定mock 做得再好如果和前端请求层没有约定好还是容易乱。最重要的约定是接口 base 路径必须统一。无论 mock 还是真实环境前端都请求同一个/api前缀只是背后被谁处理不同。这样你在代码里永远写request.get(/order/list)而底层通过 axios 的baseURL统一处理前缀和环境差异。环境变量的约定我建议这样设计环境MOCK_ENVbaseURL / proxy 说明本地开发all默认devServer mock 拦截或代理测试环境none走前端构建配置里的环境变量接口地址生产构建不启用打包产物只能指向真实接口在 axios 封装里可以用process.env.MOCK_ENV来控制一些行为。比如 mock 模式下打印更详细的路由日志真实模式下关闭。这不算复杂但能明显提升开发体验。5.2 确保 mock 代码不会进入生产包这是一个看似基础但极其容易踩雷的环节。如果 webpack 配置写得不够干净mock 的中间件代码很可能被当成普通模块打进生产 bundle。我自己就踩过一次某次上线之后线上页面所有接口都 404排查了半天发现是某个页面模块 import 了 mock 工具函数导致 mock 路由表跟着进了产出包而且 mock 中间件里的某些 node API 在浏览器环境直接报错了。规避办法有两个层面。第一业务代码里绝对不要直接引用mock/目录下的任何文件所有依赖都要通过请求层转发。第二webpack 生产配置里做显式排除比如resolve.alias指向一个空模块或者用webpack.IgnorePlugin将 mock 相关的文件忽略掉。最保险的做法还是从习惯上阻断mock 只作为 devServer 的一个配置项存在不是在业务模块里动态引用的代码。5.3 mock 模式下请求日志与调试技巧想搞清楚一个请求到底是被 mock 截住了还是走了代理还是压根没发出在真浏览器里看不出来。我给 mock 中间件加了一个日志输出在mockMiddleware前面挂一个 console.log 打印每次请求的 method、path、query以及是否命中 mock。这样你从终端就能直接看到[MOCK] GET /api/user/profile?id1 命中 [PROXY] GET /api/real/thing 未命中转发到 http://xxx这个小改进在联调排错时非常好使。刚才提到的 404 事故如果当时有这行日志排查时间至少能缩短一半。建议所有用 mock 的项目第一件事就是把日志输出加上。6. 常见问题与排查技巧实录6.1 接口被 proxy 吃掉匹配顺序不对模拟场景你在 mock 里配了GET /api/user/profile浏览器 Network 里也能看到请求发出但返回的明显是真实后端的格式而你的 mock 代码根本没有执行。大概率是中间件顺序的问题proxy 中间件在 mock 之前请求先被代理转发走了。解决方式很简单在setupMiddlewares里把 mock 中间件属性放在devServer.proxy之前处理。用middlewares.unshift(...)可以保证 mock 先执行或者干脆把 mock 和 proxy 做成同一个中间件链路在 mock 未命中时再显式调用next()让它走到代理里。顺带提醒一个问题如果你把proxy配置的context设置成/api而 mock 也期望拦截/api两边就会竞争。建议把 proxy 的 context 拆细尽量只代理后端真正提供的路径避免和 mock 大面积重名。6.2 POST 请求拿不到 body 数据这个问题我在 3.3 里已经提过这里统一记录一下。症状是handler 里req.body为 undefined或者req.body是字符串。破解方式有两个挂express.json()或者在 mock 中间件里自己拼接原始请求流再 JSON.parse。如果你用的是 webpack 5.30 以上的版本devServer 其实有个内置的devServer.middleware可以拿到解析后的 request但不同版本行为不一致自己挂 express.json 最稳定。6.3 mock 数据热更新改代码不重启webpack-dev-server 有热更新能力但热更新的范围一般限于业务模块。mock 中间件的代码如果在node_modules外被 devServer 进程加载改代码后默认不会自动 reload。你可能会陷入一个死循环改了 mock 数据刷新页面没效果以为是自己写错了查半天发现是旧代码还在内存里。解决办法是让 mock 入口文件参与模块热替换。最简单的方案是监听 mock 目录变更时强制触发 devServer 重启或者干脆让 mock 文件走 webpack 的 watch 机制。如果你用webpack-dev-server命令行跑可以加--watch-content-base配合contentBasePublicPath但实测对 mock 代码的生效速度一般。我更常用的做法是把 mock 入口做成可以被 webpack 编译处理的模块这样它在 watch 范围内。具体配置是用webpack-dev-middleware的watch: true或者让 mock 中间件通过 require 一个会被 webpack 监听的文件文件改动时触发中间件逻辑更新。如果不想搞这么复杂退而求其次的兜底方案是每次改 mock 文件后手动重启 dev server养成习惯就好毕竟 mock 代码不是每时每刻都在改。6.4 页面刷新后数据丢失mock 数据是内存态的devServer 重启后所有 mock 数据会回到初始状态。你在页面里一小时内创建了 50 条测试数据想刷新一下页面看看列表效果结果数据全没了。这个问题没法完全规避但可以缓解把常用基础数据放到静态 seed 文件里每次启动时加载对于临时修改的数据可以考虑采用 localStorage 备份或者用一个写文件的小工具把 mock 数据持久化到磁盘。不过这些属于锦上添花大部分场景下重启后回到初始数据反而是件好事保证大家的环境始终一致。6.5 中文乱码与响应头问题mock 返回中文文案时偶尔会出现乱码。通常是因为响应头里没有设置Content-Type: application/json; charsetutf-8。前文中间的代码里已经加了这个 header这里再强调一次。另外有些接口不是纯 JSON而是图片、文件流或者文本mock 时要注意设置对应的 Content-Type否则浏览器解析会异常。7. 我踩过几次坑之后的一点体会做了这么多年的前端工程化mock 这件事看起来不起眼但真正决定一个项目开发体验的往往就是这些基础环节。我最开始做 mock 就是放几个 json 文件后来才发现只有把 mock 做进构建链路里它才能真正成为前端工程的一部分而不是挂在旁边的临时脚手架。如果让我给刚上手的人一个建议那就是从小处着手先部署一个最小的中间件能拦截一个 GET 接口就行然后把响应格式、随机数据、延迟、异常分支这些能力一层层加进去。不要一开始就设计一个大而全的 mock 平台。mock 的本质是让开发“快”如果 mock 本身成了维护负担那就本末倒置了。最后再分享一个小技巧把 mock 中间的日志输出留到开发阶段永远开着等你要排查一个诡异问题的时候这个日志往往能帮你快速定位是请求层的问题还是数据层的问题。本地 mock 不是什么高深技术但它能让前后端协作的节奏稳下来这比任何花哨的工具都重要。
阅读完成 · 觉得有帮助?
咨询建站