Appearance
JSON Server
JSON Server 的作者
JSON Server 的作者 typicode 也是husky
、lowdb
的作者,乃是业界大牛。
官方网站
https://www.npmjs.com/package/json-server
https://github.com/typicode/json-server
不要用全局命令行模式
不要用全局命令行模式,要用server.js
模式,官方叫Module
模式,不过因为本文有自己的Modules
概念,为避免混乱,所以把它叫做server.js
模式。
局部安装 json-server
bash
yarn add -D json-server
目录结构
src
├─ mocks mocks 主目录
│ ├─ modules 模块目录,建议一一对应 /src/api/ 里的文件结构
│ │ ├─ .modules_md5 记录 Modules 文件 MD5 的文件,无需手动创建
│ │ ├─ child_dir 子目录举例
│ │ │ ├─ simpleArray.js 模块举例
│ │ │ └─ simpleObject.js 模块举例
│ │ └─ pagedList.js 分页列表举例
│ ├─ db.json 数据库,无需手动创建
│ ├─ routes.json 路由规则
│ └─ server.js 服务主文件
├─ App.vue
└─ main.js
Modules
在/src/mocks/modules/
下创建 JSON 或 JS 文件,为维护方便,我建议完全对应 /src/api/
里的目录文件结构。
Modules 使用 JSON 文件与 JS 文件的区别:
JSON 文件 | JS 文件 |
---|---|
数据是写死的 | 数据是动态的 |
不支持随机数据 | 支持随机数据 |
JS 文件可以完全兼容 JSON 文件,所以应一律用 JS Modules。下面举 2 个例子。
- 数据是对象
js
module.exports = {
userInfo: {
id: 1,
realName: '张三',
gender: '男',
age: 19,
// ...
},
};
- 数据是数组
js
const Mock = require('mockjs');
let mockData = {};
const total = 87; // 可以任意修改
mockData.users = [];
for (let i = 1; i <= total; i++) {
mockData.users.push({
id: i,
realName: i + '. ' + Mock.Random.cname(),
// ... 其他数据从略
});
}
module.exports = mockData;
routes.json
默认路由支持 RESTful 风格和 Query 风格的增删改查,已经很方便。在不需要额外规则的前提下,一般这一句足够:
json
{
"/prod-api/*": "/$1"
}
server.js
js
const path = require('path');
const fs = require('fs-extra');
const readdirp = require('readdirp');
const crypto = require('crypto');
/**
* 构建 db.json
*/
// db.json 是数据库文件
const dbJsonPath = path.join(__dirname, 'db.json');
// .modules_md5 是一个专用文件,用于储存所有模块文件的 MD5,以判断文件是否有变化
const modulesMd5Path = path.join(__dirname, 'modules', '.modules_md5');
// 初始化 dbJson
let dbJson = {};
if (!fs.pathExistsSync(dbJsonPath) || !fs.readFileSync(dbJsonPath, 'utf8')) {
fs.writeJsonSync(dbJsonPath, {});
// 如果 db.json 不存在,那么 .modules_md5 必须一并被删,反之则不必
fs.removeSync(modulesMd5Path);
} else {
dbJson = fs.readJsonSync(dbJsonPath);
}
// 初始化 modulesMd5Json
let modulesMd5Json = {};
if (!fs.pathExistsSync(modulesMd5Path) || !fs.readFileSync(modulesMd5Path, 'utf8')) {
fs.writeJsonSync(modulesMd5Path, {});
} else {
modulesMd5Json = fs.readJsonSync(modulesMd5Path);
}
// 立 flag
let isModuleChanged = false;
// 遍历 Modules
readdirp(path.join(__dirname, 'modules'), {
fileFilter: ['*.js', '*.json'],
type: 'files',
depth: 3,
})
.on('data', (entry) => {
const oldMd5 = modulesMd5Json[entry.path];
const newMd5 = crypto
.createHash('md5')
.update(fs.readFileSync(entry.fullPath, 'utf8'))
.digest('hex');
if (!oldMd5 || oldMd5 !== newMd5) {
isModuleChanged = true;
const json = require(entry.fullPath);
for (let k in json) {
dbJson[k] = json[k];
}
modulesMd5Json[entry.path] = newMd5;
}
})
.on('end', () => {
if (isModuleChanged) {
fs.writeJsonSync(modulesMd5Path, modulesMd5Json);
fs.writeFileSync(dbJsonPath, JSON.stringify(dbJson, null, 2));
}
/**
* 启动 JSON Server
*/
const jsonServer = require('json-server');
const server = jsonServer.create();
const router = jsonServer.router(path.join(__dirname, 'db.json'), {
routes: path.join(__dirname, 'routes.json'),
});
router.render = (req, res) => {
// req.query 有隐藏的bug,可能拿到的是空对象,所以干脆从 URL 里取值
const searchParams = /\?.+/.test(req.url)
? new URLSearchParams(req.url.match(/\?.+/)[0])
: {};
// 凡任意请求只要 URL 带上 fail=1 参数,就视为失败查询
if (/fail=1/.test(req.url)) {
res.json({
code: -1,
msg: 'error',
});
return;
}
// GET 分页列表
if (/pageNum/.test(req.url)) {
const total = res.locals.data.length;
const pageSize = Number(searchParams.get('pageSize'));
let pageNum = Number(searchParams.get('pageNum'));
pageNum =
pageNum <= Math.ceil(total / pageSize) ? pageNum : Math.ceil(total / pageSize);
const start = (pageNum - 1) * pageSize;
const end = pageNum * pageSize >= total ? total : pageNum * pageSize;
const list = res.locals.data.slice(start, end);
res.json({
code: 0,
msg: 'OK',
data: {
total,
pageNum,
pageSize,
list,
},
});
}
// 其他所有情况都先按照默认路由执行,如果默认路由不满足需要,优先考虑自定义路由规则,
// 如果还不行就考虑另写 else if
else {
res.json({
code: 0,
msg: 'OK',
data: res.locals.data,
});
}
};
const middlewares = jsonServer.defaults();
server.use(middlewares);
server.use(router);
server.listen(3000, () => {
console.log('JSON Server is listening on port 3000.');
});
});
配置 vue.config.js 的 devServer
假设.env.development
已经设置了VUE_APP_BASE_API = '/prod-api'
。
js
devServer: {
proxy: {
[process.env.VUE_APP_BASE_API + '/prod-api']: {
target: 'http://localhost:3000/',
changeOrigin: true,
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API]: '',
},
},
},
},
修改文件自动触发重启服务
JSON Server 作者不打算在server.js
模式加入监听文件功能,他推荐使用nodemon
。见GitHub 评论。
利用nodemon
监听 Modules 变化,一旦/src/mocks/
下的任何.js
和.json
文件有变化,都会触发 JSON Server 重启服务。
bash
yarn add -D nodemon
在package.json
加入一条script
,使用yarn mock
启动服务即可:
"mock": "nodemon --watch ./src/mocks ./src/mocks/server.js"
修改文件自动页面热刷新
修改 Modules 或db.json
自动触发页面热更新:
js
// main.js
if (process.env.NODE_ENV === 'development') {
require('./mocks/db.json');
}
注意事项
.modules_md5
和db.json
都不需要手动创建。如果想要清除
db.json
脏数据,可以直接编辑db.json
,这样nodemon
会自动重启 JSON Server 服务。如果想重建某个 Module 的数据,可以给这个 Module 加无用字符,比如空行里写
//
,这样会触发nodemon
监听,重建该 Module 对应数据。如果想重建全部 Module 的数据,可以清空
db.json
内容(注意,不是删除文件),剩下的事也由nodemon
自动完成。
附录
Modules 可能用到的工具函数
js
// 生成 ID 等差数列
function genIdSeries(from, to) {
return Array(to - from + 1)
.fill()
.map((e, i) => from + i);
}