mock-service-cli 3.3.6 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -5
- package/bin/mock-service-cli +7 -1
- package/lib/api-overview.html +684 -0
- package/lib/mockServer.js +81 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
[](https://github.com/chandq/mock-service-cli/actions/workflows/node.js.yml)
|
|
2
2
|
[](https://coveralls.io/github/chandq/mock-service-cli?branch=master)
|
|
3
|
-
[](https://
|
|
3
|
+
[](https://www.npmjs.com/package/mock-service-cli)
|
|
4
4
|
[](https://nodejs.org/download/release/v12.0.0/)
|
|
5
5
|
[](https://github.com/chandq/mock-service-cli/blob/master/LICENSE.md)
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
<!-- [](https://npmcharts.com/compare/mock-service-cli?minimal=true) -->
|
|
7
8
|
|
|
8
9
|
# 🦅 一个基于 [node](https://nodejs.org/en/) 和 [express](https://www.expressjs.com.cn/) 的 轻量级 Mock 套件 命令行工具
|
|
9
10
|
|
|
10
11
|
### 简介
|
|
11
12
|
|
|
12
|
-
内置 **Static Server**、**Mock Server**、**SPA Server**、Http?s request proxy 等功能集。
|
|
13
|
+
内置 **Static Server**、**Mock Server**、**SPA Server**、Http?s request proxy、API overview page 等功能集。
|
|
13
14
|
|
|
14
15
|
**简易轻量**、**B/S 架构**、**0 秒启动**的本地命令行 Mock 服务套件, 支持热更新,对于开发调试 mock 数据很实用,能提高前端开发者的开发效率。支持 `GET`,`POST`,`PUT`,`DELETE`,`PATCH`,`OPTIONS`,`COPY`,`LINK`,`UNLINK`,`PURGE` 等常用请求类型,无需布署后端,可能是本地最好用的 Mock 工具之一。
|
|
15
16
|
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
|
|
18
19
|
- 支持统计 mock 的文件数量和 mock 请求数量
|
|
19
20
|
- 支持终端打开 mock 文件的所在位置
|
|
21
|
+
- 支持自动生成和打开现代化的 API 概览页面(按目录分类、可折叠展开、可点击测试 API)
|
|
20
22
|
|
|
21
23
|
#### Static Server (和 Mock Server 互斥使用)
|
|
22
24
|
|
|
@@ -94,6 +96,7 @@ This will install `mock-service-cli` globally so that it may be run from the com
|
|
|
94
96
|
| `-P` or `--web-port` | Web server port to use. Use `-P 9090` to look for an open port, starting at 9090. | 9090 |
|
|
95
97
|
| `-b` or `--web-baseurl` | Specify public path of the SPA web server, can be base address of SPA route. | - |
|
|
96
98
|
| `-R` or `--static-server` | Specify directory of the Static Web Server. | - |
|
|
99
|
+
| `-w` or `--open` | Open API overview page automatically. | false |
|
|
97
100
|
|
|
98
101
|
## Example
|
|
99
102
|
|
|
@@ -138,9 +141,9 @@ This will install `mock-service-cli` globally so that it may be run from the com
|
|
|
138
141
|
|
|
139
142
|
- js file
|
|
140
143
|
|
|
141
|
-
|
|
144
|
+
`mock-service-cli -D ./ -O ./proxy.js`
|
|
142
145
|
|
|
143
|
-
|
|
146
|
+
proxy.js file content as follows:
|
|
144
147
|
|
|
145
148
|
```js
|
|
146
149
|
module.exports = {
|
|
@@ -149,6 +152,14 @@ module.exports = {
|
|
|
149
152
|
};
|
|
150
153
|
```
|
|
151
154
|
|
|
155
|
+
10. Start Mock Server and automatically open API overview page.
|
|
156
|
+
|
|
157
|
+
`mock-service-cli --open`
|
|
158
|
+
|
|
159
|
+
11. Start Mock Server with custom port and automatically open API overview page.
|
|
160
|
+
|
|
161
|
+
`mock-service-cli -p 8080 --open`
|
|
162
|
+
|
|
152
163
|
### 编写 Mock 文件
|
|
153
164
|
|
|
154
165
|
> mock 请求的类型支持`GET`,`POST`,`PUT`,`DELETE`,`PATCH`,`OPTIONS`,`COPY`,`LINK`,`UNLINK`,`PURGE`
|
|
@@ -219,6 +230,42 @@ module.exports = {
|
|
|
219
230
|
|
|
220
231
|

|
|
221
232
|
|
|
233
|
+
### API 概览页面
|
|
234
|
+
|
|
235
|
+
API 概览页面是一个现代化的 Web 界面,用于展示和测试所有已注册的 Mock API 接口。
|
|
236
|
+
|
|
237
|
+
#### 功能特性
|
|
238
|
+
|
|
239
|
+
- 按目录分类展示 API 接口
|
|
240
|
+
- 目录可折叠/展开
|
|
241
|
+
- 支持按 URL 或 HTTP 方法搜索接口
|
|
242
|
+
- 每种 HTTP 方法使用不同颜色的标签(GET、POST、PUT、DELETE 等)
|
|
243
|
+
- 点击接口可查看详细信息
|
|
244
|
+
- 支持直接在页面中测试 API(发送请求并查看响应)
|
|
245
|
+
- 响应数据格式化展示
|
|
246
|
+
|
|
247
|
+
#### 访问方式
|
|
248
|
+
|
|
249
|
+
1. **自动打开**:使用 `--open` 参数启动服务时,会自动在浏览器中打开 API 概览页面
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
mock-service-cli --open
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
2. **手动访问**:在浏览器中访问以下地址
|
|
256
|
+
```
|
|
257
|
+
http://localhost:8090/__api-overview
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### 页面预览
|
|
261
|
+
|
|
262
|
+
API 概览页面采用现代化的响应式设计,包含以下部分:
|
|
263
|
+
|
|
264
|
+
- 统计卡片:展示总接口数、目录数、各 HTTP 方法数量
|
|
265
|
+
- 搜索框:快速查找接口
|
|
266
|
+
- 目录列表:按目录分组展示所有接口
|
|
267
|
+
- 测试面板:点击接口后可进行测试
|
|
268
|
+
|
|
222
269
|
### 借助 socket.io 保留接口数据
|
|
223
270
|
|
|
224
271
|
> `save-data` 和 `mock-dir-stat` 事件会自动生成 `mock-list.json` 文件(维护接口`<url, [method]>`的关系映射),方便查阅接口统计信息
|
package/bin/mock-service-cli
CHANGED
|
@@ -45,7 +45,8 @@ if (argv.h || argv.help) {
|
|
|
45
45
|
' -R --static-server Specify static resource file directory when enabled static server.',
|
|
46
46
|
'',
|
|
47
47
|
' -h --help Print this list and exit.',
|
|
48
|
-
' -v --version Print the version and exit.'
|
|
48
|
+
' -v --version Print the version and exit.',
|
|
49
|
+
' -w --open Open API overview page automatically, default false.'
|
|
49
50
|
].join('\n')
|
|
50
51
|
);
|
|
51
52
|
process.exit();
|
|
@@ -60,6 +61,7 @@ var port = argv.p || argv.port,
|
|
|
60
61
|
version = argv.v || argv.version,
|
|
61
62
|
isStartSocketServer = argv.S || false,
|
|
62
63
|
rewrite = argv.r || argv.rewrite || false,
|
|
64
|
+
isOpenApiOverview = argv.w || argv.open || false,
|
|
63
65
|
log = null;
|
|
64
66
|
|
|
65
67
|
if (!argv.s && !argv.silent) {
|
|
@@ -79,6 +81,10 @@ if (isStartSocketServer) {
|
|
|
79
81
|
process.env.SOCKET_SERVER = true;
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
if (isOpenApiOverview) {
|
|
85
|
+
process.env.OPEN_API_OVERVIEW = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
82
88
|
if (version) {
|
|
83
89
|
log.info('v' + require('../package.json').version);
|
|
84
90
|
process.exit();
|
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>API 概览 - Mock Service</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
16
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
17
|
+
min-height: 100vh;
|
|
18
|
+
padding: 20px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.container {
|
|
22
|
+
max-width: 1200px;
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.header {
|
|
27
|
+
background: white;
|
|
28
|
+
padding: 24px;
|
|
29
|
+
border-radius: 12px;
|
|
30
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
31
|
+
margin-bottom: 20px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.header h1 {
|
|
35
|
+
color: #333;
|
|
36
|
+
font-size: 28px;
|
|
37
|
+
margin-bottom: 8px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.header p {
|
|
41
|
+
color: #666;
|
|
42
|
+
font-size: 14px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.stat-cards {
|
|
46
|
+
display: grid;
|
|
47
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
48
|
+
gap: 16px;
|
|
49
|
+
margin-bottom: 20px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.stat-card {
|
|
53
|
+
background: white;
|
|
54
|
+
padding: 20px;
|
|
55
|
+
border-radius: 12px;
|
|
56
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.stat-card .number {
|
|
60
|
+
font-size: 36px;
|
|
61
|
+
font-weight: bold;
|
|
62
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
63
|
+
-webkit-background-clip: text;
|
|
64
|
+
-webkit-text-fill-color: transparent;
|
|
65
|
+
background-clip: text;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.stat-card .label {
|
|
69
|
+
color: #666;
|
|
70
|
+
font-size: 14px;
|
|
71
|
+
margin-top: 4px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.api-container {
|
|
75
|
+
background: white;
|
|
76
|
+
border-radius: 12px;
|
|
77
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.search-box {
|
|
82
|
+
padding: 20px;
|
|
83
|
+
border-bottom: 1px solid #eee;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.search-box input {
|
|
87
|
+
width: 100%;
|
|
88
|
+
padding: 12px 16px;
|
|
89
|
+
border: 2px solid #e0e0e0;
|
|
90
|
+
border-radius: 8px;
|
|
91
|
+
font-size: 14px;
|
|
92
|
+
transition: border-color 0.3s;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.search-box input:focus {
|
|
96
|
+
outline: none;
|
|
97
|
+
border-color: #667eea;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.directory {
|
|
101
|
+
border-bottom: 1px solid #eee;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.directory-header {
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
padding: 16px 20px;
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
background: #f8f9fa;
|
|
110
|
+
transition: background 0.3s;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.directory-header:hover {
|
|
114
|
+
background: #e9ecef;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.toggle-icon {
|
|
118
|
+
width: 20px;
|
|
119
|
+
height: 20px;
|
|
120
|
+
margin-right: 12px;
|
|
121
|
+
transition: transform 0.3s;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.toggle-icon.collapsed {
|
|
125
|
+
transform: rotate(-90deg);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.directory-name {
|
|
129
|
+
font-weight: 600;
|
|
130
|
+
color: #333;
|
|
131
|
+
flex: 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.api-count {
|
|
135
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
136
|
+
color: white;
|
|
137
|
+
padding: 4px 12px;
|
|
138
|
+
border-radius: 12px;
|
|
139
|
+
font-size: 12px;
|
|
140
|
+
font-weight: 600;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.directory-content {
|
|
144
|
+
max-height: 5000px;
|
|
145
|
+
overflow: hidden;
|
|
146
|
+
transition: max-height 0.3s ease-out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.directory-content.collapsed {
|
|
150
|
+
max-height: 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.api-item {
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
padding: 14px 20px 14px 52px;
|
|
157
|
+
border-bottom: 1px solid #f0f0f0;
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
transition: background 0.2s;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.api-item:hover {
|
|
163
|
+
background: #f8f9fa;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.api-item:last-child {
|
|
167
|
+
border-bottom: none;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.method-badge {
|
|
171
|
+
padding: 4px 10px;
|
|
172
|
+
border-radius: 6px;
|
|
173
|
+
font-size: 12px;
|
|
174
|
+
font-weight: 700;
|
|
175
|
+
text-transform: uppercase;
|
|
176
|
+
margin-right: 12px;
|
|
177
|
+
min-width: 70px;
|
|
178
|
+
text-align: center;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.method-get { background: #61affe; color: white; }
|
|
182
|
+
.method-post { background: #49cc90; color: white; }
|
|
183
|
+
.method-put { background: #fca130; color: white; }
|
|
184
|
+
.method-delete { background: #f93e3e; color: white; }
|
|
185
|
+
.method-patch { background: #50e3c2; color: white; }
|
|
186
|
+
.method-head { background: #9012fe; color: white; }
|
|
187
|
+
.method-options { background: #0d5aa7; color: white; }
|
|
188
|
+
|
|
189
|
+
.api-url {
|
|
190
|
+
flex: 1;
|
|
191
|
+
color: #333;
|
|
192
|
+
font-family: 'Courier New', monospace;
|
|
193
|
+
font-size: 14px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.test-btn {
|
|
197
|
+
padding: 6px 16px;
|
|
198
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
199
|
+
color: white;
|
|
200
|
+
border: none;
|
|
201
|
+
border-radius: 6px;
|
|
202
|
+
font-size: 13px;
|
|
203
|
+
font-weight: 600;
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.test-btn:hover {
|
|
209
|
+
transform: translateY(-1px);
|
|
210
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.modal {
|
|
214
|
+
display: none;
|
|
215
|
+
position: fixed;
|
|
216
|
+
top: 0;
|
|
217
|
+
left: 0;
|
|
218
|
+
right: 0;
|
|
219
|
+
bottom: 0;
|
|
220
|
+
background: rgba(0, 0, 0, 0.5);
|
|
221
|
+
z-index: 1000;
|
|
222
|
+
padding: 20px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.modal.active {
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
justify-content: center;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.modal-content {
|
|
232
|
+
background: white;
|
|
233
|
+
border-radius: 12px;
|
|
234
|
+
max-width: 800px;
|
|
235
|
+
width: 100%;
|
|
236
|
+
max-height: 90vh;
|
|
237
|
+
overflow-y: auto;
|
|
238
|
+
box-shadow: 0 20px 25px rgba(0, 0, 0, 0.1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.modal-header {
|
|
242
|
+
display: flex;
|
|
243
|
+
justify-content: space-between;
|
|
244
|
+
align-items: center;
|
|
245
|
+
padding: 20px;
|
|
246
|
+
border-bottom: 1px solid #eee;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.modal-title {
|
|
250
|
+
font-size: 20px;
|
|
251
|
+
font-weight: 600;
|
|
252
|
+
color: #333;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.close-btn {
|
|
256
|
+
width: 32px;
|
|
257
|
+
height: 32px;
|
|
258
|
+
border: none;
|
|
259
|
+
background: #f0f0f0;
|
|
260
|
+
border-radius: 6px;
|
|
261
|
+
cursor: pointer;
|
|
262
|
+
font-size: 18px;
|
|
263
|
+
color: #666;
|
|
264
|
+
display: flex;
|
|
265
|
+
align-items: center;
|
|
266
|
+
justify-content: center;
|
|
267
|
+
transition: background 0.2s;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.close-btn:hover {
|
|
271
|
+
background: #e0e0e0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.modal-body {
|
|
275
|
+
padding: 20px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.api-info-row {
|
|
279
|
+
display: flex;
|
|
280
|
+
margin-bottom: 16px;
|
|
281
|
+
align-items: center;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.api-info-label {
|
|
285
|
+
width: 100px;
|
|
286
|
+
font-weight: 600;
|
|
287
|
+
color: #666;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.api-info-value {
|
|
291
|
+
flex: 1;
|
|
292
|
+
color: #333;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.request-section {
|
|
296
|
+
margin-top: 24px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.section-title {
|
|
300
|
+
font-size: 16px;
|
|
301
|
+
font-weight: 600;
|
|
302
|
+
color: #333;
|
|
303
|
+
margin-bottom: 12px;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.request-body-input {
|
|
307
|
+
width: 100%;
|
|
308
|
+
min-height: 120px;
|
|
309
|
+
padding: 12px;
|
|
310
|
+
border: 2px solid #e0e0e0;
|
|
311
|
+
border-radius: 8px;
|
|
312
|
+
font-family: 'Courier New', monospace;
|
|
313
|
+
font-size: 13px;
|
|
314
|
+
resize: vertical;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.request-body-input:focus {
|
|
318
|
+
outline: none;
|
|
319
|
+
border-color: #667eea;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.send-btn {
|
|
323
|
+
margin-top: 16px;
|
|
324
|
+
padding: 12px 24px;
|
|
325
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
326
|
+
color: white;
|
|
327
|
+
border: none;
|
|
328
|
+
border-radius: 8px;
|
|
329
|
+
font-size: 14px;
|
|
330
|
+
font-weight: 600;
|
|
331
|
+
cursor: pointer;
|
|
332
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.send-btn:hover {
|
|
336
|
+
transform: translateY(-2px);
|
|
337
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.send-btn:disabled {
|
|
341
|
+
opacity: 0.6;
|
|
342
|
+
cursor: not-allowed;
|
|
343
|
+
transform: none;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.response-section {
|
|
347
|
+
margin-top: 24px;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.response-status {
|
|
351
|
+
display: inline-flex;
|
|
352
|
+
align-items: center;
|
|
353
|
+
padding: 6px 12px;
|
|
354
|
+
border-radius: 6px;
|
|
355
|
+
font-weight: 600;
|
|
356
|
+
margin-bottom: 12px;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.response-status.success {
|
|
360
|
+
background: #d4edda;
|
|
361
|
+
color: #155724;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.response-status.error {
|
|
365
|
+
background: #f8d7da;
|
|
366
|
+
color: #721c24;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.response-output {
|
|
370
|
+
width: 100%;
|
|
371
|
+
min-height: 150px;
|
|
372
|
+
padding: 12px;
|
|
373
|
+
background: #f8f9fa;
|
|
374
|
+
border: 1px solid #e0e0e0;
|
|
375
|
+
border-radius: 8px;
|
|
376
|
+
font-family: 'Courier New', monospace;
|
|
377
|
+
font-size: 13px;
|
|
378
|
+
white-space: pre-wrap;
|
|
379
|
+
word-break: break-all;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.loading {
|
|
383
|
+
display: inline-block;
|
|
384
|
+
width: 16px;
|
|
385
|
+
height: 16px;
|
|
386
|
+
border: 2px solid #ffffff;
|
|
387
|
+
border-radius: 50%;
|
|
388
|
+
border-top-color: transparent;
|
|
389
|
+
animation: spin 0.8s linear infinite;
|
|
390
|
+
margin-right: 8px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
@keyframes spin {
|
|
394
|
+
to { transform: rotate(360deg); }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.empty-state {
|
|
398
|
+
text-align: center;
|
|
399
|
+
padding: 60px 20px;
|
|
400
|
+
color: #999;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.empty-state svg {
|
|
404
|
+
width: 100px;
|
|
405
|
+
height: 100px;
|
|
406
|
+
margin-bottom: 20px;
|
|
407
|
+
opacity: 0.5;
|
|
408
|
+
}
|
|
409
|
+
</style>
|
|
410
|
+
</head>
|
|
411
|
+
<body>
|
|
412
|
+
<div class="container">
|
|
413
|
+
<div class="header">
|
|
414
|
+
<h1>🎯 API 概览</h1>
|
|
415
|
+
<p>探索和测试您的 Mock API 接口</p>
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
<div class="stat-cards" id="statCards">
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
<div class="api-container">
|
|
422
|
+
<div class="search-box">
|
|
423
|
+
<input type="text" id="searchInput" placeholder="搜索 API 接口 (输入 URL 或 HTTP 方法)...">
|
|
424
|
+
</div>
|
|
425
|
+
<div id="apiList">
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
<div class="modal" id="apiModal">
|
|
431
|
+
<div class="modal-content">
|
|
432
|
+
<div class="modal-header">
|
|
433
|
+
<div class="modal-title" id="modalTitle">API 测试</div>
|
|
434
|
+
<button class="close-btn" onclick="closeModal()">×</button>
|
|
435
|
+
</div>
|
|
436
|
+
<div class="modal-body" id="modalBody">
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<script>
|
|
442
|
+
let apiData = null;
|
|
443
|
+
let currentApi = null;
|
|
444
|
+
|
|
445
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
446
|
+
fetchApiData();
|
|
447
|
+
document.getElementById('searchInput').addEventListener('input', filterApis);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
async function fetchApiData() {
|
|
451
|
+
try {
|
|
452
|
+
const response = await fetch('/__api-data');
|
|
453
|
+
apiData = await response.json();
|
|
454
|
+
renderStatCards();
|
|
455
|
+
renderApiList();
|
|
456
|
+
} catch (error) {
|
|
457
|
+
console.error('Failed to fetch API data:', error);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function renderStatCards() {
|
|
462
|
+
const methodCounts = {};
|
|
463
|
+
apiData.apis.forEach(api => {
|
|
464
|
+
methodCounts[api.method] = (methodCounts[api.method] || 0) + 1;
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const container = document.getElementById('statCards');
|
|
468
|
+
let html = `
|
|
469
|
+
<div class="stat-card">
|
|
470
|
+
<div class="number">${apiData.apis.length}</div>
|
|
471
|
+
<div class="label">总接口数</div>
|
|
472
|
+
</div>
|
|
473
|
+
<div class="stat-card">
|
|
474
|
+
<div class="number">${Object.keys(apiData.groupedApis).length}</div>
|
|
475
|
+
<div class="label">目录数</div>
|
|
476
|
+
</div>
|
|
477
|
+
`;
|
|
478
|
+
|
|
479
|
+
Object.entries(methodCounts).forEach(([method, count]) => {
|
|
480
|
+
html += `
|
|
481
|
+
<div class="stat-card">
|
|
482
|
+
<div class="number">${count}</div>
|
|
483
|
+
<div class="label">${method.toUpperCase()}</div>
|
|
484
|
+
</div>
|
|
485
|
+
`;
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
container.innerHTML = html;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function renderApiList(filterText = '') {
|
|
492
|
+
const container = document.getElementById('apiList');
|
|
493
|
+
const searchLower = filterText.toLowerCase();
|
|
494
|
+
|
|
495
|
+
let html = '';
|
|
496
|
+
let hasResults = false;
|
|
497
|
+
|
|
498
|
+
Object.entries(apiData.groupedApis).forEach(([dir, apis]) => {
|
|
499
|
+
const filteredApis = apis.filter(api =>
|
|
500
|
+
api.url.toLowerCase().includes(searchLower) ||
|
|
501
|
+
api.method.toLowerCase().includes(searchLower)
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
if (filteredApis.length === 0) return;
|
|
505
|
+
hasResults = true;
|
|
506
|
+
|
|
507
|
+
const dirName = dir.replace(/\\/g, '/');
|
|
508
|
+
html += `
|
|
509
|
+
<div class="directory">
|
|
510
|
+
<div class="directory-header" onclick="toggleDirectory(this)">
|
|
511
|
+
<svg class="toggle-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
512
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
513
|
+
</svg>
|
|
514
|
+
<span class="directory-name">📁 ${dirName}</span>
|
|
515
|
+
<span class="api-count">${filteredApis.length}</span>
|
|
516
|
+
</div>
|
|
517
|
+
<div class="directory-content">
|
|
518
|
+
${filteredApis.map(api => renderApiItem(api)).join('')}
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
`;
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
if (!hasResults) {
|
|
525
|
+
html = `
|
|
526
|
+
<div class="empty-state">
|
|
527
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
528
|
+
<circle cx="11" cy="11" r="8"></circle>
|
|
529
|
+
<path d="m21 21-4.35-4.35"></path>
|
|
530
|
+
</svg>
|
|
531
|
+
<h3>没有找到匹配的接口</h3>
|
|
532
|
+
<p>尝试使用其他关键词搜索</p>
|
|
533
|
+
</div>
|
|
534
|
+
`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
container.innerHTML = html;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function renderApiItem(api) {
|
|
541
|
+
return `
|
|
542
|
+
<div class="api-item" onclick="openApiModal('${api.method}', '${api.url.replace(/'/g, "\\'")}', '${api.file.replace(/'/g, "\\'")}')">
|
|
543
|
+
<span class="method-badge method-${api.method.toLowerCase()}">${api.method}</span>
|
|
544
|
+
<span class="api-url">${api.url}</span>
|
|
545
|
+
<button class="test-btn" onclick="event.stopPropagation(); testApi('${api.method}', '${api.url.replace(/'/g, "\\'")}')">测试</button>
|
|
546
|
+
</div>
|
|
547
|
+
`;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function toggleDirectory(header) {
|
|
551
|
+
const icon = header.querySelector('.toggle-icon');
|
|
552
|
+
const content = header.nextElementSibling;
|
|
553
|
+
icon.classList.toggle('collapsed');
|
|
554
|
+
content.classList.toggle('collapsed');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function filterApis(e) {
|
|
558
|
+
renderApiList(e.target.value);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function openApiModal(method, url, file) {
|
|
562
|
+
currentApi = { method, url, file };
|
|
563
|
+
const modal = document.getElementById('apiModal');
|
|
564
|
+
const title = document.getElementById('modalTitle');
|
|
565
|
+
const body = document.getElementById('modalBody');
|
|
566
|
+
|
|
567
|
+
title.innerHTML = `<span class="method-badge method-${method.toLowerCase()}">${method}</span> ${url}`;
|
|
568
|
+
|
|
569
|
+
body.innerHTML = `
|
|
570
|
+
<div class="api-info-row">
|
|
571
|
+
<span class="api-info-label">文件路径:</span>
|
|
572
|
+
<span class="api-info-value">${file}</span>
|
|
573
|
+
</div>
|
|
574
|
+
${(method === 'POST' || method === 'PUT' || method === 'PATCH') ? `
|
|
575
|
+
<div class="request-section">
|
|
576
|
+
<div class="section-title">请求体 (JSON)</div>
|
|
577
|
+
<textarea class="request-body-input" id="requestBody" placeholder='{"key": "value"}'></textarea>
|
|
578
|
+
</div>
|
|
579
|
+
` : ''}
|
|
580
|
+
<button class="send-btn" id="sendBtn" onclick="sendRequest()">发送请求</button>
|
|
581
|
+
<div class="response-section" id="responseSection" style="display: none;">
|
|
582
|
+
<div class="section-title">响应</div>
|
|
583
|
+
<div class="response-status" id="responseStatus"></div>
|
|
584
|
+
<pre class="response-output" id="responseOutput"></pre>
|
|
585
|
+
</div>
|
|
586
|
+
`;
|
|
587
|
+
|
|
588
|
+
modal.classList.add('active');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function closeModal() {
|
|
592
|
+
document.getElementById('apiModal').classList.remove('active');
|
|
593
|
+
currentApi = null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function testApi(method, url) {
|
|
597
|
+
currentApi = { method, url };
|
|
598
|
+
await sendRequestDirect(method, url);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function sendRequest() {
|
|
602
|
+
if (!currentApi) return;
|
|
603
|
+
await sendRequestDirect(currentApi.method, currentApi.url);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function sendRequestDirect(method, url) {
|
|
607
|
+
const sendBtn = document.getElementById('sendBtn');
|
|
608
|
+
const responseSection = document.getElementById('responseSection');
|
|
609
|
+
const responseStatus = document.getElementById('responseStatus');
|
|
610
|
+
const responseOutput = document.getElementById('responseOutput');
|
|
611
|
+
|
|
612
|
+
let requestBody = null;
|
|
613
|
+
const requestBodyEl = document.getElementById('requestBody');
|
|
614
|
+
if (requestBodyEl) {
|
|
615
|
+
const bodyText = requestBodyEl.value.trim();
|
|
616
|
+
if (bodyText) {
|
|
617
|
+
try {
|
|
618
|
+
requestBody = JSON.parse(bodyText);
|
|
619
|
+
} catch (e) {
|
|
620
|
+
alert('请求体 JSON 格式错误');
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (sendBtn) {
|
|
627
|
+
sendBtn.disabled = true;
|
|
628
|
+
sendBtn.innerHTML = '<span class="loading"></span>发送中...';
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
const options = {
|
|
633
|
+
method: method,
|
|
634
|
+
headers: {
|
|
635
|
+
'Content-Type': 'application/json'
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
if (requestBody) {
|
|
640
|
+
options.body = JSON.stringify(requestBody);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const response = await fetch(url, options);
|
|
644
|
+
const data = await response.json();
|
|
645
|
+
|
|
646
|
+
if (responseSection) responseSection.style.display = 'block';
|
|
647
|
+
if (responseStatus) {
|
|
648
|
+
responseStatus.className = 'response-status success';
|
|
649
|
+
responseStatus.textContent = `✓ ${response.status} ${response.statusText}`;
|
|
650
|
+
}
|
|
651
|
+
if (responseOutput) {
|
|
652
|
+
responseOutput.textContent = JSON.stringify(data, null, 2);
|
|
653
|
+
}
|
|
654
|
+
} catch (error) {
|
|
655
|
+
if (responseSection) responseSection.style.display = 'block';
|
|
656
|
+
if (responseStatus) {
|
|
657
|
+
responseStatus.className = 'response-status error';
|
|
658
|
+
responseStatus.textContent = '✗ 请求失败';
|
|
659
|
+
}
|
|
660
|
+
if (responseOutput) {
|
|
661
|
+
responseOutput.textContent = error.message;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (sendBtn) {
|
|
666
|
+
sendBtn.disabled = false;
|
|
667
|
+
sendBtn.innerHTML = '发送请求';
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
document.getElementById('apiModal').addEventListener('click', (e) => {
|
|
672
|
+
if (e.target === e.currentTarget) {
|
|
673
|
+
closeModal();
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
document.addEventListener('keydown', (e) => {
|
|
678
|
+
if (e.key === 'Escape') {
|
|
679
|
+
closeModal();
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
</script>
|
|
683
|
+
</body>
|
|
684
|
+
</html>
|
package/lib/mockServer.js
CHANGED
|
@@ -4,7 +4,8 @@ const express = require('express'), // 引入express
|
|
|
4
4
|
os = require('os'),
|
|
5
5
|
path = require('path'),
|
|
6
6
|
colors = require('colors/safe'),
|
|
7
|
-
portfinder = require('portfinder')
|
|
7
|
+
portfinder = require('portfinder'),
|
|
8
|
+
{ exec } = require('child_process');
|
|
8
9
|
const { createProxyMiddleware } = require('http-proxy-middleware');
|
|
9
10
|
const ifaces = os.networkInterfaces();
|
|
10
11
|
const { Server } = require('socket.io');
|
|
@@ -19,6 +20,8 @@ const {
|
|
|
19
20
|
} = require('./utils');
|
|
20
21
|
const { genMockFiles, getMockStatFromDir, getMockStatFromFile } = require('./manageMockFiles');
|
|
21
22
|
|
|
23
|
+
const registeredApis = [];
|
|
24
|
+
|
|
22
25
|
const methodRegExp = new RegExp(`((${SupportMethods.join('|')}) +)?([^?]*)`, 'i');
|
|
23
26
|
|
|
24
27
|
const app = express();
|
|
@@ -72,6 +75,36 @@ function init() {
|
|
|
72
75
|
app.use(crossDomain()); // 允许跨域
|
|
73
76
|
app.use(bodyParser.json()); // 解析body
|
|
74
77
|
|
|
78
|
+
// API 概览页面路由
|
|
79
|
+
app.get('/__api-overview', (req, res) => {
|
|
80
|
+
const htmlPath = path.resolve(__dirname, './api-overview.html');
|
|
81
|
+
if (existsSync(htmlPath)) {
|
|
82
|
+
res.sendFile(htmlPath);
|
|
83
|
+
} else {
|
|
84
|
+
res.status(404).send('API overview page not found');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// API 数据路由
|
|
89
|
+
app.get('/__api-data', (req, res) => {
|
|
90
|
+
// 按目录分组 API
|
|
91
|
+
const groupedApis = {};
|
|
92
|
+
registeredApis.forEach(api => {
|
|
93
|
+
// 从文件路径中提取相对路径作为目录
|
|
94
|
+
const dirPath = path.dirname(api.file);
|
|
95
|
+
if (!groupedApis[dirPath]) {
|
|
96
|
+
groupedApis[dirPath] = [];
|
|
97
|
+
}
|
|
98
|
+
groupedApis[dirPath].push(api);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
res.json({
|
|
102
|
+
apis: registeredApis,
|
|
103
|
+
groupedApis: groupedApis,
|
|
104
|
+
port: process.env.PORT
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
75
108
|
let requireMockServe = true;
|
|
76
109
|
if (process.env.SPECIFIED_FILE) {
|
|
77
110
|
composeRouteFromJsFile(process.env.SPECIFIED_FILE);
|
|
@@ -158,6 +191,13 @@ function composeRouteFromJsFile(file) {
|
|
|
158
191
|
res.json(fileObject[item]);
|
|
159
192
|
});
|
|
160
193
|
}
|
|
194
|
+
// 收集 API 信息
|
|
195
|
+
registeredApis.push({
|
|
196
|
+
method: reqMethod.toUpperCase(),
|
|
197
|
+
url: reqUrl,
|
|
198
|
+
file: file,
|
|
199
|
+
data: fileObject[item]
|
|
200
|
+
});
|
|
161
201
|
});
|
|
162
202
|
}
|
|
163
203
|
/**
|
|
@@ -199,6 +239,13 @@ function parseMockFiles(specialDir = '../mock') {
|
|
|
199
239
|
app[method](apiUrl, function (req, res) {
|
|
200
240
|
res.json(fileObject[method]);
|
|
201
241
|
});
|
|
242
|
+
// 收集 API 信息
|
|
243
|
+
registeredApis.push({
|
|
244
|
+
method: method.toUpperCase(),
|
|
245
|
+
url: apiUrl,
|
|
246
|
+
file: filePath,
|
|
247
|
+
data: fileObject[method]
|
|
248
|
+
});
|
|
202
249
|
});
|
|
203
250
|
continue;
|
|
204
251
|
}
|
|
@@ -361,6 +408,13 @@ function startServer() {
|
|
|
361
408
|
);
|
|
362
409
|
console.info(colors.yellow(`\n Mock server available on:\n`));
|
|
363
410
|
console.info(' http://localhost:' + colors.green(process.env.PORT));
|
|
411
|
+
console.info(
|
|
412
|
+
' ' +
|
|
413
|
+
colors.blue('API Overview: ') +
|
|
414
|
+
'http://localhost:' +
|
|
415
|
+
colors.green(process.env.PORT) +
|
|
416
|
+
colors.green('/__api-overview')
|
|
417
|
+
);
|
|
364
418
|
Object.keys(ifaces).forEach(function (dev) {
|
|
365
419
|
ifaces[dev].forEach(function (details) {
|
|
366
420
|
if (details.family === 'IPv4') {
|
|
@@ -369,6 +423,32 @@ function startServer() {
|
|
|
369
423
|
});
|
|
370
424
|
});
|
|
371
425
|
}
|
|
426
|
+
|
|
427
|
+
// 自动打开浏览器
|
|
428
|
+
if (process.env.OPEN_API_OVERVIEW && !process.env.RESTARTED) {
|
|
429
|
+
const apiOverviewUrl = `http://localhost:${process.env.PORT}/__api-overview`;
|
|
430
|
+
console.info(colors.yellow(`\nOpening API overview page...`));
|
|
431
|
+
let openCommand;
|
|
432
|
+
switch (process.platform) {
|
|
433
|
+
case 'darwin':
|
|
434
|
+
openCommand = `open ${apiOverviewUrl}`;
|
|
435
|
+
break;
|
|
436
|
+
case 'win32':
|
|
437
|
+
openCommand = `start ${apiOverviewUrl}`;
|
|
438
|
+
break;
|
|
439
|
+
case 'linux':
|
|
440
|
+
openCommand = `xdg-open ${apiOverviewUrl}`;
|
|
441
|
+
break;
|
|
442
|
+
default:
|
|
443
|
+
console.warn(colors.yellow(`Could not automatically open browser. Please visit: ${apiOverviewUrl}`));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
exec(openCommand, error => {
|
|
447
|
+
if (error) {
|
|
448
|
+
console.warn(colors.yellow(`Could not automatically open browser. Please visit: ${apiOverviewUrl}`));
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
372
452
|
});
|
|
373
453
|
}
|
|
374
454
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mock-service-cli",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "🦅 Local Mock/Static/SPA server, Http?s request proxy",
|
|
3
|
+
"version": "3.4.0",
|
|
4
|
+
"description": "🦅 Local Mock/Static/SPA server, Http?s request proxy, API overview page",
|
|
5
5
|
"main": "./lib/mockServer.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mock-service-cli": "bin/mock-service-cli"
|