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