1. 为什么前端总要自己造数据
做过前后端分离项目的人都有一个共同体会:页面骨架搭好了,样式调完了,接口文档也拿到了,但后端接口就是还没准备好。这时候你有三个选择——干等、写死数据、或者自己搭一个临时数据服务。干等浪费时间,写死数据改起来痛苦,而搭一个临时数据服务,是我这些年试下来最舒服的方案。
Mock.js加express这套组合,就是解决这个问题的经典搭配。express负责起一个本地 HTTP 服务,Mock.js负责按照规则生成看起来像真的的随机数据。两者一结合,你就能在本地跑起一个"假后端",前端该发请求发请求,该走axios走axios,代码一行都不用改,等真后端上线了,把请求地址一换就完事。
这篇文章适合谁看?如果你正在做前后端分离的项目,接口还没就绪但页面要联调;如果你在写前端 Demo 或者课程作业,需要一个能返回数据的服务;如果你在带新人,想让他们理解"请求-响应"这条链路到底怎么跑通——那这套方案你直接抄就行。我会从整体思路讲到具体实现,把参数怎么定、坑在哪里、怎么排查都摊开说清楚。
需要先说明一点:这套方案是本地开发阶段的临时手段,它的定位是"让前端不被后端阻塞",而不是替代真实后端。理解这个定位,后面很多取舍就顺了。
2. 整体方案设计与选型考量
2.1 为什么是 express 而不是别的
起本地服务这件事,能选的工具不少。json-server开箱即用,koa更轻,http模块最原始。我最终常用express,理由很实际。
第一,express的中间件模型足够直观。你要加跨域、加日志、加延迟模拟,都是往中间件链上一挂的事,代码读起来就是一条线。第二,生态成熟,遇到问题搜一下基本都有答案,不用自己造轮子。第三,它和Mock.js的配合几乎没有摩擦——Mock.mock()返回一个对象,res.json()直接吐出去,中间不需要任何转换。
json-server确实更省事,但它对"动态生成随机数据"的支持偏弱,它更适合你有一份固定 JSON 文件的场景。而实际开发中,我们往往需要每次请求返回不一样的数据,比如列表里十条记录的名字、时间、金额都不同,这样才能测出前端渲染有没有问题。这种"每次都不一样"的需求,Mock.js才是主角。
2.2 Mock.js 到底解决了什么
很多人对Mock.js的理解停留在"生成随机数",这就小看它了。它真正的价值在于数据模板这套语法。
你写'name|1-10': 'x',它就能生成 1 到 10 个字符的随机字符串;你写'age|18-60': 0,它就给你 18 到 60 的随机整数;你写'id|+1': 1,它还能自增。这套语法把"数据长什么样"和"数据怎么生成"解耦了,你描述结构,它负责填充。
更关键的是它内置了一堆语义化的占位符,比如@name、@email、@datetime、@city、@image。这意味着你不用自己维护一个姓名数组、城市数组,直接写占位符,出来的数据就有模有样。对于需要展示"用户列表""订单列表"这类页面的场景,这个能力省下的时间非常可观。
2.3 两者结合的分工边界
把职责划清楚,后面写代码就不会乱:
| 角色 | 负责的事 | 不负责的事 |
|---|---|---|
| express | 路由分发、请求方法处理、跨域、状态码、延迟 | 不关心数据内容长什么样 |
| Mock.js | 按模板生成数据内容、随机化、语义化字段 | 不关心请求从哪来、走什么路径 |
这个边界一旦清晰,你的服务代码就会非常薄——每个路由基本就是"取参数、调 Mock、返回"。薄的好处是改起来快,加一个接口就是加几行。
2.4 一个容易被忽略的设计点:接口契约
搭这个服务之前,我强烈建议先把接口契约定下来。所谓契约,就是路径、方法、请求参数、响应结构这四样。为什么强调这个?因为假数据的价值不只是"让页面有东西显示",更是"让前端提前按真实结构写解析逻辑"。
如果响应结构跟真后端不一致,等真接口上线,前端还是要返工。所以我的习惯是:拿到接口文档后,先照着文档把响应结构一比一还原,字段名、嵌套层级、分页格式全部对齐。Mock.js只负责往这个结构里填随机值,不负责改结构。这样切换真接口时,前端代码几乎零改动。
3. 核心细节与实操要点拆解
3.1 环境准备与依赖安装
先建一个独立目录,别混在业务项目里,避免依赖污染。初始化之后装两个包:
mkdir mock-server && cd mock-server npm init -y npm install express mockjs如果你希望改代码后自动重启,可以再装一个nodemon作为开发依赖:
npm install --save-dev nodemon然后在package.json的scripts里加一行"dev": "nodemon server.js",之后用npm run dev启动,改完保存自动生效,省去反复手动重启的麻烦。
注意:
Mock.js的包名是mockjs,全小写,没有点。装错成mock.js会报找不到模块,这个坑我见过不止一个新人踩。
3.2 数据模板的写法要点
Mock.js的模板语法里,最容易搞混的是"属性名里的竖线"。规则是:'属性名|规则': 值。竖线后面跟的是规则,值决定了生成逻辑。举几个我常用的例子:
const Mock = require('mockjs'); const userTemplate = { 'list|10': [ { 'id|+1': 1, 'name': '@cname', 'age|18-60': 1, 'email': '@email', 'city': '@city', 'createTime': '@datetime("yyyy-MM-dd HH:mm:ss")', 'status|1': ['active', 'inactive', 'pending'] } ] };这里有几个细节值得说。'list|10'表示生成 10 条记录的数组;'id|+1': 1表示从 1 开始每条自增 1;'status|1'后面跟数组,表示从数组里随机取一个。@cname生成中文姓名,@datetime可以带格式参数。
实操心得:
@datetime的格式字符串要用双引号包在括号里,写成@datetime("yyyy-MM-dd")。如果写成单引号,在某些版本下会解析异常,返回空字符串,排查起来很费劲。
3.3 路由与请求方法的对应
express里路由方法要和前端请求方法严格对应。前端用GET拿列表,你就写app.get;前端用POST提交表单,你就写app.post。方法对不上,请求会直接 404,而且报错信息不会告诉你"方法错了",只会说找不到路由,容易误导排查方向。
const express = require('express'); const Mock = require('mockjs'); const app = express(); app.use(express.json()); app.get('/api/users', (req, res) => { const data = Mock.mock(userTemplate); res.json({ code: 0, message: 'ok', data }); }); app.listen(3000, () => { console.log('mock server running at http://localhost:3000'); });app.use(express.json())这行别漏,它负责解析POST请求体里的 JSON。漏了的话,req.body会是undefined,你拿不到前端传的参数。
3.4 分页参数的处理逻辑
真实列表接口基本都带分页,假接口也得跟上,否则前端的分页组件没法测。处理思路是:从req.query里取page和pageSize,然后按这两个值切片。
app.get('/api/users', (req, res) => { const page = parseInt(req.query.page) || 1; const pageSize = parseInt(req.query.pageSize) || 10; const total = 86; const all = Mock.mock({ 'list|86': [userTemplate.list[0]] }).list; const start = (page - 1) * pageSize; const list = all.slice(start, start + pageSize); res.json({ code: 0, message: 'ok', data: { list, total, page, pageSize } }); });这里有个取舍:我先一次性生成 86 条,再切片。为什么不每次只生成pageSize条?因为分页要保证"总数固定",如果每次随机生成,总数会飘,前端算总页数就不准。先生成固定总数再切,逻辑上更接近真实后端。
注意:
parseInt一定要加,req.query里的值默认是字符串。不加的话(page - 1)会变成字符串运算,结果完全不对。
4. 完整实操流程与关键环节实现
4.1 从零搭起一个可用的服务
我把完整流程拆成六步,照着走一遍就能跑起来。
第一步,建目录、初始化、装依赖,前面已经说过。第二步,新建server.js,引入express和mockjs。第三步,定义数据模板,把每个接口需要的结构写清楚。第四步,写路由,把路径、方法、参数处理、响应组装串起来。第五步,加跨域中间件,解决前端本地调试的跨域问题。第六步,启动服务,用浏览器或接口工具验证。
跨域这块单独说一下。前端项目一般跑在另一个端口,直接请求会触发跨域限制。最省事的做法是加一个中间件:
app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); });OPTIONS那一段是处理预检请求的,不加的话,带自定义头的POST请求会失败。这个细节很多人第一次搭服务时会卡住。
4.2 模拟延迟,让加载状态可见
真实接口有网络延迟,假接口如果瞬间返回,前端的 loading 动画根本来不及显示,你就测不出加载态的逻辑对不对。所以我会主动加一点延迟:
app.use((req, res, next) => { const delay = Math.floor(Math.random() * 500) + 200; setTimeout(next, delay); });这段中间件让每个请求随机延迟 200 到 700 毫秒。这样前端的骨架屏、loading 转圈、按钮禁用这些状态都能被真实触发,联调时能提前发现"加载中重复点击"这类问题。
实操心得:延迟别设太大,超过 2 秒会拖慢你自己的调试节奏。200 到 700 毫秒这个区间,既能让加载态可见,又不至于让人等得烦躁。
4.3 覆盖增删改查四类接口
一个完整的假服务,至少要覆盖四类操作。下面是我常用的骨架:
// 查询列表 app.get('/api/users', (req, res) => { /* 分页返回 */ }); // 查询详情 app.get('/api/users/:id', (req, res) => { const data = Mock.mock(userTemplate.list[0]); data.id = parseInt(req.params.id); res.json({ code: 0, message: 'ok', data }); }); // 新增 app.post('/api/users', (req, res) => { const body = req.body; res.json({ code: 0, message: 'created', data: { id: Date.now(), ...body } }); }); // 删除 app.delete('/api/users/:id', (req, res) => { res.json({ code: 0, message: 'deleted', data: { id: req.params.id } }); });详情接口用:id做路径参数,req.params.id取到的就是路径里的值。新增接口把请求体原样返回并补一个id,这样前端能验证"提交后拿到新 id"的逻辑。删除接口返回成功即可,前端主要关心状态码和code字段。
4.4 响应结构的统一约定
我习惯把所有响应包一层统一结构:code、message、data。code为 0 表示成功,非 0 表示业务错误。这样做的好处是前端可以写一个统一的响应拦截器,不用每个接口单独判断。
function success(data, message = 'ok') { return { code: 0, message, data }; } function fail(code, message) { return { code, message, data: null }; }有了这两个辅助函数,路由里返回就变成res.json(success(data)),代码干净很多。想模拟错误场景时,直接res.json(fail(1001, '参数错误')),前端就能测异常分支了。
注意:错误场景一定要主动模拟几次。很多前端代码只测了成功路径,等真后端返回错误码时直接白屏。假服务阶段把错误分支跑通,能省下后面大量返工。
4.5 用配置文件管理多个接口
接口一多,全堆在server.js里会很难维护。我的做法是把数据模板和路由配置抽出来,用一个数组描述所有接口:
const routes = [ { method: 'get', path: '/api/users', handler: userListHandler }, { method: 'get', path: '/api/users/:id', handler: userDetailHandler }, { method: 'post', path: '/api/users', handler: userCreateHandler } ]; routes.forEach(r => { app[r.method](r.path, r.handler); });这样加接口就是往数组里加一项,server.js主体保持稳定。团队协作时,每个人负责自己的模块,冲突也少。
5. 常见问题与排查技巧实录
5.1 请求 404 的几种可能
404 是搭假服务时最高频的问题。按我的排查顺序,先看路径拼写,/api/users和/api/user差一个字母就是 404。再看请求方法,前端POST后端写GET,同样 404。最后看端口,服务起在 3000,前端请求 8080,那必然连不上。
还有一种隐蔽情况:路径里带了多余的斜杠,比如/api//users,express默认不匹配。这种问题用浏览器的网络面板看请求 URL 最直观,一眼就能发现。
5.2 跨域报错的定位方法
跨域报错在控制台会明确提示CORS相关字样。如果加了中间件还报错,八成是中间件的位置不对——它必须写在所有路由之前。express的中间件是按注册顺序执行的,写在路由后面就轮不到它处理。
另一个常见原因是预检请求没处理。带Content-Type: application/json的POST请求会先发一个OPTIONS,如果服务没返回 200,正式请求就不会发出。前面那段if (req.method === 'OPTIONS')就是干这个的。
5.3 数据不随机的排查
有时候你会发现每次刷新数据都一样,这通常是因为模板写成了固定值。检查一下属性名后面有没有加竖线规则,比如name: '张三'就是固定值,'name|1': ['张三', '李四']才会随机。
还有一种情况是Mock.mock()只调用了一次,结果被缓存复用。确保每次请求处理函数内部都重新调用一次Mock.mock(),而不是在模块顶层调一次存起来。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 请求 404 | 路径拼写错、方法不匹配、端口错 | 对照网络面板的请求 URL 逐项核对 |
| 跨域报错 | 中间件缺失或位置靠后 | 把 CORS 中间件移到路由之前 |
| 预检失败 | 未处理 OPTIONS 请求 | 对 OPTIONS 直接返回 200 |
| 数据不随机 | 模板写成固定值或只调一次 Mock | 加竖线规则,每次请求重新生成 |
| req.body 为空 | 未加 express.json() | 在路由前注册 JSON 解析中间件 |
| 分页总数飘 | 每次随机生成导致总数变化 | 先生成固定总数再切片 |
5.5 几个我踩过的坑
第一个坑是中文乱码。早期我没设响应头,某些环境下中文会显示成乱码。后来统一在返回前设置res.header('Content-Type', 'application/json; charset=utf-8'),问题就没了。虽然res.json()一般会自动处理,但显式设置更稳妥。
第二个坑是端口占用。服务启动报EADDRINUSE,说明端口被别的进程占了。换个端口,或者找到占用进程关掉。我习惯在启动日志里打印实际端口,避免自己都忘了改到哪。
第三个坑是模板嵌套过深。有次我写了一个三层嵌套的模板,结果生成的数据结构跟预期对不上,排查了半天。后来我的原则是:模板层级不超过两层,复杂结构拆成多个小模板组合,可读性和可调试性都好很多。
实操心得:调试模板时,先写一个最小模板单独跑,确认输出符合预期,再往大结构里拼。一次性写一大坨模板然后对着错误输出猜,效率极低。
6. 从假数据平滑切换到真接口
假服务的最终目的是被替换掉,所以切换过程要尽量无痛。我的做法是在前端统一管理请求基地址,用一个环境变量或者配置文件控制:
const BASE_URL = process.env.API_BASE || 'http://localhost:3000';开发阶段指向本地假服务,联调阶段改成真后端地址,代码里所有请求都基于BASE_URL拼接。这样切换只改一个地方,不用满项目找硬编码的地址。
切换时还要注意响应结构的差异。如果真后端的字段名和假服务不一致,前端的解析逻辑就要改。这也是我前面强调"假服务响应结构要一比一还原接口文档"的原因——结构对齐了,切换就是改个地址的事。
另外,假服务里模拟的错误码和真后端未必一致。切换后要重点回归异常分支,看看前端的错误提示、重试逻辑是否还正常。这一步容易被跳过,但恰恰是线上问题的高发区。
我个人在实际操作中的体会是,这套Mock.js加express的组合,最大的价值不在于"能生成随机数据",而在于它让前端在接口未就绪时依然能保持完整的开发节奏。页面能跑、请求能发、状态能测,等真接口来了,切换成本又极低。搭一次可能花你半小时,但省下的等待和返工时间,往往是这个数字的好几倍。如果你还没试过,下次遇到接口没就绪的情况,别再写死数据了,花点时间搭个假服务,后面会轻松很多。