1. 为什么我放弃了手写假数据这条路
刚入行那会儿,前端联调最头疼的事就是后端接口还没影,页面却得先跑起来。那时候我的做法很原始:在组件里直接写一个const data = {...},把假数据硬编码进去。页面确实能渲染,但问题很快就来了——产品经理说“这个列表再加二十条看看滚动效果”,我得手动复制粘贴;测试同学说“帮我模拟一下接口报错的情况”,我又得去改代码;最要命的是,等后端接口真上线了,我还得把散落在各个组件里的假数据一个个删掉,稍不留神就漏掉一处,上线后页面白屏。
后来我试过用本地 JSON 文件配合fetch请求,情况好了一点,至少数据集中管理了。但新的麻烦又冒出来:想加一条数据得手动改 JSON 文件,想模拟分页得自己写逻辑,想模拟增删改查更是得把整个文件读写流程重来一遍。说白了,我是在用最笨的方式模拟一个本该由服务端提供的能力。
直到有一次,一个做全栈的朋友看我还在手动改 JSON,随口说了句“你怎么不用 json-server”。我当时的第一反应是:又是个要学新东西的工具,值得吗?结果花了一个下午跑通之后,我直接把项目里所有硬编码的假数据全删了。这个工具的核心价值就一句话:你给它一个 JSON 文件,它还你一套完整的 RESTful 接口,增删改查、分页、排序、过滤全都有,零后端代码。
这篇内容适合所有正在做前后端分离开发的前端同学,尤其是那种“后端接口还没好,但页面必须动起来”的场景。不管你是刚接触前端的新手,还是做了几年但一直用土办法模拟数据的老手,json-server 都能帮你把联调效率拉高一个档次。接下来我会从它到底解决了什么问题讲起,把安装、配置、进阶用法、踩坑经验全部拆开揉碎,让你看完就能直接在自己的项目里用起来。
2. json-server 到底替你干了哪些活
2.1 一句话理解它的工作原理
json-server 的本质是一个基于 Node.js 的轻量级 HTTP 服务。你准备一个db.json文件,里面按照约定的格式放好数据,然后在命令行敲一行json-server --watch db.json,它就会在本地起一个服务,把文件里的每个顶层字段映射成一组 RESTful 路由。
举个例子,你的db.json长这样:
{ "users": [ { "id": 1, "name": "张三", "role": "admin" }, { "id": 2, "name": "李四", "role": "editor" } ], "articles": [ { "id": 1, "title": "第一篇", "authorId": 1 } ] }启动服务后,你就能直接访问这些接口:
| 操作 | 请求方法与路径 | 说明 |
|---|---|---|
| 查全部用户 | GET /users | 返回 users 数组 |
| 查单个用户 | GET /users/1 | 返回 id 为 1 的用户 |
| 新增用户 | POST /users | 请求体带用户数据,自动生成 id |
| 全量更新 | PUT /users/1 | 替换 id 为 1 的整条记录 |
| 局部更新 | PATCH /users/1 | 只更新请求体里带的字段 |
| 删除用户 | DELETE /users/1 | 删除 id 为 1 的记录 |
这套路由规则完全遵循 RESTful 规范,也就是说,你前端代码里用 axios 或者 fetch 写的请求,等后端真接口上线后,只需要把 baseURL 从http://localhost:3000换成真实域名,其他代码一行都不用改。这就是它最大的价值——让你的前端请求代码从第一天起就是“生产级”的,而不是等接口好了再返工。
2.2 它和 Mock.js、MSW 这些工具的区别在哪
市面上模拟数据的方案不少,我简单说说我的选型逻辑。Mock.js 擅长的是“生成随机数据”,比如随机姓名、随机日期、随机段落,但它不提供 HTTP 服务,你得自己搭一个服务去接它。MSW(Mock Service Worker)走的是拦截请求的路子,在浏览器层面把请求截住返回假数据,好处是不需要真实起服务,但配置相对复杂,而且它模拟的是“请求拦截”而不是“真实接口”。
json-server 的定位很明确:它就是一个真实运行的 HTTP 服务。你的浏览器发出去的请求是真的走网络层的,你能在 Network 面板里看到完整的请求和响应,能测跨域,能测请求头,能测状态码。这种“真实感”是拦截式方案给不了的。而且它的学习成本极低,一个 JSON 文件加一行命令就能跑,不需要写任何 JavaScript 配置。
当然它也有短板,比如不支持复杂的业务逻辑校验、不支持 WebSocket、数据持久化就是直接写文件。但对于前端开发阶段的数据模拟来说,这些短板基本不影响,因为你要的就是“快速把页面跑起来”。
2.3 什么场景下它最香,什么场景下别硬上
根据我这几年的使用经验,json-server 在下面这些场景里几乎是神器级别的存在:
- 前后端分离项目初期:后端还在设计数据库表结构,前端已经要开始写页面了,这时候先用 json-server 把接口定下来,前后端并行开发。
- 组件库或页面的独立开发:你在做一个通用表格组件,需要不同结构的数据来测试渲染效果,json-server 可以快速切换数据源。
- 演示和原型验证:给客户或者领导演示功能,不想依赖后端环境,本地起一个 json-server 就能完整跑通增删改查。
- 自动化测试的数据准备:测试用例需要预置一批数据,用 json-server 起一个干净的数据库,每次测试前重置。
但下面这些情况我就不建议用它了:需要模拟复杂的鉴权流程(比如 token 刷新、权限校验)、需要模拟文件上传下载、需要模拟服务端计算逻辑(比如根据参数动态计算结果)。这些场景要么用 MSW 配合自定义逻辑,要么直接搭一个简单的 Node 服务更合适。
3. 从零跑通第一个模拟接口
3.1 安装与最小可用配置
安装方式有两种,我推荐全局安装,因为这样在任何目录下都能直接敲命令:
npm install -g json-server如果你不想污染全局环境,也可以用npx直接运行,效果一样:
npx json-server --watch db.json安装完成后,在任意目录下新建一个db.json文件,内容就写一个空对象{}也行,然后执行:
json-server --watch db.json你会看到终端输出类似这样的信息:
\{^_^}/ hi! Loading db.json Done Resources http://localhost:3000/users http://localhost:3000/articles Home http://localhost:3000默认端口是 3000,如果被占用了,可以用--port参数指定其他端口,比如--port 3001。启动后打开浏览器访问http://localhost:3000,你会看到一个内置的首页,列出了当前所有可用的资源路由,点进去还能直接对数据进行增删改查操作。这个内置页面在演示的时候特别好用,不用写任何前端代码就能展示数据操作效果。
提示:
--watch参数的作用是监听db.json文件的变化。你手动改了文件内容,服务会自动重新加载,不需要重启。这个参数强烈建议每次都带上。
3.2 db.json 的数据组织逻辑
db.json的结构决定了你有哪些接口。顶层每个 key 就是一个资源名,对应的值必须是一个数组(或者对象,后面会讲嵌套场景)。数组里的每条记录建议都带一个id字段,类型可以是数字也可以是字符串,json-server 会自动用它来生成/资源名/:id这样的路由。
我一般会这样组织一个项目的模拟数据:
{ "users": [ { "id": 1, "name": "王五", "email": "wangwu@example.com", "department": "研发部" }, { "id": 2, "name": "赵六", "email": "zhaoliu@example.com", "department": "产品部" } ], "projects": [ { "id": 1, "name": "内部管理系统", "ownerId": 1, "status": "active" }, { "id": 2, "name": "数据看板", "ownerId": 2, "status": "archived" } ], "tasks": [ { "id": 1, "projectId": 1, "title": "登录页开发", "done": false }, { "id": 2, "projectId": 1, "title": "权限模块", "done": true } ] }这里有个关键点:资源之间的关联关系靠字段值来维护,而不是靠数据库外键。比如projects里的ownerId指向users里的id,tasks里的projectId指向projects里的id。json-server 本身不会帮你做关联查询,但你可以利用它的参数查询能力在前端自己组装,或者用后面会讲到的_expand参数。
3.3 用 curl 或 Postman 验证接口是否正常
服务起来之后,别急着写前端代码,先用命令行工具验证一下接口通不通。我习惯用 curl,因为最直接:
# 查全部用户 curl http://localhost:3000/users # 查单个用户 curl http://localhost:3000/users/1 # 新增一个用户 curl -X POST http://localhost:3000/users \ -H "Content-Type: application/json" \ -d '{"name":"孙七","email":"sunqi@example.com","department":"测试部"}' # 修改用户 curl -X PATCH http://localhost:3000/users/1 \ -H "Content-Type: application/json" \ -d '{"department":"架构组"}' # 删除用户 curl -X DELETE http://localhost:3000/users/2每执行一次写操作(POST、PUT、PATCH、DELETE),你回头去看db.json文件,会发现内容已经被同步修改了。这就是 json-server 的持久化机制——所有变更直接写回文件。这个特性有好有坏,好处是数据不会丢,坏处是如果你测试时乱改数据,文件会变得很乱,所以建议把db.json纳入版本管理,改乱了直接git checkout还原。
注意:POST 请求如果不带
id字段,json-server 会自动生成一个递增的数字 id。如果你手动传了 id,它会用你传的值。但如果你传的 id 和已有记录冲突,它不会报错,而是直接覆盖,这个行为要留意。
4. 让模拟数据更贴近真实业务的几个进阶玩法
4.1 分页、排序、过滤:不用写一行后端代码
真实业务里列表页几乎不可能一次性返回所有数据,分页、排序、按条件筛选是标配。json-server 内置了这些能力,全部通过 URL 查询参数实现,前端该怎么传参就怎么传,完全不用改。
分页用_page和_limit两个参数:
# 第 2 页,每页 5 条 curl "http://localhost:3000/users?_page=2&_limit=5"返回的响应头里会带X-Total-Count字段,告诉你总共有多少条记录,前端分页组件可以直接用这个值算总页数。
排序用_sort和_order:
# 按 id 降序 curl "http://localhost:3000/users?_sort=id&_order=desc" # 多字段排序:先按部门升序,部门相同的按 id 降序 curl "http://localhost:3000/users?_sort=department,id&_order=asc,desc"过滤更灵活,任何字段都可以直接作为查询参数:
# 查部门为研发部的用户 curl "http://localhost:3000/users?department=研发部" # 模糊匹配:名字里包含“王”的用户 curl "http://localhost:3000/users?name_like=王" # 范围查询:id 大于等于 2 且小于等于 5 curl "http://localhost:3000/users?id_gte=2&id_lte=5" # 多条件组合 curl "http://localhost:3000/users?department=研发部&id_gte=2"这些参数可以自由组合,比如?_page=1&_limit=10&_sort=name&_order=asc&department=研发部,完全模拟真实后端接口的行为。我一般会在项目里建一个api.md文件,把这些查询参数的用法记下来,前端同学直接照着拼 URL 就行。
4.2 关联查询:用 _expand 和 _embed 省掉手动组装
前面提到资源之间靠字段值关联,json-server 提供了两个参数来帮你做关联查询,省去前端手动 join 的麻烦。
_expand用于“向上查找”,比如查任务列表时把所属项目的信息一起带出来:
curl "http://localhost:3000/tasks?_expand=project"返回的数据里,每条 task 会多一个project对象,包含对应项目的完整信息。注意_expand的字段名必须是单数形式,且值要能匹配到目标资源的 id。
_embed用于“向下查找”,比如查项目列表时把该项目下的所有任务一起带出来:
curl "http://localhost:3000/projects?_embed=tasks"返回的每个 project 会多一个tasks数组。_embed的字段名必须是复数形式,且目标资源里要有对应的外键字段。
这两个参数在模拟“详情页”或“带子列表的列表页”时特别有用。不过要注意,它们只支持一层关联,不支持嵌套多层。如果你的数据关系很复杂,建议还是在前端用 JavaScript 自己组装,或者考虑用 GraphQL 类的模拟方案。
4.3 自定义路由和中间件:应对非标准接口
真实项目里总有一些接口不按 RESTful 规范来,比如/api/login、/api/upload、/api/statistics/summary这种。json-server 允许你通过一个配置文件来自定义路由和中间件。
新建一个routes.json文件:
{ "/api/*": "/$1", "/blog/posts/:id/comments": "/comments?postId=:id" }然后启动时加上--routes参数:
json-server --watch db.json --routes routes.json第一条规则把所有/api/开头的请求重写到根路径,这样你前端代码里写的/api/users就能正确映射到/users。第二条规则把/blog/posts/1/comments映射到/comments?postId=1,模拟了嵌套资源的路由风格。
如果需要更复杂的逻辑,比如根据请求头返回不同数据、模拟延迟、模拟错误状态码,可以用--middlewares参数加载一个自定义中间件文件:
// middleware.js module.exports = (req, res, next) => { // 模拟 500 毫秒网络延迟 setTimeout(next, 500); };这个中间件能力在测试 loading 状态和错误处理时非常实用。比如你可以写一个中间件,当请求头里带X-Mock-Error: true时直接返回 500 状态码,前端就能测试错误提示的展示逻辑。
5. 那些年我用 json-server 踩过的坑
5.1 id 字段的坑:类型不一致导致查询失败
json-server 默认会把 id 当作字符串来处理路由匹配,但你在db.json里写的 id 可能是数字。大多数情况下它自己能兼容,但有一种情况会出问题:你手动新增了一条 id 为字符串"1"的记录,而原有记录的 id 是数字1,这时候GET /users/1可能返回的不是你预期的那条。
我的做法是统一 id 类型,要么全用数字,要么全用字符串,不要混着来。另外,如果你在 POST 请求里手动传了 id,一定要确保它和现有 id 不冲突,否则会静默覆盖,这个坑我在一次演示前差点翻车——手动加了一条测试数据,id 和已有数据重复,结果把原来的记录覆盖了,演示时数据对不上,排查了半天才发现是 id 冲突。
5.2 文件写入的并发问题:多人同时操作会丢数据
json-server 的持久化是直接写文件,这意味着如果两个人同时往同一个db.json发 POST 请求,可能会出现写入覆盖的情况。在团队协作场景下,如果大家都连同一个 json-server 实例,这个问题会偶发。
我的解决方案是:每个人本地起自己的 json-server,db.json文件通过 git 管理,各自改各自的,需要共享数据时再手动合并。如果确实需要团队共享一个模拟服务,那就不要用文件持久化,改用内存模式(启动时不加--watch,数据只存在内存里,重启就重置),然后准备一个初始化脚本,每次启动时灌入基础数据。
5.3 跨域问题的处理:其实它已经帮你解决了
很多同学第一次用 json-server 时会习惯性地去找跨域配置,其实它默认就开启了 CORS,所有响应头里都带了Access-Control-Allow-Origin: *。你前端代码里直接请求http://localhost:3000不会遇到跨域拦截。
但有一种情况例外:如果你用了自定义中间件,并且在中间件里提前返回了响应而没有调用next(),那 CORS 头可能不会加上。这时候要么在中间件里手动设置 CORS 头,要么确保中间件逻辑走完next()流程。
5.4 数据量大了之后性能下降
db.json文件如果超过几兆,json-server 的每次读写都会变慢,尤其是_page分页查询,它实际上是先把整个数组加载到内存再切片。我实测过一个 5MB 左右的 JSON 文件,每次请求大概要 200 到 300 毫秒,页面加载明显变卡。
应对办法有两个:一是拆分数据文件,把不同资源放到不同的 JSON 文件里,用多个 json-server 实例分别服务,或者用--watch指定多个文件(新版本支持传入多个文件路径)。二是控制模拟数据量,前端开发阶段没必要模拟几万条数据,每个资源保留几十条足够测试分页和滚动效果了。
6. 把 json-server 融入日常开发流的几个习惯
6.1 用 npm scripts 管理启动命令
每次都手敲json-server --watch db.json --port 3001太麻烦,我习惯在package.json里加一个脚本:
{ "scripts": { "mock": "json-server --watch mock/db.json --port 3001 --routes mock/routes.json", "mock:delay": "json-server --watch mock/db.json --port 3001 --middlewares mock/middleware.js" } }这样团队成员拉下代码后,只需要npm run mock就能起服务,不用记那些参数。把mock目录单独管理,里面放db.json、routes.json、middleware.js,和业务代码分开,清清爽爽。
6.2 配合环境变量切换真实接口和模拟接口
前端项目一般会有.env.development和.env.production两个环境变量文件。我通常这样配置:
# .env.development VITE_API_BASE_URL=http://localhost:3001 # .env.production VITE_API_BASE_URL=https://api.example.com然后在 axios 实例里读取这个变量作为 baseURL。开发阶段指向本地的 json-server,打包上线时自动切换到真实接口。这样切换过程零成本,不需要改任何业务代码。
6.3 把 db.json 当作接口文档来维护
我有个习惯:每次后端同学定好一个新的接口结构,我就同步更新db.json里对应资源的数据结构。时间长了,db.json本身就成了一份“活着的接口文档”——字段名、数据类型、嵌套关系一目了然。新加入项目的同学,让他先看db.json再去看后端接口文档,理解起来快得多。
而且因为 json-server 支持_page、_sort、_like这些查询参数,前端在开发列表页时可以直接按照真实后端的参数规范来写请求,等后端接口好了,只需要确认后端也支持同样的参数名即可。如果后端参数名不一样,那就在 axios 层加一个参数映射,改动量很小。
6.4 用 nodemon 监听文件变化自动重启
虽然--watch已经能监听db.json的变化,但如果你改了routes.json或middleware.js,服务不会自动重启。这时候可以配合 nodemon 来用:
nodemon --watch mock --exec "json-server --watch mock/db.json --routes mock/routes.json"这样mock目录下任何文件变动都会触发重启,省去手动 Ctrl+C 再重跑的麻烦。不过要注意,nodemon 重启期间服务会短暂中断,如果前端正在发请求可能会报错,所以这个方案更适合在配置调试阶段用,稳定之后还是直接用json-server命令更省心。
7. 关于数据模拟这件事的一点个人体会
我从最早在组件里硬编码假数据,到用本地 JSON 文件,再到用 json-server,最后到根据项目需要灵活组合 json-server 和 MSW,这一路走下来最大的感受是:工具本身不复杂,复杂的是你有没有想清楚“模拟数据到底是为了什么”。
如果只是为了页面能渲染出来,那硬编码最快。但如果你的目标是让前端代码在接口切换时零改动、让团队协作时数据源统一、让测试同学能自己造数据验证边界情况,那 json-server 这种“给你一个真实 HTTP 服务”的方案就是更优解。它逼着你在开发阶段就用真实的方式发请求、处理响应、应对错误,而不是等联调时才手忙脚乱地改代码。
另外一个小建议:不要把db.json当成一次性用品。我见过很多项目,模拟数据用完就删,下次新项目又从头写一遍。其实可以把常用的数据结构沉淀下来,比如用户、订单、商品这些通用模型,下次新项目直接复制过去改改字段就能用。时间长了,你会发现自己攒了一套“模拟数据模板库”,新项目启动速度能快不少。
最后说一个我最近才发现的用法:json-server 配合--static参数可以同时托管静态文件。比如你打包了一个纯静态的前端页面,想快速在本地预览并且带上模拟接口,一条命令就能同时起静态服务和数据服务,不用再单独开一个 http-server。这个组合在给非技术同学演示时特别方便,一个命令行窗口搞定所有事情。