打造多檔案結構的json-server

繁體中文 | English

# 前言

先附上範例 (opens new window)

這篇文章只是分享我自己運用的方法,或許有更好的方法也說不定。

# json-server 能幹麻

先簡單看過 json-server 提供的 Getting started (opens new window)

Create a db.json file with some data

{
  "posts": [
    { "id": 1, "title": "json-server", "author": "typicode" }
  ],
  "comments": [
    { "id": 1, "body": "some comment", "postId": 1 }
  ],
  "profile": { "name": "typicode" }
}

Start JSON Server

json-server --watch db.json

Now if you go to http://localhost:3000/posts/1, you'll get

{ "id": 1, "title": "json-server", "author": "typicode" }

就那麼簡單,但是實際專案中資料會更龐大複雜,需要更多 API 接口,於是我決定為每個 API 對應一個檔案。

# 開始

當下版本為 "json-server": "^0.15.0"

# 建立多檔案結構

建立新的資料夾mock,並在package.json寫個 script:

"scripts": {
  "mock": "json-server --watch ./mock/db.js"
}

首先 db.json 可以改成 js 的格式:

// db.js
// 必須是回傳一個return物件的function
module.exports = () => ({
  posts: [{ id: 1, title: "json-server", author: "typicode" }],
  comments: [{ id: 1, body: "some comment", postId: 1 }],
  profile: { name: "typicode" }
});

既然是 js,代表我們可以引入其他檔案:

// db.js
const posts = require("./posts");
const comments = require("./comments");
const profile = require("./profile");
module.exports = () => ({
  posts,
  comments,
  profile
});

新建對應檔案 例如:

// posts.js
module.exports = [{ id: 1, title: "json-server", author: "typicode" }];

# db.js 自動引入檔案

可以把資料拆分到多個不同檔案了,但是每次新增一個檔案都要引入一次,有夠麻煩。

這邊運用glob (opens new window)取出檔名,我們可以動態的引入檔案:

// db.js
const Path = require("path");
const glob = require("glob");
const apiFiles = glob.sync(Path.resolve(__dirname, "./") + "/**/*.js", {
  nodir: true
});
// apiFiles會是
// [ '/Users/billy/Desktop/json-server-multiple-files-sample/mock/comments.js',
//   '/Users/billy/Desktop/json-server-multiple-files-sample/mock/db.js',
//   '/Users/billy/Desktop/json-server-multiple-files-sample/mock/posts.js',
//   '/Users/billy/Desktop/json-server-multiple-files-sample/mock/profile.js' ]

let data = {};

apiFiles.forEach(filePath => {
  const api = require(filePath);
  let [, url] = filePath.split("mock/"); // e.g. comments.js
  url = url.slice(0, url.length - 3)  // remove .js >> comments
  data[url] = api;
});

// data會是
// { 'comments': [ { id: 1, body: 'some comment', postId: 1 } ],
//   'db': {},
//   'posts': [ { id: 1, title: 'json-server', author: 'typicode' } ],
//   'profile': { name: 'typicode' } }

module.exports = () => {
  return data;
};

但是我不需要 db.js 也被拿去當 API 啊
db.js改名為_db.js,趁機改個規則 把所有開頭為_的檔案不視為 API 接口:

// db.js
// ...
const apiFiles = glob.sync(Path.resolve(__dirname, "./") + "/**/[!_]*.js", {
// ...

# 監聽多檔案

json-server 只有在_db.js有更動時重啟,每當我新增 API,或修改回傳資料,我還得手動 yarn mock,好麻煩啊, 解決方法 (opens new window)

yarn add -D nodemon
"scripts": {
  "mock": "nodemon --watch mock --exec 'json-server ./mock/_db.js'"
}

# 更完整的 router

快要完成了,那 API 變得複雜點呢:

/blog/posts
/blog/comments
/blog/profile
/documents/query

我希望我的 mock 資料夾結構如下:

├── _db.js
├── blog
│   ├── comments.js
│   ├── posts.js
│   └── profile.js
└── documents
    └── query.js

動手修改_db.js規則:

// db.js
const Path = require('path');
const glob = require('glob');
const config = require('./config.json');
const apiFiles = glob.sync(
  Path.resolve(__dirname, './') + '/**/[!_]*.js',
  {
    nodir: true,
  },
);

let data = {};


apiFiles.forEach(filePath => {
  const api = require(filePath);
  let [, url] = filePath.split('mock/');
  url = url.slice(0, url.length - 3);
  data[url.replace(/\//g, '-')] = api; // 只有這裡更動
});

// data會是
// { 'blog-comments': [ { id: 1, body: 'some comment', postId: 1 } ],
//   'blog-posts': [ { id: 1, title: 'json-server', author: 'tydpicode' } ],
//   'blog-profile': { name: 'typicode' },
//   'documents-query': { data: 123 } }

module.exports = () => {
  return data;
};

建立config.jsonrouter.json: (config 配置 (opens new window)router 配置 (opens new window)

// config.json
{
  "port": 9898,
  "routes": "./mock/router.json"
}
// router.json
{
  "/*/*/*": "/$1-$2-$3",
  "/*/*": "/$1-$2"
}

這樣一來 /documents/query 就會對應到 /documents-query
如果有/documents/query/something 那就會對應到/documents-query-something,完全符合現在的資料夾結構了。

最後稍微修改一下:

// _db.js
const Path = require("path");
const glob = require("glob");
const apiFiles = glob.sync(Path.resolve(__dirname, "./") + "/**/[!_]*.js", {
  nodir: true
});

let data = {};
apiFiles.forEach(filePath => {
  const api = require(filePath);
  let [, url] = filePath.split("mock/");
  url =
    url.slice(url.length - 9) === "/index.js"
      ? url.slice(0, url.length - 9) // remove /index.js
      : url.slice(0, url.length - 3); // remove .js
  data[url.replace(/\//g, "-")] = api;
});
module.exports = () => {
  return data;
};

萬一有個 API 是/documents 我就能把

├── _db.js
├── blog
│   ├── comments.js
│   ├── posts.js
│   └── profile.js
├── config.json
├── documents
│   └── query.js
├── documents.js
└── router.json

更條理的改成:

├── _db.js
├── blog
│   ├── comments.js
│   ├── posts.js
│   └── profile.js
├── config.json
├── documents
│   ├── index.js
│   └── query.js
└── router.json

# 其他

# 中文解碼

假使我有個 API 為documents/基本,我是訪問不到他的。
基本會被編碼成%E5%9F%BA%E6%9C%AC,編碼規則是 %加上十六進制,那就新增一個 middleware 來處理吧。
每次收到 request 時都會經過 middleware,判斷 url 若包含編碼規則的字串,那就把 url 解碼。
使用我前面預留的_作為檔名開頭:

├── _db.js
├── blog
│   ├── comments.js
│   ├── posts.js
│   └── profile.js
├── config.json
├── documents
│   ├── index.js
│   ├── query.js
│   └── 基本.js
├── middlewares
│   ├── _decodeChinese.js
└── router.json
// _decodeChinese.js
module.exports = function(req, res, next) {
  const regex = /%(\d|[A-Z]){2}/;
  const isMatch = regex.test(req.url);
  if (isMatch) {
    req.url = decodeURI(req.url);
  }
  next();
};
// config.json
{
  // ...
  "middlewares": ["./mock/middlewares/_decodeChinese.js"]
  // ...
}

# POST 轉成 GET

有時候會用 POST 來取得資料,但 json-server 提供的 custom route 都是 GET。 解決方法 (opens new window)
一樣使用 middleware 處理,判斷 request 的方式為 POST 時就轉成 GET。

// _postAsGet.js
module.exports = function(req, res, next) {
  if (req.method === 'POST') {
    // Converts POST to GET and move payload to query params
    // This way it will make JSON Server that it's GET request
    req.method = 'GET';
    req.query = req.body;
  }
  // Continue to JSON Server router
  next();
};
// config.json
{
  // ...
  "middlewares": ["./mock/middlewares/_postAsGet.js"]
  // ...
}

# 同時運行 App 與 mock server 在一個 terminal

你可能覺得需要開兩個 terminal,一個 yarn start 一個 yarn mock。 其實很簡單 yarn mock & yarn start 就都會運行了,但就會發現分不出 log 是從那個 server 發出的。
推薦使用Concurrently (opens new window)解決這件事:

yarn add -D concurrently
"scripts": {
  "dev": "concurrently \"yarn:start\" \"yarn:mock\"",
}

End.