1 | # @36node/mock-server
|
2 |
|
3 | mock-server 基于 json-server, 为了更好的提供数据 mock 服务.
|
4 |
|
5 | ## Install
|
6 |
|
7 | ```bash
|
8 | $ yarn install @36node/mock-server
|
9 | ```
|
10 |
|
11 | ## Use
|
12 |
|
13 | ### 1. 在 Nodejs 中使用
|
14 |
|
15 | ```js
|
16 | #!/usr/bin/env node
|
17 |
|
18 | const mockServer = require("@36node/mock-server");
|
19 |
|
20 | const app = mockServer({
|
21 | db: {
|
22 | pets: [
|
23 | { id: 1, name: "kitty", tag: "CAT", grade: 3 },
|
24 | { id: 2, name: "pi", tag: "DOG", grade: 4 },
|
25 | ],
|
26 | },
|
27 | rewrites: {
|
28 | "/store/pets*": "/pets$1",
|
29 | },
|
30 | routers: [], // custom middle ware
|
31 | aggregations: {
|
32 | "/pets": {
|
33 | grade: records => _.sumBy(records, "grade") / records.length,
|
34 | count: records => records.length,
|
35 | },
|
36 | },
|
37 | });
|
38 |
|
39 | app.listen(3000, () => {
|
40 | console.log("JSON Server is running on port 3000");
|
41 | });
|
42 | ```
|
43 |
|
44 | ### 2. 在 webpack develop server 中使用
|
45 |
|
46 | 使用 react-app-rewired 时, 通过 config-overwrites.js 文件 配置 devServer
|
47 |
|
48 | ```js
|
49 | const stopMock = process.env.MOCK === "false" || process.env.MOCK === "FALSE";
|
50 | const defaultServerOpts = { delay: 500 };
|
51 | const {
|
52 | serverOpts = defaultServerOpts,
|
53 | db: {
|
54 | pets: [
|
55 | { id: 1, name: "kitty", tag: "CAT", grade: 3 },
|
56 | { id: 2, name: "pi", tag: "DOG", grade: 4 },
|
57 | ],
|
58 | },
|
59 | rewrites: {
|
60 | "/store/pets*": "/pets$1",
|
61 | },
|
62 | routers: [], // custom middle ware
|
63 | aggregations: {
|
64 | "/pets": {
|
65 | grade: records => _.sumBy(records, "grade") / records.length,
|
66 | count: records => records.length,
|
67 | },
|
68 | },
|
69 | } = someMockConfig;
|
70 |
|
71 | const mockServer = require("@36node/mock-server");
|
72 |
|
73 | module.exports = {
|
74 | ...otherConfig,
|
75 |
|
76 | devServer: function(configFunction) {
|
77 | return function(proxy, allowedHost) {
|
78 | const config = configFunction(proxy, allowedHost);
|
79 |
|
80 | if (stopMock) {
|
81 | return config;
|
82 | }
|
83 |
|
84 | /**
|
85 | * mock server hoc
|
86 | * @param {Express.Application} app
|
87 | */
|
88 | function configMock(app) {
|
89 | // 根据 请求的 header.accept 的类型决定是正常渲染,还是进入mock-server
|
90 | const shouldMockReq = req => {
|
91 | return (
|
92 | req.method !== "GET" ||
|
93 | (req.headers.accept &&
|
94 | req.headers.accept.indexOf("application/json") !== -1)
|
95 | );
|
96 | };
|
97 |
|
98 | if (serverOpts.delay) {
|
99 | app.use((req, res, next) => {
|
100 | if (shouldMockReq(req)) {
|
101 | return pause(serverOpts.delay)(req, res, next);
|
102 | }
|
103 | return next();
|
104 | });
|
105 | }
|
106 |
|
107 | mockServer({ app, db, rewrites, routers, shouldMockReq });
|
108 |
|
109 | return app;
|
110 | }
|
111 |
|
112 | const prev = config.before;
|
113 |
|
114 | config.before = compose(
|
115 | configMock,
|
116 | app => {
|
117 | prev(app);
|
118 | return app;
|
119 | }
|
120 | );
|
121 |
|
122 | return config;
|
123 | };
|
124 | },
|
125 | };
|
126 | ```
|
127 |
|
128 | ## Api
|
129 |
|
130 | mockServer(opts)
|
131 |
|
132 | params:
|
133 |
|
134 | 1. opts: Object
|
135 |
|
136 | db: // 同 json-server 的 db 配置 https://github.com/typicode/json-server#getting-started
|
137 |
|
138 | rewrites (Optional): 同 json-server https://github.com/typicode/json-server#rewriter-example
|
139 |
|
140 | routes (Optional): [Express.Middleware] 同 json-server custom-middle https://github.com/typicode/json-server#add-middlewares
|
141 |
|
142 | aggregations (Optional): Object 见下文 Aggregation
|
143 |
|
144 | app (Optional): Express.Application, 如果没有则自动新建
|
145 |
|
146 | shouldMock (Optional): (req, res) => Boolean, 判断 request 是否使用 mock-server 的 中间件
|
147 |
|
148 | 返回:Express.Appliction
|
149 |
|
150 | ## Array
|
151 |
|
152 | 使用标准 url query 格式传递数组数据
|
153 |
|
154 | ```curl
|
155 | a=1&a=2
|
156 | ```
|
157 |
|
158 | ## Filter
|
159 |
|
160 | Use `.` to access deep properties
|
161 |
|
162 | ```curl
|
163 | GET /posts?title=json-server&author=typicode
|
164 | GET /posts?id=1&id=2
|
165 | GET /comments?author.name=typicode
|
166 | ```
|
167 |
|
168 | ## Paginate
|
169 |
|
170 | Use `_offset` and optionally `_limit` to paginate returned data. (an `X-Total-Count` header is included in the response)
|
171 |
|
172 | In the `Link` header you'll get `first`, `prev`, `next` and `last` links.
|
173 |
|
174 | ```curl
|
175 | GET /posts?_offset=10
|
176 | GET /posts?_offset=7&_limit=20
|
177 | ```
|
178 |
|
179 | note: _10 items are returned by default_
|
180 |
|
181 | ## Sort
|
182 |
|
183 | Add `_sort` and `_order` (ascending order by default)
|
184 |
|
185 | ```curl
|
186 | # asc
|
187 | GET /posts?_sort=views
|
188 |
|
189 | # desc
|
190 | GET /posts/1/comments?_sort=-votes
|
191 | ```
|
192 |
|
193 | note: _list posts by views ascending order and comments by votes descending order_
|
194 |
|
195 | For multiple fields, use the following format:
|
196 |
|
197 | ```curl
|
198 | GET /posts/1/comments?_sort=-votes&_sort=likes
|
199 | ```
|
200 |
|
201 | \_prefixing a path with `-` will flag that sort is descending order.
|
202 | When a path does not have the `-` prefix, it is ascending order.
|
203 |
|
204 | ## Operators
|
205 |
|
206 | Add `_gt`, `_lt`, `_gte` or `_lte` for getting a range
|
207 |
|
208 | ```curl
|
209 | GET /posts?views_gte=10&views_lte=20
|
210 | ```
|
211 |
|
212 | Add `_ne` to exclude a value
|
213 |
|
214 | ```curl
|
215 | GET /posts?id_ne=1
|
216 | ```
|
217 |
|
218 | Add `_like` to filter (RegExp supported)
|
219 |
|
220 | `_like` support array
|
221 |
|
222 | ```curl
|
223 | GET /posts?title_like=server
|
224 | ```
|
225 |
|
226 | ## Select
|
227 |
|
228 | Specifies which document fields to include or exclude
|
229 |
|
230 | ```curl
|
231 | GET /posts?_select=title&_select=body
|
232 | GET /posts?_select=-comments&_select=-views
|
233 | ```
|
234 |
|
235 | or
|
236 |
|
237 | ```curl
|
238 | _select=title,body
|
239 | ```
|
240 |
|
241 | _prefixing a path with `-` will flag that path as excluded._
|
242 | _When a path does not have the `-` prefix, it is included_
|
243 | _A projection must be either inclusive or exclusive._
|
244 | _In other words, you must either list the fields to include (which excludes all others),_
|
245 | _or list the fields to exclude (which implies all other fields are included)._
|
246 |
|
247 | ## Aggregation
|
248 |
|
249 | 聚合的 query 请求。聚合请求通过 \_group 和 \_select 参数来控制,通过 opts.aggregations 配置:
|
250 |
|
251 | 比如对于一个 db 配置:
|
252 |
|
253 | ```js
|
254 | const faker = require("faker");
|
255 | const _ = require("lodash");
|
256 | const moment = require("moment");
|
257 |
|
258 | const now = moment();
|
259 |
|
260 | const generate = count =>
|
261 | _.range(count).map((val, index) => {
|
262 | const birthAt = faker.date.between(
|
263 | moment()
|
264 | .subtract(10, "year")
|
265 | .toDate(),
|
266 | moment()
|
267 | .subtract(1, "year")
|
268 | .toDate()
|
269 | );
|
270 |
|
271 | const age = now.diff(moment(birthAt), "year");
|
272 |
|
273 | return {
|
274 | id: faker.random.uuid(), // pet id
|
275 | name: faker.name.lastName(), // pet name
|
276 | tag: faker.random.arrayElement(["CAT", "DOG"]), // pet tag
|
277 | owner: faker.name.firstName(), // pet owner
|
278 | grade: faker.random.number({ min: 1, max: 5 }), // pet grade
|
279 | age, // pet age
|
280 | birthAt: birthAt.toISOString(), // pet birth time
|
281 | };
|
282 | });
|
283 |
|
284 | const db = {
|
285 | pets: generate(100),
|
286 | };
|
287 | ```
|
288 |
|
289 | 其中包括了 100 个 pets 的 mock 数据,可使用的路由有:
|
290 |
|
291 | ```
|
292 | GET /pets
|
293 | GET /pets/{petId}
|
294 | POST /pets
|
295 | PUT /pets/{petId}
|
296 | PATCH /pets/{petId}
|
297 | DELETE /pets/{petId}
|
298 | ```
|
299 |
|
300 | 聚合只在 `GET /pets` 中有效
|
301 |
|
302 | ### 简单分组
|
303 |
|
304 | 如果需要统计 pets 中猫和狗的数量, 可以对 tag 分组
|
305 |
|
306 | 配置 aggregations 参数
|
307 |
|
308 | ```js
|
309 | aggregations: {
|
310 | "/pets": {
|
311 | // records 是分组后的数据集合
|
312 | count: records => records.length,
|
313 | // 默认支持 两种聚合简写 求和 'sum' 和 平均 ‘avg'
|
314 | grade: 'avg',
|
315 | },
|
316 | },
|
317 | ```
|
318 |
|
319 | 请求:
|
320 |
|
321 | ```
|
322 | GET /pets?_group=tag
|
323 | ```
|
324 |
|
325 | 结果:
|
326 |
|
327 | ```json
|
328 | [
|
329 | {
|
330 | "id": "tag=CAT",
|
331 | "tag": "CAT",
|
332 | "grade": 3.017857142857143,
|
333 | "count": 56
|
334 | },
|
335 | {
|
336 | "id": "tag=DOG",
|
337 | "tag": "DOG",
|
338 | "grade": 3.3863636363636362,
|
339 | "count": 44
|
340 | }
|
341 | ]
|
342 | ```
|
343 |
|
344 | ### 按时间粒度分组
|
345 |
|
346 | 如果需要统计每个月分别生了多少猫和狗, 可以按 tag 和 birthAt.month 分组
|
347 |
|
348 | 对于时间的分组条件,可以采用不同粒度进行分组,query 的格式为 `birthAt.month` 表示在 birthAt 字段上 按照 月粒度进行分组。
|
349 |
|
350 | 支持的粒度包括:
|
351 |
|
352 | ```js
|
353 | [
|
354 | "year", // 年
|
355 | "quarter", // 季度
|
356 | "month", // 月
|
357 | "week", // 星期
|
358 | "isoWeek", // iso 星期
|
359 | "day", // 天
|
360 | "hour", // 小时
|
361 | "min", // 分钟
|
362 | "second", // 秒
|
363 | ];
|
364 | ```
|
365 |
|
366 | 请求:
|
367 |
|
368 | ```
|
369 | GET /pets?_group=tag&_group=birthAt.month
|
370 | ```
|
371 |
|
372 | 结果:
|
373 |
|
374 | ```json
|
375 | [
|
376 | ...,
|
377 | {
|
378 | "id": "tag=CAT&birthAt=2012-12-31T16%3A00%3A00.000Z",
|
379 | "tag": "CAT",
|
380 | "birthAt": "2012-12-31T16:00:00.000Z",
|
381 | "count": 6
|
382 | },
|
383 | {
|
384 | "id": "tag=DOG&birthAt=2014-12-31T16%3A00%3A00.000Z",
|
385 | "tag": "DOG",
|
386 | "birthAt": "2014-12-31T16:00:00.000Z",
|
387 | "count": 7
|
388 | }
|
389 | ]
|
390 | ```
|
391 |
|
392 | Tips:
|
393 |
|
394 | 1. 在返回结果中,birthAt 当前月的起始时间(UTC),如果使用其他粒度,则类似。
|
395 | 2. 如果同时传入统一字段的多个时间粒度,比如 `_group=birthAt.year&_group=birthAt.month`, 则较小的时间粒度(month)会生效.
|