Skip to content

Commit 8e9d035

Browse files
committed
feat: result action and swagger
1 parent a77da9a commit 8e9d035

19 files changed

+883
-234
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
node_modules
1+
node_modules
2+
mock
3+
mock.config.json

README.md

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
## dev-mock-cli
2+
支持能力/价值:
3+
- Mock API
4+
- 读取Swagger API,并生成接口
5+
- 无需担心跨域问题
6+
- 安装和启动方便
7+
8+
待办:
9+
- [待确认]支持版本:node>=14
10+
- 发布启动的配置信息汇总,提示产品使用文档:CLI使用说明,配置说明mock.config.json
11+
- 同时只能支持一个风格,当风格切换时,检测到有mock文件夹,则提示清空
12+
- TODO: restful: 读取swagger,生成json,处理中,晚上CLI的文档,用来测试
13+
- TODO: 读取配置文件,全局使用处理
14+
- TODO: .js默认加载
15+
- TODO: mock能力细节处理
16+
- 安装则生成mock.config.json
17+
- 生成单元测试
18+
- 录制演示视频
19+
- 优化打印日志,全部英文显示
220

321
### 二、启动一个Mock-API服务
422

523
#### action 风格的api
624

25+
726
在mock文件新建`[Action].json`文件,Action为对应api的名字,如果请求地址路径有参数,可以创建多层
827

928
比如一个请求url: `http://localhost:9000/list`, Action: "List"的api,数据为
@@ -66,33 +85,13 @@ npm run publish:patch
6685
2、撤回 24 小时内发布的版本,撤回后 24 小时内不允许发布
6786

6887
```
69-
npm unpublish u-admin-cli@1.0.2
88+
npm unpublish dev-mock-cli@1.0.2
7089
```
7190

7291
### 五、本地开发
7392

7493
```
75-
cd u-admin-cli
94+
cd dev-mock-cli
7695
yarn install
7796
yarn start [command]
78-
```
79-
80-
81-
## 待办
82-
83-
- 安装在项目中
84-
- 自动安装和启动,mock api
85-
- 支持restful风格API
86-
- 本地直接打开项目使用文档
87-
88-
89-
- 配置html页面
90-
- 添加默认配置
91-
- 读取项目中的参数
92-
- 优化日志
93-
- 通过swagger API生成mock API
94-
- 需要增加错误判断,避免程序报错退出
95-
- 单元测试
96-
- 使用文档
97-
- 若端口被占用,重新启动一个端口
98-
- 录制视频
97+
```

bin/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import { commandOptions } from './config.js';
66

77
const yargsCommand = yargs(hideBin(process.argv));
88

9+
// 检查是否提供了命令
10+
if (process.argv.length < 3) {
11+
console.error("请提供一个命令,例如: 'dev-mock-cli help'");
12+
process.exit(1); // 非正常退出
13+
}
14+
915
commandOptions.forEach(commandConfig => {
1016
const { command, descriptions, options, callback } = commandConfig;
1117
yargsCommand.command(

command/action.js

Lines changed: 47 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,54 @@
11
import fs from 'fs';
2-
import { NotFoundResponse } from './response.js';
2+
import { createProxyMiddleware } from 'http-proxy-middleware';
3+
import { ResponseExample } from './response.js';
4+
import { getAllAction } from '../utils/index.js';
5+
import { getConfig } from '../utils/config.js';
6+
import { generateApi, createAPIFile, fetchAndCreateRoutes } from './localAction.js';
37

4-
const checkFileExist = filePath => {
5-
// 检查当前目录是否存在
6-
if (!fs.existsSync(filePath) && type === 'action') {
7-
if (create) {
8-
fs.mkdir(`mock`, function (err) {
9-
if (err) {
10-
return console.error(err.message);
11-
}
12-
fs.mkdirSync(`./mock/api`);
13-
const data = JSON.stringify(ResponseExample, '', '\t');
14-
fs.writeFileSync('./mock/api/list.json', data);
15-
});
16-
console.log(
17-
chalk.green(
18-
`curl --location --request POST 'http://localhost:9000/api' \ --header 'Content-Type: application/json' \ --data-raw '{ "Action": "list" }'`
19-
)
20-
);
21-
} else {
22-
console.log(
23-
'The current directory mock folder does not exist, you can create it use : ' + chalk.red('u-admin-cli mock -n')
24-
);
25-
}
26-
}
27-
};
28-
29-
const action = ({ app, filePath }) => {
30-
// 如果在mock下,判断下key的值
31-
app.post('*', async (req, res) => {
32-
const key = req.params[0].substring(1);
33-
const { Action } = req.body;
34-
if (key) {
35-
const fileList = [];
36-
try {
37-
fs.readdirSync(filePath).forEach(fileName => {
38-
fileList.push(fileName);
39-
});
40-
} catch (err) {
41-
res.send(NotFoundResponse);
42-
}
438

44-
if (!fileList.includes(key) || !Action) {
45-
res.send(NotFoundResponse);
46-
}
47-
}
9+
const action = async ({ app, filePath }) => {
10+
// TODO: 配置信息需要统一处理,作为全局使用,type的每个优先级需要确认和逻辑开发
11+
const { proxyApiUrl, swaggerApiJSON=[] } = await getConfig();
12+
13+
// 如果 mock 文件夹不存在,自动创建它
14+
if (!fs.existsSync(filePath)) {
15+
createAPIFile(ResponseExample, filePath, 'ActionName.json');
16+
}
4817

49-
const file = `${filePath}${key}/${Action}.json`; //文件路径,__dirname为当前运行js文件的目录
18+
// 获取所有 action 名称
19+
const allActions = getAllAction(filePath);
20+
if (allActions.length === 0) {
21+
console.warn('No actions found in the mock directory.');
22+
return;
23+
}
24+
25+
// Step 1: 中间件来拦截请求并动态修改 URL
26+
// app.use('/', (req, res, next) => {
27+
// const { Action } = req.body;
28+
// if (!Action) {
29+
// // 如果请求体中没有 Action,返回 400 错误
30+
// return res.status(400).send({ error: 'Action field is required' });
31+
// }
32+
// // 动态修改请求的 URL,将它转发到 /{Action} 路径
33+
// req.url = `/${Action}`;
34+
// next(); // 继续处理下一个中间件
35+
// });
5036

51-
fs.readFile(file, 'utf-8', function (err, data) {
52-
if (err) {
53-
res.send(NotFoundResponse);
54-
} else {
55-
res.send(data);
56-
}
57-
});
58-
});
37+
// Step2: 为每个 action 注册对应的路由
38+
generateApi(app, filePath, allActions);
39+
40+
// Step3: 获取swagger api json的数据,注册接口
41+
// 调用 fetchAndCreateRoutes 函数
42+
await fetchAndCreateRoutes({app, swaggerApiJSON});
43+
44+
// Step4: 获取代理配置并设置代理
45+
if (proxyApiUrl) {
46+
app.use('*', createProxyMiddleware({
47+
target: proxyApiUrl,
48+
changeOrigin: true,
49+
})
50+
);
51+
}
5952
};
6053

61-
export default action;
54+
export default action;

command/localAction.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import path from 'path';
2+
import fs from 'fs';
3+
import Mock from 'mockjs';
4+
import axios from 'axios';
5+
import { createSwaggerMockData } from './swaggerAction.js'
6+
export const createAPIFile = (dataJson, folderPath, fileName) => {
7+
const apiFolderPath = path.resolve(folderPath); // 定义 API 目录的绝对路径
8+
const filePath = path.resolve(apiFolderPath, fileName); // 定义 get.json 文件的绝对路径
9+
10+
// 递归创建目录,确保 mock/api 文件夹存在
11+
if (!fs.existsSync(apiFolderPath)) {
12+
fs.mkdirSync(apiFolderPath, { recursive: true });
13+
}
14+
15+
const data = JSON.stringify(dataJson || ResponseExample, null, 2);
16+
fs.writeFileSync(filePath, data);
17+
console.log(`File created at: ${filePath}`);
18+
};
19+
20+
export const generateApi = (app, filePath, actions) => {
21+
actions.forEach(action => {
22+
app.use(`/${action}`, async (req, res) => {
23+
const { Action } = req.body;
24+
const actionFile = path.join(filePath, `${Action}.json`);
25+
if (!fs.existsSync(actionFile)) {
26+
return res.status(404).send(NotFoundResponse);
27+
}
28+
29+
fs.readFile(actionFile, 'utf-8', (err, data) => {
30+
if (err) {
31+
console.error(`Error reading file: ${actionFile}`, err);
32+
return res.status(404).send(NotFoundResponse);
33+
}
34+
try {
35+
const mockData = Mock.mock(JSON.parse(data));
36+
res.send(mockData);
37+
} catch (parseError) {
38+
console.error(`Error parsing JSON from file: ${actionFile}`, parseError);
39+
return res.status(500).send({ error: 'Invalid JSON format' });
40+
}
41+
});
42+
});
43+
});
44+
}
45+
46+
// swagger生成路由
47+
export const createRoutes = ({ app, data }) => {
48+
const paths = data.paths || {}
49+
if(!paths){
50+
return
51+
}
52+
const keys = Object.keys(paths);
53+
const limit = Math.min(keys.length, 100); // 只生成前100个路由
54+
55+
for (let i = 0; i < limit; i++) {
56+
const pathKey = keys[i];
57+
const pathInfo = paths[pathKey];
58+
const lastSegment = `/${pathKey.split('/').pop()}`;
59+
60+
Object.keys(pathInfo).forEach(method => {
61+
app[method](lastSegment, async (req, res) => {
62+
const { Action } = req.body;
63+
if (Action) {
64+
// TODO: 这里指读取了$ref字段,如果没有关联需要处理下
65+
const responseSchema = pathInfo[method].responses['200'].schema['$ref'].split('/').pop();
66+
const response = data.definitions[responseSchema]
67+
const mockResponse = await createSwaggerMockData(data, response)
68+
return res.status(200).json(mockResponse);
69+
} else {
70+
return res.status(404).send(NotFoundResponse);
71+
}
72+
});
73+
});
74+
}
75+
};
76+
77+
// Step3: 获取swagger api json的数据,注册接口
78+
export const fetchAndCreateRoutes = async ({ app, swaggerApiJSON }) => {
79+
const routePromises = swaggerApiJSON.map(async (item) => {
80+
const { type, url } = item;
81+
if (type === 'action') {
82+
try {
83+
// TODO: 优化, 全局加载,方便后面读取直接使用
84+
const response = await axios.get(url);
85+
const data = response.data;
86+
createRoutes({ app, data });
87+
} catch (error) {
88+
console.error('Error fetching data from URL:', url, error);
89+
}
90+
}
91+
});
92+
93+
// 等待所有的请求完成
94+
await Promise.all(routePromises);
95+
};

command/localRestful.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import fs from 'fs';
2+
import chalk from 'chalk';
3+
import path from 'path';
4+
import Mock from 'mockjs';
5+
import { mockRestfulAPI, ResponseExample, NotFoundResponse } from './response.js'
6+
7+
// 确保目录存在,并创建API文件
8+
const createAPIFile = (dataJson, folderPath, fileName) => {
9+
const apiFolderPath = path.resolve(folderPath); // 定义 API 目录的绝对路径
10+
const filePath = path.resolve(apiFolderPath, fileName); // 定义 get.json 文件的绝对路径
11+
// 递归创建目录,确保 mock/api 文件夹存在
12+
if (!fs.existsSync(apiFolderPath)) {
13+
fs.mkdirSync(apiFolderPath, { recursive: true });
14+
}
15+
16+
const data = JSON.stringify(dataJson || ResponseExample, null, '\t');
17+
fs.writeFileSync(filePath, data);
18+
console.log(`File created at: ${filePath}`);
19+
};
20+
21+
// 创建多个 API 文件
22+
export const createMultipleAPIFiles = (apiArray, basePath) => {
23+
apiArray.forEach(item => {
24+
const filePath = path.join(basePath, item.url);
25+
createAPIFile(item.content, filePath, item.fileName);
26+
});
27+
};
28+
29+
// 检查文件夹是否存在,如果不存在就自动创建
30+
export const checkFileExist = (filePath, autoCreate = false) => {
31+
const mkdirExist = fs.existsSync(filePath);
32+
if (mkdirExist) {
33+
return true;
34+
}
35+
if (!fs.existsSync(filePath) && !autoCreate) {
36+
console.log(
37+
'The current directory mock folder does not exist, you can create it use : ' + chalk.red('u-admin-cli mock -n')
38+
);
39+
return false;
40+
} else {
41+
fs.mkdirSync(filePath, { recursive: true }); // 使用 mkdirSync 保证同步执行,不需要回调函数
42+
createMultipleAPIFiles(mockRestfulAPI, filePath);
43+
console.log(chalk.green(`Auto create API folder successful: ${filePath}`));
44+
return true;
45+
}
46+
};
47+
48+
// 替换url中的变量为占位符,
49+
// /user/1 -> /user/{id}、
50+
// /user/12/23 -> /user/12/23 | /user/{id}/23 | /user/12/{id}
51+
/* url中第一个/user不变,
52+
1、先精准匹配,
53+
2、依次将每一个替换成变量{id},先替换1个,再2个...
54+
3、只到所有的替换成{id},每次替换完成后,读取上面的文件目录是否存在,如果存在则读取,终止,不存在则继续替换,都没有则返回404
55+
*/
56+
const replaceUrlWithPlaceholders = (url, placeholder = '{id}') => {
57+
const parts = url.split('/').filter(Boolean);
58+
const patterns = [];
59+
60+
// 1. 精确匹配
61+
patterns.push(url);
62+
63+
// 2. 依次将每个部分替换成变量{id}
64+
for (let i = 1; i <= parts.length; i++) {
65+
for (let j = 0; j < parts.length; j++) {
66+
const currentPattern = [...parts];
67+
if (j < i) {
68+
currentPattern[j] = placeholder; // 替换为占位符
69+
}
70+
patterns.push(currentPattern.join('/'));
71+
}
72+
}
73+
74+
return patterns;
75+
};
76+
77+
export const checkFileExistsAndRespond = (url, filePath, req, res) => {
78+
const patterns = replaceUrlWithPlaceholders(url);
79+
for (const pattern of patterns) {
80+
const method = req.method.toLowerCase();
81+
const fileToLoad = path.join(filePath, `${pattern}/${method}.json`);
82+
83+
if (fs.existsSync(fileToLoad)) {
84+
fs.readFile(fileToLoad, 'utf-8', (err, data) => {
85+
if (err) {
86+
res.send(NotFoundResponse);
87+
} else {
88+
res.send(Mock.mock(JSON.parse(data)));
89+
}
90+
});
91+
return; // 成功读取后终止
92+
}
93+
}
94+
// 如果没有匹配到任何文件
95+
res.send(NotFoundResponse);
96+
};

0 commit comments

Comments
 (0)