做前端开发这几年,我最怕听到的一句话不是“这个需求下周一上线”,而是“后端接口下周才给,你先看文档把页面写了”。文档里往往只有十几个字段名,返回结构写得模棱两可,等后端真正联调时才发现字段大小写对不上、嵌套层级差了一层、分页参数完全不是约定那一套。后来我接触到 json-server,才发现原来一个 JSON 文件加一条命令,就能在十分钟内拥有一套看起来像模像样的后端接口。它既能支撑前端页面开发,又能拿来给同事做 demo,甚至还能在小型活动页里临时充当数据服务。这篇文章我就围绕 json-server 这个零代码后端模拟神器,把从安装、路由规则到进阶用法的完整经验整理出来,方便你在下一个前后端并行开发的项目里直接照抄。
1. 后端接口没就绪时,前端开发为什么要备一套 mock 武器
1.1 前后端并行开发里,时间都浪费在哪些地方
前后端分离之后,理论上两边可以按接口文档并行推进,但实际项目里“接口文档先行”往往只是理想状态。需求评审刚结束,后端要排期做表结构设计,前端却已经接到了“先搭页面框架”的任务。你总不能对着空白页面干等一周。常见做法是自己写一套本地 mock,比如在 webpack 或者 vite 的 dev server 里配几个中间件,又或者用工具拦截请求返回固定 JSON。这些方案能跑,但有一个通病:mock 逻辑分散在前端工程里,只对自己本地生效,换台电脑或者换个人来协作,mock 代码能不能跑起来都成问题。
json-server 不一样。它把后端模拟这事从“前端工程内部”剥离出来,变成一个独立运行的服务进程。你只需要维护一个 JSON 数据文件,它就能生成一套完整的 RESTful 接口,包括列表查询、单条读取、新增、修改、删除这些基本操作。前端代码里只关心 axios 请求的 URL,完全不需要知道数据是从 json-server 来的还是从真实后端来的。联调切换的时候,只要把环境变量里的 baseURL 改一下,页面代码零改动就能接上真实服务。
1.2 json-server 的定位和边界:它能做什么,不能做什么
很多人第一次用 json-server 会误以为它是数据库,或者是一个低代码后端平台,其实它的定位很简单:基于一个 JSON 文件,快速生成 REST API 的服务。它适合做这几类事:
- 前端页面开发时,提供稳定的假接口
- 给 UI 设计稿配一份可点击的演示环境
- 写单元测试或集成测试时,作为可控的依赖服务
- 本地开发小程序或 App 时,充当临时后端
- 给演示项目提供一个不依赖真实环境的后端底座
它的边界也很清晰:没有用户体系,没有权限控制,没有事务保障,数据持久化就是把整个 JSON 文件重写一遍。并发量一高就会出问题,所以它只适合开发调试,不适合做生产服务。理解这个边界很重要,因为很多人踩坑就是因为在错误的环境里用了它。
我在项目里见过一个反面案例:某个内部管理系统的报表导出功能,为了快速上线,直接把 json-server 部署到了内网服务器上当真实接口用。刚开始数据量小没事,后来有人开始往里写业务数据,一旦两个人同时提交,文件就会被覆盖。最后运维排查了半天,发现数据全丢在了一个 json 文件里。所以使用之前,心里要有数:它是模拟工具,不是生产数据库。
2. 一条命令跑起来的接口服务:安装、数据结构与基础路由规则
2.1 安装方式和第一条启动命令
json-server 是一个 Node.js 工具,用 npm 全局安装或者用 npx 直接执行都行。个人推荐在项目里作为 devDependency 安装,锁版本,避免不同机器上行为不一致。
npm install json-server --save-dev然后准备一个数据文件,习惯上叫 db.json,放在项目根目录或者 mock 目录下。最小示例:
{ "posts": [ { "id": 1, "title": "json-server 入门", "author": "张三" }, { "id": 2, "title": "零代码后端模拟", "author": "李四" } ], "comments": [ { "id": 1, "postId": 1, "body": "写得好" }, { "id": 2, "postId": 1, "body": "收藏了" } ] }启动命令:
npx json-server db.json默认监听 3000 端口,启动后终端会打印出所有可用的路由地址。浏览器打开http://localhost:3000/posts就能看到 posts 列表,打开根路径http://localhost:3000/会进入一个可视化操作界面,可以在页面上直接测接口。
这里注意一个细节:json-server 默认会监听 db.json 文件的变化,也就是“watch 模式”。你改了文件内容,服务会自动重载数据,部分场景下甚至不用重启。但文件格式一旦写错(比如多加了一个逗号),服务会直接崩溃或者报错,需要重启。开发时改完数据,最好看一眼终端日志。
2.2 db.json 的字段设计:主键、外键和嵌套结构
db.json 的结构决定了生成的接口形态。最外层是一个 JSON 对象,每个 key 对应一个资源名,value 是数组,数组里每个元素就是一条记录。资源名会被直接拼成路由路径,比如"posts"对应/posts。
每条记录必须有唯一的主键,默认字段名是id,可以是数字也可以是字符串。如果 POST 提交的数据里没有 id,json-server 会自动生成一个随机递增的数字 id。这里建议前端小伙伴注意:如果你们的前端代码依赖 id 是数字类型,就在 Mock 数据里显式用数字;如果后端的 id 是字母数字混合字符串,也先把类型定好,避免前后联调时类型不一致。
资源之间可以表达一对多关系,最朴素的方式就是用外键字段,比如示例里的postId指向posts.id。json-server 支持两种关系查询方式:_embed和_expand,我后面会专门展开讲。嵌套结构也可以写,但不太推荐,因为嵌套层级一旦加深,路由规则会变得不符直觉。比如"comments": [{ "post": { "id": 1 } }]这种写法,增删改查都会变得别扭。
2.3 基础路由规则:RESTful 风格是从文件名自动长出来的
不需要写任何路由配置,json-server 会根据资源名自动生成一整套 RESTful 路由。这是它“零代码”气质的核心体现。自动生成的路由完整列表如下:
| HTTP 方法 | 路由 | 作用 |
|---|---|---|
| GET | /posts | 获取列表 |
| GET | /posts/1 | 获取单条 |
| POST | /posts | 新增一条 |
| PUT | /posts/1 | 整体替换单条 |
| PATCH | /posts/1 | 局部更新单条 |
| DELETE | /posts/1 | 删除单条 |
这个设计天然对应前端常用的 ajax 库和 fetch API,不需要做任何适配。前端代码里写:
const response = await fetch('/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: '新文章', author: '王五' }) });数据就会被写入 db.json,然后出现在/posts列表里。整个过程不需要写后端逻辑。
还有一个容易被忽略的接口:GET /posts?_page=1&_limit=2能拿到分页数据,响应头里的X-Total-Count会返回总条数。这个在后面的查询语法部分细说。
3. 查询语法里那些每天都会碰到的细节:过滤、排序、分页与关系字段
3.1 过滤、排序与分页的正确打开方式
json-server 的查询语法很像后端框架自动生成的列表查询接口,熟悉之后会觉得无比顺手。先说最常用的几个参数。
按字段精确过滤,直接拼 URL query:
GET /posts?author=张三 GET /posts?id=1&id=2 GET /comments?postId=1模糊搜索用_like:
GET /posts?title_like=json范围筛选用_gte、_lte:
GET /posts?views_gte=100&views_lte=500排除某个值用_ne:
GET /posts?author_ne=张三排序用_sort和_order:
GET /posts?_sort=views&_order=desc GET /posts?_sort=author,views&_order=asc,desc第二个示例是多字段排序,第一个字段按作者升序,第二个字段按浏览量降序。这个细节很多人第一次用会卡住,以为_order只能传一个值,其实它可以写成逗号分隔,和_sort里的字段一一对应。
分页有两种模式。一种是公开的_page和_limit:
GET /posts?_page=2&_limit=10响应头里会带X-Total-Count,告诉你有多少条记录,前端可以据此计算总页数。另一种是区间截取,用_start和_end:
GET /posts?_start=0&_end=10也可以配合_limit单独用。区别在于区间截取不返回X-Total-Count,响应头里没有总条数。如果前端的分页组件依赖总数做页数展示,就用_page方案。
3.2 关系字段的嵌套查询和全文检索
实际业务里,列表页经常要同时展示关联信息。比如文章列表要显示作者的昵称和头像,评论列表要显示文章标题。这种需求在真实后端往往要用 join 查询,在 json-server 里则通过资源关系自动处理。
先交代两个概念。_embed是“把子资源嵌进来”,内容方向是父级读取子级;_expand是“把父资源展开”,内容方向是子级读取父级。
GET /posts?_embed=comments返回结果里,每篇文章会多出一个comments数组,里面是postId等于当前文章 id 的评论。
GET /comments?_expand=post返回结果里,每条评论会多出一个post对象,内容是外键指向的文章。两个方向也可以组合使用:
GET /posts?_embed=comments&_expand=user前提是你的数据文件里有对应的资源和外键字段。如果资源名拼错了,它不会报错,只是不展开任何字段。这个特性用来模拟联调阶段的详情页数据非常方便,不用前端自己拼多个请求。
全文检索用q参数,它会扫描整个资源里的所有字段,进行模糊匹配:
GET /posts?q=json-server这个功能在写全局搜索框原型时很好用,一行配置都不用写,直接把搜索关键词拼到 query 里就行。不过要注意,q的是全字段检索,性能和语义都不如真实后端的全文检索,只适合模拟阶段使用。
3.3 响应体里那些容易混淆的字段:关于 embed 和 expand 的选择
我用 json-server 折腾了几个月之后,最大的体会是_embed和_expand虽然看起来接近,但选错会导致返回数据的结构非常别扭,前端解析代码跟着写错。
举例说,评论列表页通常需要展示“评论内容 + 所属文章标题”。这时候用/comments?_expand=post,返回的每条评论是一个扁平的post字段嵌套在评论对象里,前端代码写成comment.post.title就行,清晰直观。
但如果需求是“文章列表 + 每篇文章的前三条评论”,用/posts?_embed=comments也顺理成章。麻烦的场景是:评论区下方还要展示“评论者信息”,数据结构变成了评论里嵌文章、文章里嵌评论者,很容易出现循环嵌套。实际使用中要克制,不要用一个 super 长 URL 把所有关系都嵌进来,不然返回 JSON 会非常庞大,前端调试起来也头疼。
我的习惯是:详情页用_embed,列表页用_expand。这个偏好不一定是标准答案,但能让 mock 数据和真实后端返回结构最接近,避免换到真实接口时前端代码大改。
4. 从静态数据到业务状态机:POST、PUT、PATCH 与自定义路由的玩法
4.1 三种写操作的语义差异
很多刚接触 json-server 的同学会困惑:POST、PUT、PATCH 到底有什么区别?在真实后端里,这三个方法语义不同;在 json-server 里,它们对应着不同的数据处理方式。
- POST:新增一条数据。没传 id 时自动生成,传了 id 就按传的值存。
- PUT:整体替换。前端必须提交完整的对象,尤其是要把 id 一起带上。
- PATCH:局部更新。只要提交需要修改的字段集合就行。
- DELETE:删除指定 id 的数据。
实际开发中,前端最常用的是 POST 和 PATCH。PUT 用得少,因为很多业务场景里只需要改某个字段,整体替换容易把其他字段意外清空。你在 json-server 里测试 PUT 时要特别注意:它不会自动保留缺失的字段,没传的都视为空。这个行为和某些后端不一致,联调时容易造成“Mock 时好好的,一接真实环境就缺字段”的错觉。
POST 提交时的数据结构还有一个细节:如果提交的是数组,json-server 会批量插入;如果提交的是普通对象,就只插一条。这个批量插入特性在初始化测试数据时很好用。
4.2 用 routes 文件改写 URL,让接口路径贴近后端规范
真实后端接口路径往往带前缀,比如/api/posts或者/v1/users。json-server 默认生成的路径是不带前缀的,直接使用会导致前端代码在 Mock 和真实环境之间切换时,需要额外改 baseURL。解决办法是创建一份routes.json:
{ "/api/*": "/$1", "/v1/posts": "/posts", "/v1/posts/:id": "/posts/:id" }启动时加参数:
npx json-server db.json --routes routes.json这样GET /api/posts/1会被改写映射到GET /posts/1。前端代码里的请求地址始终保持真实环境的路径风格,Mock 环境下由 json-server 做一次转换。
routes 文件里的写法支持通配符和路径参数。我最常用的是第一个"/api/*": "/$1",意思是将/api/后面的部分直接匹配到原始路由。这样整个项目的 URL 前缀统一成/api,最贴近真实环境。后面几个具体路径的配置适合做差异化定制,比如当某个资源在真实后端不叫这个名,或者嵌套关系更深时,可以灵活重写。
4.3 浏览器端的可视化操作面板
启动后打开根路径,json-server 内置了一个简单的操作面板。左侧是资源列表,右侧展示每条数据的 JSON 内容,顶部能切换 GET、POST、PUT、PATCH、DELETE 等操作。这个面板对不会写命令行的同事特别友好,产品经理或者设计师想看数据长什么样,直接点开就能浏览,不需要让他们学 curl 或 Postman。
面板底部还有个“新资源”输入框,可以直接往 db.json 里加初始数据。我在团队里试过几次,大家上手没有门槛。当然,真正常用接口的还是习惯用命令行工具或者 Postman,但把它作为团队的 mock 数据展示入口,比让新人直接读 JSON 文件体验好很多。
5. 模拟数据接近生产环境的最后一公里:中间件、造数与持久化
5.1 用 middleware 模拟网络延迟、登录态和按条件返回
这一节是 json-server 从“玩具”走向“工具”的分水岭。默认情况下,接口响应是即时的,页面开发时看不出加载态和 loading 效果。真实网络的延迟、请求失败、登录态失效这些场景,都需要靠中间件模拟。
json-server 支持通过-m参数挂载自定义中间件:
npx json-server db.json -m ./middleware.js中间件文件内容大致如下:
module.exports = function (req, res, next) { // 模拟网络延迟 setTimeout(next, 500); };更复杂的场景可以玩出这些花样:
- 模拟登录校验:如果请求头里没有
Authorization: Bearer xxx,直接返回 401。 - 模拟随机失败:根据概率返回 500,让前端处理错误分支。
- 模拟接口限流:某个接口连续点击 N 次后返回 429。
- 模拟业务错误:根据请求体内容返回
{ code: 10001, message: "库存不足" }。
这些能力让前端能在本地就把异常跑通,不需要等真实后端配合。尤其是权限校验:真实环境里调接口要带 token,Mock 阶段如果完全不校验,前端代码一旦把 token 逻辑写成“先判断有无再请求”,到联调阶段就会漏掉 token 的边界处理。用中间件提前模拟,比联调时再发现问题省心得多。
5.2 批量造数:用脚本生成 db.json 而不是手写
手写 5 条测试数据没问题,但要写 200 条分页数据,手写既费时又容易重复。推荐做法是写一个 Node 脚本,用循环生成数据,然后写入 db.json。
下面是一个实际用过的造数脚本示例,生成 150 篇文章和 300 条评论:
const fs = require('fs'); const posts = []; const comments = []; const authors = ['张三', '李四', '王五', '赵六']; for (let i = 1; i <= 150; i++) { posts.push({ id: i, title: `文章标题 ${i}`, author: authors[i % authors.length], views: Math.floor(Math.random() * 1000), createdAt: new Date(Date.now() - i * 86400000).toISOString() }); } let commentId = 1; for (let i = 1; i <= 150; i++) { const count = Math.floor(Math.random() * 3) + 1; for (let j = 0; j < count; j++) { comments.push({ id: commentId++, postId: i, body: `评论内容 ${commentId}` }); } } const db = { posts, comments }; fs.writeFileSync('./db.json', JSON.stringify(db, null, 2));生成之后直接启动 json-server,接口数据量就足够前端调试分页和排序了。脚本本身建议放在 mock 目录下,和 db.json 放一起,方便后来的人重新生成。还有一个小技巧:如果项目本身是前端工程,可以把造数脚本接到package.json的 scripts 里,比如"mock:generate": "node mock/generate.js"。这样团队成员一键就能重新生成数据,不用互相拷贝 db.json。
5.3 数据持久化的读写时机和隐患
json-server 收到写操作(POST、PUT、PATCH、DELETE)后,会更新内存中的数据,然后同步把整个 db.json 重写一遍。这意味着每次写操作都是全量写文件,数据量大起来会有明显卡顿。
我测试过一个数据规模约 2 万条记录的 db.json,文件体量大概 5MB。每执行一次 POST 或 DELETE,服务端大约要花几百毫秒重写文件。如果前端连续发多个写请求,表现可能是 ajax 排队,接口变慢。在纯 Mock 场景下可以接受,但如果你的演示项目需要频繁写数据,就要考虑减少数据量,或者把无关历史数据拆分到另一个 JSON 文件。
json-server 还支持--static参数指定静态资源目录,也能通过--delay参数给所有响应加统一延迟。这些参数组合起来,可以比较接近地模拟一个状态比较真实的慢接口服务。
6. 我在真实项目里踩过的坑,以及最终沉淀下来的工作流
6.1 并发写入导致数据丢失
这个坑我前面提过,这里详细展开。有一次我们在做内部运营后台的前端原型,多人同时使用同一个 json-server,数据文件放在共享目录里。某个下午,运营同事反馈“文章标题改了保存后,过一会儿又变回旧值”。排查后发现原因是两个同事同时打开页面,各自编辑不同文章,先后提交后端。每次提交都会全量重写 db.json 文件,后一次提交覆盖前一次,导致数据互相覆盖。
这类问题不是代码 bug,而是 json-server 的并发模型天然不适合多人写。解决思路有三种:
- 只让一个人负责维护 db.json,其他人通过接口读写,避免手工改文件。
- 演示场景下,把写操作禁止掉,只保留查询权限(可以在中间件里拦截写方法)。
- 多人协作时共用一个 mock 服务,但约定好每个人只操作自己负责的资源前缀,减少互相覆盖面积。
6.2 中文乱码和 JSON 文件格式问题
json-server 默认读取和写入 db.json 时都按 UTF-8 处理,正常情况下中文不会乱码。但 Windows 环境下,如果 db.json 文件本身不是 UTF-8 编码,启动后接口返回的中文就会变成乱码。遇到这种情况,用编辑器把文件重新保存为 UTF-8 without BOM 格式即可。
更隐蔽的问题是 BOM。有些编辑器保存 UTF-8 文件时会带上 BOM 头,json-server 解析时会报错或者把第一个 key 名解析出特殊字符。我遇到过启动后/posts路由正常,但/Ϊposts多出一个诡异路由的情况,最后发现是文件带 BOM。建议项目里统一用 VS Code 或代码格式化工具,保存时约定 UTF-8。
JSON 格式还有一个常见坑:多人手工编辑 db.json 时,容易在数组末尾多写一个逗号。严格模式下 JSON 不允许尾逗号,会导致服务启动失败。规范的做法是不直接编辑 db.json,而是维护一个 seed 脚本,通过脚本生成。
6.3 端口占用和 watch 模式失效
默认端口 3000 经常被别的开发服务占用。启动时报EADDRINUSE错误时,不需要慌,换一个端口就行:
npx json-server db.json --port 4000更推荐的方式是把它写进 npm scripts,统一固定在 mock 端口上。比如:
{ "scripts": { "mock": "json-server mock/db.json -p 4000 -w -r mock/routes.json -m mock/middleware.js" } }watch 模式失效一般是因为 db.json 被软链接指向了项目外的共享目录,json-server 监听不到。如果希望外部目录变化能触发更新,最好把 db.json 放在服务启动目录下,或者直接重启服务。我自己很少依赖热重载,因为改数据之后往往要同时刷新页面看效果,服务自动重载反而会让连续操作出现一瞬间的空窗,手动重启控制感更强。
6.4 我在团队里最终落地的工作流
经过这些折腾,我在不同项目里沉淀了一套固定打法,目前用下来比较省心。如果你刚接触 json-server,可以直接按这个路径搭建:
- 在项目根目录建
mock/文件夹,里面放db.json、routes.json、middleware.js、generate.js四个文件。 - 用
generate.js统一造数,保证数据可再生成、可审查。 - 用
routes.json统一加/api前缀,让 mock 接口风格和真实后端保持一致。 - 在
middleware.js里写统一的延迟和模拟鉴权逻辑。 - 把启动命令写进
package.json的scripts,团队共享。 - 开发环境里,前端请求 baseURL 指向 mock 服务;联调时切换环境变量指向真实后端。
这套流程最大的好处是“环境切换成本几乎为零”。前端代码不需要知道 mock 服务存在,因为 URL 永远是/api/posts这种形式,只是环境变量里的 host 不同。我的同事接手这类项目时,只需要运行npm run mock,再开一个前端 dev server,就能在不依赖真实后端的情况下继续开发。
最后再分享一个小技巧:如果你在写一个完整的前端演示项目,希望别人在本地一条命令就能跑起来,可以把 mock 服务和前端 dev server 做成 parallel 启动,用 concurrently 之类的工具同时拉起。这样别人 clone 下项目,执行一条命令就能看到完整页面效果。json-server 虽然叫“零代码后端模拟神器”,但真正把它用顺手,靠的是把它嵌进整个开发流程里,让团队的协作方式适应它。我的建议是,任何一个项目在第一天就配好 mock 环境,而不是等后端接口延期了才去补。