Skip to content
On this page

JSON Server

JSON Server 的作者

JSON Server 的作者 typicode 也是huskylowdb的作者,乃是业界大牛。

官方网站

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 个例子。

  1. 数据是对象
js
module.exports = {
  userInfo: {
    id: 1,
    realName: '张三',
    gender: '',
    age: 19,
    // ...
  },
};
  1. 数据是数组
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');
}

注意事项

  1. .modules_md5db.json都不需要手动创建。

  2. 如果想要清除db.json脏数据,可以直接编辑db.json,这样nodemon会自动重启 JSON Server 服务。

  3. 如果想重建某个 Module 的数据,可以给这个 Module 加无用字符,比如空行里写//,这样会触发nodemon监听,重建该 Module 对应数据。

  4. 如果想重建全部 Module 的数据,可以清空db.json内容(注意,不是删除文件),剩下的事也由nodemon自动完成。

附录

Modules 可能用到的工具函数

js
// 生成 ID 等差数列
function genIdSeries(from, to) {
  return Array(to - from + 1)
    .fill()
    .map((e, i) => from + i);
}

杨亮的前端解决方案