这是我参与更文挑战的第15天,活动详情查看: 更文挑战。
前言
使用 Vue
、React
框架来开发项目的同学,是不是也会遇到类似的问题。迭代中,后端新增和修改了很多接口,没有合适工具的情况下,我们要傻乎乎地修改接口文件和相应的 dto
(inputDto、outputDto)?
有一次,我接触到一个 Angular
的项目,发现里面有个工具可以根据 swagger.json
文件,自动生成我们需要的 service
文件。于是,我就萌发了一个想法,我也要开发个小工具来实现类似的功能—— swagger-codegen-next 。(之前一直没什么时间拖了好久,这两天抽空赶了一下)
定义配置文件
为了方便用户使用,我们至少得提供 swagger.json 地址和代码生成目录的配置选项。
用户可以在项目根目录下创建一个配置文件,swagger-codegen.config.js
const path = require("path");
module.exports = {
url: "http://xxx/swagger/v1/swagger.json", // 文件的访问地址
output: {
path: "/Users/xxx/services" // 输出目录
}
}
复制代码
读取 swagger.json 文件
要想解析,我们首先需要下载 swagger.json
。这里,我直接用的 node 中的 http
模块:
#!/usr/bin/env node
const path = require("path");
const http = require("http");
const fs = require("fs");
const process = require("process");
const codegen = require("../lib/index.js");
let swaggerData = "";
console.log('start request swagger.json\n')
const client = http.get(url, (res) => {
res.on("data", (c) => {
swaggerData += c;
});
res.on("end", () => {
console.log('end request swagger.json\n')
const swaggerJson = JSON.parse(swaggerData);
fs.writeFileSync(
path.join(process.cwd(), ".swagger-cache"),
swaggerData,
{ encoding: "utf-8" }
);
codegen(swaggerJson, data.options);
});
});
复制代码
获取到 swagger.json 内的数据后,为了避免重复请求,把获取的数据存到缓存文件中,然后调用 codegen
生成接口文件。
上面出现的 options
,就是从 swagger-codegen.config.js
获取到的配置选项。
分析 swagger.json的结构
{
"swagger": "2.0" // swagger 的版本
“info”: {
"version": "v1", // 接口版本
"title": "xxx api"
},
"path": { // 里面都是接口 api 地址,包含接口类型和参数信息
"api/servies/app/User/getInfo": {
//...
}
},
"definitions": { // 这里面都是 dto
"UserInfoDto": {
"properties": {
"name": {
"type": "string",
"description": "姓名"
}
}
}
}
}
复制代码
生成 Dto 文件
我们把所有 dto 都输出到一个文件。
这有两个好处:
- 写接口文件时,需要的 DTO 都从一个模块导入,不需要考虑接口文件和 DTO 多对多的复杂关联关系;
- DTO 文件只导出 dto 接口,可以避免模块引用循环。
范型 DTO
swagger里涉及到范型的 DTO,会显示成形如PagedResult[ListItem]
。这里,我选择使用正则来获取前面的 PagedResult
:
// 泛型接口
const genericDtos = SwaggerHelper.instance.getGenericDtos(); // 获取名字包含 [ 字符的 definition
for (let i = 0; i < genericDtos.length; i++) {
const definitionKeys = Object.keys(json.definitions);
const reg = new RegExp(`${genericDtos[i]}\\[[a-zA-Z0-9]+\\]`);
const targetDto = definitionKeys.find((n) => reg.exec(n));
if (targetDto) {
s += `
export interface ${genericDtos[i]}<T> {
${getProperties(targetDto, json.definitions[targetDto])}
}
`;
}
}
复制代码
普通 DTO
// 通用接口
const commonDtos = keys(dtoMap).filter((n) => !n.includes("["));
for (let i = 0; i < commonDtos.length; i++) {
let dto = json.definitions[commonDtos[i]];
s += `
export interface ${commonDtos[i]} {
${getProperties(commonDtos[i], dto)}
}
`;
}
复制代码
getProperties
的内部实现是循环 definition
的 properties
属性,挨个拼接属性字符串:
s += `
/**
* @description ${description ? description : ""}
*/
${p}${isRequired}: ${type};
`;
复制代码
这里的isRequired
只有在解析 inputDto 类型的 DTO 才有用。
inputDto 和 outputDto
简单说明两者的区别,inputDto 是请求数据的 DTO,outputDto 是服务端返回数据的 DTO。
只有使用请求体传输数据的对象才会是 DTO,像在 url 里拼接的数据不是 DTO。
生成不同模块的接口文件
首先,我们要把接口进行分类,各个模块的接口要收集到一起,写到各自的文件中。
这里,我是用 tags
属性来重新分组的。
let paths = SwaggerHelper.instance.paths;
const urls: ApiUrl[] = Object.keys(paths);
let moduleNames: string[] = flow(
flattenDeep,
uniq
)(
urls.map((pathName) => {
return paths[pathName].tags;
})
);
复制代码
然后,遍历 swagger.json
的 paths
,把接口分到不同的的模块中,再遍历模块,生成模块对应的接口文件内容。
// 创建模块
for (let i = 0, len = modules.length; i < len; i++) {
const dtos = getDtos(modules[i].children); // 模块内 各个api 引用到的 dto 名称
let dtoImport = "";
if (dtos.length) {
dtoImport += `import { ${uniq(dtos).join(", ")} } from './dto'\n`;
}
let s = `
import http from "../http"; // 用户自己实现的请求方法,可以使用axios
${dtos.length ? dtoImport : ""}
${
useQueryString(modules[i].children)
? 'import queryString from "query-string"'
: "" // 如果有使用 query 参数的
}
class ${modules[i].moduleName} {
${getChildModules(modules[i].children)}
}
export default ${modules[i].moduleName};
`;
复制代码
美化
在写入文件前,使用 prettier
来格式化内容:
s = prettier.format(s, { semi: false, parser: "babel-ts" });
复制代码
最终,生成的接口模块文件类似下图:
已知缺陷(后续计划)
- 支持 enum 枚举类型
- 自定义输出模板
开源
最后,再贴一下这个小工具的 github 地址,swagger-codegen-next 。
欢迎大家,点赞、提供意见以及帮助!
ps:我暂时只用公司接口的swagger测试了一下,感兴趣的同学可以多拿点swagger的数据帮忙测测,提提issue~?