-
Notifications
You must be signed in to change notification settings - Fork 25
/
business.js
1277 lines (1191 loc) · 45.4 KB
/
business.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
const lib = require(`./lib.js`)
const { print } = require('./log.js')
const tool = require(`./tool.js`)
function business() { // 与业务相关性的函数
/**
* 通过重新保存文件的方式触发 nodemon 的文件监听, 然后让服务重启
*/
function reStartServer(filePath) {
const fs = require(`fs`)
const str = fs.readFileSync(filePath, `utf8`)
fs.writeFileSync(filePath, str)
}
function wrapApiData({data, code = 200}) { // 包裹 api 的返回值
code = String(code)
return {
code,
success: Boolean(code.match(/^[2]/)), // 如果状态码以2开头则为 true
data,
}
}
/**
* 把类似 schema 的列表转换为数据
* @param {array} list 列表
* @param {object} options 规则
*/
function listToData(list, options = {}){
const Mock = require('better-mock')
const mockMethods = Object.keys(Mock.Random).map(key => `@${key}`)
function listToDataRef (list) {
// 注: 通过此函数转换出的结果并不是可以生成随机值的模板, 而是已生成固定值模板, 因为需要使用值转换为对应的类型
let res = {}
list.forEach(item => {
let example = item.example ? String(item.example) : ``
if(item.type === `eval`) { // 使用代码执行结果
try {
const { NodeVM } = require(`vm2`)
const vm = new NodeVM({
sandbox: { // 给 vm 使用的变量
Mock,
}
})
example = vm.run(`module.exports = ${example}`, `vm.js`) || ``;
} catch (err) {
console.log(`err`, err)
}
// 处理含有 @mock 方法或为正则的 example
} else if(mockMethods.some(item => example.includes(item)) === false) { // 如果不包含 @mock 方法则进行类型转换
// todo 不应该直接使用 includes 判断, 例如可以是 `@inc` 或 `@inc c` 但不能是 `@incc`
// 根据 type 转换 example 值的类型
if(strReMatch(example)) { // 猜测为正则
example = strReMatch(example)
} else if(item.type === `number`) { // 数字
example = Number(example)
} else if(item.type === `boolean`) { // 布尔
example = [`false`, `0`, `假`, `T`, `t`].includes(example) ? false : Boolean(example)
}
}
// 如果是对象或数里进行递归调用
if([`object`, `array`].includes(item.type) && Array.isArray(item.children)) {
switch(item.type) {
case `object`:
res[item.name] = listToDataRef(item.children)
break;
case `array`:
res[item.name] = res[item.name] || []
res[item.name].push(listToDataRef(item.children))
break;
default:
console.log(`no type`, item.type)
}
} else { // 如果不是引用类型, 则应用最后转换后的值 example
res[`${item.name}#${item.type || 'string'}`] = example
}
})
return res
}
let res = listToDataRef(list)
res = {
[`data${options.rule ? `|${options.rule}` : ''}`]: {object: res, array: [res]}[options.type]
}
const data = Mock.mock(res)
return data
}
/**
* 如果字符串是正则就返回正则, 否则返回 false
* @param {string} str 前后有 / 号的字符串
*/
function strReMatch (str) {
let reStr = (str.match(/^\/(.*)\/$/) || [])[1]
let re = undefined
if(reStr) {
try {
re = new RegExp(reStr)
} catch (error) { // 正则转换失败
return false
}
} else {
return false
}
return re
}
function staticHandle({config}) { // 处理 static 为 api, 实际上是转换为 use 中间件的形式
let obj = {}
config.static.map(item => {
obj[`use ${item.path}`] = [
async (req, res, next) => { // mode history
if(item.mode === `history`) {
await tool.generate.initPackge(`connect-history-api-fallback`)
require(`connect-history-api-fallback`)(item.option)(req, res, next)
} else {
next()
}
},
require('serve-static')(item.fileDir),
]
})
return obj
}
function apiWebHandle({config}) { // 处理 webApi 为 api
const apiWebStore = tool.file.fileStore(config.apiWeb)
const paths = apiWebStore.get(`paths`)
const disableApiList = apiWebStore.get(`disable`)
const pathList = Object.keys(paths).map(path => {
return Object.keys(paths[path]).map(method => ({
key: `${method} ${path}`,
path,
method,
...paths[path][method],
}))
}).flat()
const apiObj = pathList.reduce((acc, cur, curIndex) => {
let fn = (req, res, next) => {
const {example = {}, table = []} = cur.responses[`200`]
const {headers = {}, useDataType = `table`, custom, history, rule, type} = example
if(useDataType === `table`) { // 使用表格中的数据
try {
const { setHeader } = clientInjection({config})
setHeader(res, headers) // 设置自定义 header
let data
const listToDataRes = listToData(table, {rule, type})
data = listToDataRes.data
// 根据 apiWebWrap 处理数据
if(config.apiWebWrap === true) {
data = wrapApiData({data, code: 200})
} else if(typeof(config.apiWebWrap) === `function`) {
data = config.apiWebWrap({data, code: 200})
}
res.json(data)
} catch (error) {
res.status(500).json({msg: `转换错误: ${error.message}`})
}
}
if (useDataType === `custom`) { // 自定义逻辑
try {
const { NodeVM } = require(`vm2`);
const vm = new NodeVM({
sandbox: { // 给 vm 使用的变量
tool: {
libObj: lib,
wrapApiData,
listToData,
cur,
},
}
});
const code = vm.run(`module.exports = ${custom}`, `vm.js`) || ``;
const codeType = typeof(code)
if([`function`].includes(codeType)) { // 如果是函数则运行函数
code(req, res, next)
} else { // 函数之外的类型则当作 json 返回
res.json(code)
}
} catch (err) {
console.log(`err`, err)
// 处理客户端代码出现问题, 代码错误或出现不允许的权限
res.status(403).json({
msg: err.message
})
}
}
if (useDataType === `history`) { // 使用历史记录
tool.middleware.replayHistoryMiddleware({
id: history,
config,
business: business(),
})(req, res, next)
}
}
if(cur.method === `ws`) { // 如果是 websocket 方法则更换函数模版
fn = (ws, req) => {
const sendData = (data) => {
const strData = JSON.stringify(data)
ws.send(strData)
ws.on('message', (msg) => {
ws.send(strData)
})
}
const {example = {}, table = []} = cur.responses[`200`]
const {headers = {}, useDataType = `table`, custom, history, rule, type} = example
if(useDataType === `table`) { // 使用表格中的数据
try {
let data
const listToDataRes = listToData(table, {rule, type})
data = listToDataRes.data
sendData(data)
} catch (error) {
sendData({msg: `转换错误: ${error.message}`})
}
}
if (useDataType === `custom`) { // 自定义逻辑
try {
const { NodeVM } = require(`vm2`);
const vm = new NodeVM({
sandbox: { // 给 vm 使用的变量
tool: {
libObj: lib,
wrapApiData,
listToData,
cur,
},
}
});
const code = vm.run(`module.exports = ${custom}`, `vm.js`) || ``;
const codeType = typeof(code)
if([`function`].includes(codeType)) { // 如果是函数则运行函数
code(ws, req)
} else { // 函数之外的类型则当作 json 返回
sendData(code)
}
} catch (err) {
console.log(`err`, err)
// 处理客户端代码出现问题, 代码错误或出现不允许的权限
sendData({msg: err.message})
}
}
}
}
fn.type = `apiWeb`
fn.description = cur.description
fn.disable = disableApiList.includes(`/`) // 如果含有根结点, 则表示全部禁用
? true
: disableApiList.includes(cur.key)
return {
...acc,
[cur.key]: fn,
}
}, {})
return apiObj
}
function customApi({api, db, config}) {
/**
* 自定义 api 处理程序, 包括配置中的用户自定义路由(config.api), 以及mock数据生成的路由(config.db)
*/
function parseApi() { // 解析自定义 api
const pathToRegexp = require('path-to-regexp')
const serverRouterList = [] // server 可使用的路由列表
Object.keys(api).forEach(key => {
let {method, url} = tool.url.fullApi2Obj(key)
method = method.toLowerCase()
let val = api[key]
if(method === `use`) { // 自定义中间件时不使用自动返回 json 的规则
if([`function`, `array`].includes(tool.type.isType(val)) === false) { // use 支持单个和多个(数组)中间件
print(tool.cli.colors.red(`Data other than function|array type is not allowed in the use mode in config.api: ${val}`))
val = (req, res, next) => next()
}
} else if([
`string`,
`number`,
`object`,
`array`,
`boolean`,
`null`,
].includes(tool.type.isType(val))) { // 如果配置的值是 json 支持的数据类型, 则直接作为返回值, 注意, 读取一个文件也是对象
const backVal = val
if(method === `ws`) {
val = (ws, req) => {
const strData = JSON.stringify(backVal)
ws.send(strData)
ws.on('message', (msg) => ws.send(strData))
}
} else {
val = (req, res, next) => res.json(backVal)
}
}
const re = pathToRegexp(url)
serverRouterList.push({method, router: url, action: val, re})
})
function noProxyTest({upgrade, method, pathname}) {
// return true 时不走真实服务器, 而是走自定义 api
return serverRouterList.some(item => {
if (((item.method === `ws` ) && (method === `get` ))) { // ws 连接时, 实际上得到的 method 是 get, 并且 pathname + .websocket
return (
item.re.exec(pathname)
&& upgrade.match(/websocket/i)
&& Boolean(item.action.disable) === false
)
}
if(item.method === `use`) { // 当为中间件模式时, 匹配其后的任意路径
return (
pathname.startsWith(item.router)
&& Boolean(item.action.disable) === false
)
}
// 当方法相同时才去匹配 url
if(((item.method === `all`) || (item.method === method))) {
return (
item.re.exec(pathname) // 如果匹配到自定义的 api 则走自定义 api
&& Boolean(item.action.disable) === false // 如果自定义 api 为禁用状态, 则走真实服务器
)
} else {
return false
}
})
}
return {
serverRouterList,
noProxyTest,
}
}
function parseDbApi() {
const isType = tool.type.isType
let apiList = []
Object.keys(db).forEach(key => {
const val = db[key]
if (isType(val, `object`)) {
`get post put patch`.split(` `).forEach(method => {
apiList.push({
method,
path: `/${key}`,
})
})
}
if (isType(val, `array`)) {
`get post`.split(` `).forEach(method => {
apiList.push({
method,
path: `/${key}`,
type: `db`,
})
})
;`get put patch delete`.split(` `).forEach(method => {
apiList.push({
method,
path: `/${key}/:id`,
type: `db`,
})
})
return apiList
}
})
apiList = apiList.concat(Object.keys(config.route).map(key => {
return {
path: key,
type: `route`,
}
}))
return apiList
}
function getDataRouter({method, pathname}) {
/**
给定一个 method 和 path, 根据 db.json 来判断是否应该过滤
根据 db.json 获取要拦截的 route , 参考 node_modules/json-server/lib/server/router/index.js
*/
const pathToRegexp = require('path-to-regexp')
method = method.trim().toLowerCase()
const isType = tool.type.isType
const res = Object.keys(db).some(key => {
const execPathname = pathToRegexp(`/${key}`).exec(pathname)
const val = db[key]
if (isType(val, `object`)) {
return `get post put patch `.includes(`${method} `) && execPathname // 方法与路由匹配
}
if (isType(val, `array`)) {
return (
(`get post `.includes(`${method} `) && execPathname) // 获取所有或创建单条
|| (`get put patch delete `.includes(`${method} `) && pathToRegexp(`/${key}/:id`).exec(pathname)) // 处理针对于 id 单条数据的操作, 注意 id 的取值字段 foreignKeySuffix
)
}
})
return res
}
return {
parseApi: parseApi(),
parseDbApi: parseDbApi(),
getDataRouter,
}
}
/**
* 过时的 API 提示
* @param {*} param0
* @param {*} param0.type api 类型: cli 命令行 option 选项
* @param {*} param0.no 旧API
* @param {*} param0.yew 新API
* @param {*} param0.v 被删除的版本
*/
function oldAPI({type, no, yes, v}) {
type === `cli` && print(tool.cli.colors.red(`API更新: 请将命令行参数 ${no} 替换为 ${yes}, 在 v${v} 版本之后 ${no} 将停止使用!`))
}
function initHandle() { // 初始化处理程序
/**
* 检查运行环境是否兼容
*/
function checkEnv () {
return lib.compareVersions.compare(process.version, `10.12.0`, `>=`)
}
function templateFn({cliArg, version}) {
if(cliArg[`--template`]) {
const path = require(`path`)
const cwd = process.cwd()
if(tool.file.hasFile(cwd) === false) {
require(`fs`).mkdirSync(cwd, { recursive: true })
}
const copyPath = path.normalize(`${cwd}/mm/`)
tool.file.copyFolderSync(path.normalize(`${__dirname}/../example/template/`), copyPath)
{ // 创建 package.json 中的 scripts 和 devDependencies
const fs = require(`fs`)
const jsonPath = `${process.cwd()}/package.json`
const hasJson = tool.file.hasFile(jsonPath)
const scripts = `npx mockm --cwd=mm`
const devDependencies = version
if(
hasJson
&& (require(jsonPath).scripts || {}).mm
&& (require(jsonPath).devDependencies || {}).mockm
) {
// console.log(`无需修改`)
} else {
hasJson === false && fs.writeFileSync(jsonPath, tool.string.removeLeft(`
{
"scripts": {
"mm": "${scripts}"
},
"devDependencies": {
"mockm": "${devDependencies}"
}
}
`).trim())
let packageText = fs.readFileSync(jsonPath, `utf8`)
const packageJson = JSON.parse(packageText)
packageJson.scripts = packageJson.scripts || {}
packageJson.devDependencies = packageJson.devDependencies || {}
packageJson.scripts.mm = packageJson.scripts.mm || scripts
packageJson.devDependencies.mockm = packageJson.devDependencies.mockm || devDependencies
const split = (packageText.match(/[\t ]+/) || [` `])[0] // 获取缩进风格
packageText = JSON.stringify(packageJson, null, split)
fs.writeFileSync(jsonPath, packageText)
}
}
process.chdir(copyPath)
print(`模板 ${copyPath} 已创建成功!`)
print(`使用命令 npm run mm`)
}
}
function configFileFn({cliArg}) {
const path = require(`path`)
const fs = require(`fs`)
const cwdConfigPath = `${process.cwd()}/mm.config.js`
const hasCwdConfig = tool.file.hasFile(cwdConfigPath)
let res = `${__dirname}/../config.js` // 默认配置文件
cliArg[`config`] && (cliArg[`--config`] = cliArg[`config`]) && oldAPI({
type: `cli`,
no: `config`,
yes: `--config`,
v: `1.1.26`,
});
if((cliArg[`--config`] === true) && (hasCwdConfig === false)) { // 如果 config=true 并且当前目录没有配置时, 则生成示例配置并使用
const example = fs.readFileSync( `${__dirname}/../example/full.mm.config.js`, `utf8`)
fs.writeFileSync(cwdConfigPath, example)
res = cwdConfigPath
} else if((cliArg[`--config`] === true) && (hasCwdConfig === true)) { // 使用生成的示例配置
res = cwdConfigPath
} else if(typeof(cliArg[`--config`]) === `string`) { // 命令行上指定的 config 文件
res = cliArg[`--config`]
} else if(tool.file.hasFile(cwdConfigPath)) { // 命令运行位置下的配置
res = cwdConfigPath
}
res = path.normalize(res)
cliArg[`--config`] = res
return res
}
function getOpenApi({openApi}) { // 使用服务器获取远程 openApi , 避免跨域
const [, tag = ``, username, password] = openApi.match(/:\/\/((.+):(.+)@)/) || []
openApi = openApi.replace(tag, ``)
const axios = require('axios')
return new Promise((resolve, reject) => {
axios.get(openApi, {
auth: username ? {username, password} : {},
}).then(res => {
resolve(res.data)
}).catch(err => {
print(`err`, `Failed to get openApi`)
reject(err.message)
})
})
}
function getDb({config}) { // 根据配置返回 db
const fs = require(`fs`)
const newDb = config.db()
const o2s = tool.obj.o2s
if(tool.file.isFileEmpty(config.dbJsonPath) || config.dbCover) { // 如果 db 文件为空或声明总是覆盖, 都重写整个文件
fs.writeFileSync(config.dbJsonPath, o2s(newDb))
}
const oldDb = require(config.dbJsonPath)
const resDb = {...newDb, ...oldDb}
fs.writeFileSync(config.dbJsonPath, o2s(resDb)) // 更新 db 文件, 因为 jsonServer.router 需要用它来生成路由
return resDb
}
function init({config}) { // 初始化, 例如创建所需文件, 以及格式化配置文件
const fs = require(`fs`)
const fileStore = tool.file.fileStore
if(tool.file.hasFile(config.dataDir) === false) { // 如果没有目录则创建目录
fs.mkdirSync(config.dataDir, {recursive: true})
}
fileStore(config._httpHistory)
fileStore(config.apiWeb, {
paths: {},
disable: [],
})
{ // 监听自定义目录更改后重启服务
const nodemon = require(`nodemon`)
tool.type.isEmpty(config.watch) === false && nodemon({
exec: `node -e 0`, // 由于必须存在 exec 参数, 所以放置一条啥也不干的命令
watch: config.watch,
}).on('restart', () => {
reStartServer(config.config)
})
}
{ // 配置 httpData 目录中的 gitignore
tool.file.isFileEmpty(config._gitIgnore.file)
&& fs.writeFile(
config._gitIgnore.file,
tool.string.removeLeft(config._gitIgnore.content).trim(),
() => {}
)
}
{ // 初始化错误日志保存文件
tool.file.isFileEmpty(config._errLog)
&& fs.writeFile(
config._errLog,
tool.string.removeLeft(`
readme:
- 本文件用于存储 mockm 运行过程中捕获到的一些错误.
`).trim(),
() => {}
)
}
{ // 初始化 store 中的内容
const osIp = config.osIp
const store = fileStore(config._store, {
apiCount: 0,
note: {
remote: {},
},
})
// 需要每次根据 osIp 更新调试地址
store.set(`note.local`, {
port: `http://${osIp}:${config.port}`,
replayPort: `http://${osIp}:${config.replayPort}`,
testPort: `http://${osIp}:${config.testPort}`,
})
}
{ // 清理 history
config.clearHistory && business().historyHandle().clearHistory(config)
}
{ // 定时备份 openApi
const openApiList = Boolean(config.openApi && config.backOpenApi) === false ? [] : {
string: () => [config.openApi],
array: () => config.openApi,
object: () => Object.values(config.openApi),
}[tool.type.isType(config.openApi)]()
const backFn = () => {
openApiList.forEach(item => {
tool.file.backUrl(config._openApiHistoryDir, item, data => { // 格式化 openApi 后再保存, 避免压缩的内容不易比较变更
return JSON.stringify(JSON.parse(data), null, 2)
})
})
}
backFn()
setInterval(backFn, config.backOpenApi * 60 * 1000)
}
fileStore(config._share, {config}).set(`config`, config)
const db = getDb({config})
const { setHeader, allowCors } = clientInjection({config})
const run = {
async curl({req, res, cmd}) { // cmd: curl/bash
const options = await tool.cli.getOptions(cmd)
return new Promise(async (resolve, reject) => {
const request = await tool.generate.initPackge(`request`)
request(options, (err, curlRes = {}, body) => {
setHeader(res, curlRes.headers) // 复制远程的 header
allowCors({req, res}) // 设置 header 为允许跨域模式
const mergeRes = curlRes
err ? reject(err) : resolve(mergeRes)
})
})
},
fetch({req, res, fetchRes}) { // node-fetch
return new Promise((resolve, reject) => {
fetchRes.then(fetchThenRes => {
const headers = [...fetchThenRes.headers].reduce((acc, cur) => ({...acc, [cur[0]]: cur[1]}), {})
const contentEncoding = headers[`content-encoding`]
if(contentEncoding && contentEncoding.includes(`gzip`)) {
// 由于返回的内容其实已经被解码过了, 所以不能再告诉客户端 content-encoding 是压缩的 `gzip`, 否则会导致客户端无法解压缩
// - 例如导致浏览器无法解码: net::ERR_CONTENT_DECODING_FAILED 200 (OK)
delete headers[`content-encoding`]
}
setHeader(res, headers)
allowCors({req, res})
const mergeRes = fetchThenRes
resolve(mergeRes)
}).catch(err => {
console.log(`err`, err)
reject(err)
})
})
},
}
const api = {
...business().apiWebHandle({config}),
...config.api({ // 向 config.api 暴露一些工具库
run,
}),
...business().staticHandle({config}), // warn: use 放在后面其实是具有较低优先级
}
const apiRootInjection = api[`*`] || api[`/`] || function (req, res, next) {return next()} // 统一处理所有自定义的根级别拦截器
// 移交给 apiRootInjection, 它表示所有【自定义api, config.api 和 config.db 产生的 api】前的拦截功能
// 为什么要删掉?
// 因为这是用于判断是否进入 proxy 的条件
// 如果不删除会导致恒等匹配成功, 无法进入 proxy
delete api[`/`]
delete api[`*`]
return {
db,
api,
apiRootInjection,
}
}
return {
templateFn,
checkEnv,
wrapApiData,
configFileFn,
init,
getOpenApi,
}
}
function historyHandle() {
/**
* 历史记录处理
*/
/**
* 获取原始 history
* @param {object} param0 参数
* @param {object} param0.history require(config._httpHistory)
*/
function getRawHistory({history}) {
let list = []
list = Object.keys(history).reduce((acc, cur) => {
return acc.concat(history[cur])
}, [])
return list
}
/**
* 获取简要信息的 history 列表
* @param {object} param0 参数对象
* @param {object} param0.history require(config._httpHistory)
* @param {string} param0.method 方法 - 可选
* @param {id} param0.method 方法 - 可选
* @param {string} param0.api api - 可选
* @return {array} 数组
*/
function getHistoryList({md5 = false, history, method: methodRef, api: apiRef} = {}) {
const fs = require(`fs`)
let list = getRawHistory({history})
list = list.filter(item => item.data).map(({path, fullApi, id, data: {req, res}}) => {
const {method, url} = tool.url.fullApi2Obj(fullApi)
if(methodRef && apiRef) {
if(((method === methodRef) && (url === apiRef)) === false) { // 如果没有找到就返回, 找到才进入数据处理
return false
}
}
const reqBodyPath = req.bodyPath
const resBodyPath = res.bodyPath
const resBodySize = resBodyPath && tool.file.hasFile(resBodyPath) ? fs.statSync(resBodyPath).size : 0
const resBodyMd5 = resBodyPath && md5 && tool.file.hasFile(resBodyPath) ? tool.file.getMd5Sync(resBodyPath) : undefined
const reqBodySize = reqBodyPath && tool.file.hasFile(reqBodyPath) ? fs.statSync(reqBodyPath).size : 0
const reqBodyMd5 = reqBodyPath && md5 && tool.file.hasFile(reqBodyPath) ? tool.file.getMd5Sync(reqBodyPath) : undefined
return {
id,
method,
path,
api: url,
fullApi,
statusCode: res.lineHeaders.line.statusCode,
contentType: res.lineHeaders.headers[`content-type`],
extensionName: (resBodyPath || '').replace(/(.*)(\.)/, ''),
resBodySize,
resBodyMd5,
resBodyPath,
reqBodySize,
reqBodyMd5,
reqBodyPath,
date: res.lineHeaders.headers.date,
}
}).filter(item => item)
return list
}
/**
* 获取单条记录的 history
* @param {object} param0 参数对象
* @param {object} param0.history require(config._httpHistory)
* @param {string} param0.fullApi `method api` 可选
* @param {function} param0.find 自定义筛选逻辑
* @return {object} 为空时返回空对象
*/
function getHistory({history, fullApi, id, status, find}) { // 获取指定 fullApi/id 中的历史记录
if(fullApi === undefined && id) {
return getRawHistory({history}).find(item => item.id === id) || {}
}
const { path } = tool.url.fullApi2Obj(fullApi)
const list = [...(history[path] || [])].reverse().filter(item => ( // 传入 id 时比较 id
(id === undefined ? true : (item.id === id))
&& (item.fullApi === fullApi)
))
const res = find ? find(list) : list[0] || {}
return res
}
function ignoreHttpHistory({config, req}) { // 是否应该记录 req
const {method, url} = req
return Boolean(
method.match(/OPTIONS/i)
|| (
method.match(/GET/i) && url.match(new RegExp(`//${config._proxyTargetInfo.pathname}//`))
)
)
}
function createBodyPath({config, req, headersObj, reqOrRes, apiId}) { // 根据 url 生成文件路径, reqOrRes: req, res
const filenamify = require('filenamify')
const fs = require(`fs`)
const mime = require('mime')
const headers = headersObj[reqOrRes]
const contentType = headers[`content-type`]
const extensionName = mime.getExtension(contentType) || ``
const {url, path} = tool.httpClient.getClientUrlAndPath(req.originalUrl)
let {
method,
} = req
method = method.toLowerCase()
const newPath = () => {
const osPath = require(`path`)
const basePath = osPath.relative(process.cwd(), config._requestDir) // 获取相对路径下的 dataDir 目录
const apiDir = osPath.normalize(`./${basePath}/${path}`).replace(/\\/g, `/`) // 以 path 创建目录, 生成相对路径以避免移动 dataDir 后无法使用
if(tool.file.hasFile(apiDir) === false) { // 如果不存在此目录则进行创建
fs.mkdirSync(apiDir, { recursive: true })
}
let shortUrl = url.indexOf(path) === 0 ? url.replace(path, ``) : url // 为了节约目录长度删除 url 中的 path 部分, 因为 pathDir 已经是 path 的表示
shortUrl = shortUrl.slice(1, 100)
const filePath = `${apiDir}/${
filenamify(
`${shortUrl}_${method}_${apiId}_${reqOrRes}.${extensionName}`,
{maxLength: 255, replacement: '_'}
)
}`
// 如果 filePath 已存在于记录中, 则使用新的
return filePath
}
// 使用 bodyPath 的后缀判断文件类型, 如果与新请求的 contentType 不同, 则更改原文件名后缀
let bodyPath = newPath()
return bodyPath
}
function createHttpHistory({config, history, dataDir, buffer, req, res}) {
const fs = require(`fs`)
let {
method,
} = req
method = method.toLowerCase()
const {url, path} = tool.httpClient.getClientUrlAndPath(req.originalUrl)
const headersObj = {req: req.headers || req.getHeaders(), res: res.headers || res.getHeaders()}
headersObj.res.date = headersObj.res.date || (new Date()).toGMTString() // 居然没有 date ?
const {statusCode, statusMessage, headers} = res
const fullApi = `${method} ${url}`
const reqBody = req.body
// 保存 body 数据文件, 由于操作系统对文件名长度有限制, 下面仅取 url 的前 100 个字符, 后面自增
const apiCount = tool.file.fileStore(config._store).updateApiCount()
const apiId = tool.hex.string10to62(apiCount)
function getBodyPath() {
const arg = {config, req, headersObj, dataDir, apiId}
return {
bodyPathReq: tool.type.isEmpty(reqBody) === false ? createBodyPath({...arg ,reqOrRes: `req`}) : undefined,
bodyPathRes: tool.type.isEmpty(buffer) === false ? createBodyPath({...arg ,reqOrRes: `res`}) : undefined,
}
}
const {bodyPathReq, bodyPathRes} = getBodyPath()
bodyPathReq && fs.writeFileSync(bodyPathReq, JSON.stringify(reqBody), {encoding: 'utf8'})
bodyPathRes && fs.writeFileSync(bodyPathRes, buffer, {encoding: 'buffer'})
const resDataObj = {
req: {
lineHeaders: {
line: tool.obj.removeEmpty({
method,
url,
path,
query: req.query,
params: req.params,
version: req.httpVersion,
}),
headers: headersObj.req,
// _header: proxyRes.req._header,
},
// body: null,
bodyPath: bodyPathReq,
},
res: {
lineHeaders: {
line: {
statusCode,
statusMessage,
version: res.httpVersion,
},
headers: headersObj.res,
// _header: res._header,
},
// body: null,
bodyPath: bodyPathRes,
},
}
setHttpHistory({
config,
data: {path, fullApi, id: apiId, data: resDataObj},
history,
})
}
function setHttpHistory({config, data, history}) {
const fs = require(`fs`)
const {path} = data
history[path] = (history[path] || []).concat(data)
fs.writeFileSync(config._httpHistory, tool.obj.o2s(history))
}
function setHttpHistoryWrap({config, history, req, res, mock = false, buffer}) { // 从 req, res 记录 history
if(ignoreHttpHistory({config, req}) === false) {
const data = [];
const arg = {
config,
history,
buffer,
req,
res,
}
clientInjection({config}).setApiInHeader({req, res})
if(mock === true) {
createHttpHistory(arg)
return false
} else {
let isSave = false
// eslint-disable-next-line no-inner-declarations
function saveHistory(ev) {
if(isSave === false) { // 没有保存过才进行保存
const buffer = Buffer.concat(data)
createHttpHistory({...arg, buffer})
isSave = true
}
}
res.on('data', function(chunk) {
data.push(chunk)
})
req.on('close', () => saveHistory(`req close`))
res.on('close', () => saveHistory(`res close`))
}
}
}
function clearHistory(config) {
function getDelIdList(list, options = {}) {
options = {
retentionTime: 60 * 24 * 3,
num: 1,
...options,
}
list = list.filter(item => { // 获取多少分钟之前的请求
return Date.now() - new Date(item.date).getTime() > options.retentionTime * 60 * 1000
} )
let obj = {} // 合并相同内容为对象
list = tool.array.sort(list, {key: `id`})
list.forEach(({id, resBodyMd5, reqBodyMd5, fullApi, statusCode}) => {
const tag = [fullApi, statusCode, resBodyMd5, reqBodyMd5].join(` | `)
obj[tag] = [id, ...obj[tag] || []]
if(options.num < 0) {
obj[tag] = obj[tag].reverse()
}
})
let delIdList = [] // 获取要删除的 ID 列表
Object.keys(obj).forEach(key => {
delIdList = delIdList.concat(obj[key].slice(Math.abs(options.num)))
})
return delIdList
}
const HTTPHISTORY = require(config._httpHistory) // 请求历史
let list = business().historyHandle().getHistoryList({history: HTTPHISTORY, md5: true})
const delIdList = {
function: config.clearHistory,
object: list => getDelIdList(list, config.clearHistory),
boolean: list => config.clearHistory ? getDelIdList(list) : [],
undefined: () => [],
}[tool.type.isType(config.clearHistory)](list)
// 删除文件
const fs = require(`fs`)
delIdList.forEach(id => {
let {reqBodyPath, resBodyPath, path} = list.find(item => item.id === id) || {}
const apiList = HTTPHISTORY[path] || []
const findIndex = id => apiList.findIndex(item => item.id === id)
// 删除 json 中的记录
apiList.splice(findIndex(id), 1)
if(findIndex(id) === -1) { // 如果删除成功则删除对应的文件
reqBodyPath && tool.file.hasFile(reqBodyPath) && fs.unlinkSync(reqBodyPath)
resBodyPath && tool.file.hasFile(resBodyPath) && fs.unlinkSync(resBodyPath)
}
})
fs.writeFileSync(config._httpHistory, tool.obj.o2s(HTTPHISTORY))
delIdList.length && print(`Record cleared`, delIdList)
}
return {
clearHistory,
setHttpHistoryWrap,
createHttpHistory,
createBodyPath,
getHistory,
getHistoryList,
ignoreHttpHistory,
}
}
function clientInjection({config}) { // 到客户端前的数据注入, 例如 添加测试 api, 统一处理数据格式
function setHeader(reqOrRes, headerObj = {}) {
reqOrRes.setHeader = reqOrRes.setHeader || reqOrRes.set || function (key, val) {reqOrRes.headers[key] = val}
Object.keys(headerObj).forEach(key => {
const val = headerObj[key]
val && reqOrRes.setHeader(key, val)