阅读 1220

还在傻傻的手写DTO吗?自己来实现个代码生成工具吧

这是我参与更文挑战的第15天,活动详情查看: 更文挑战

前言

使用 VueReact 框架来开发项目的同学,是不是也会遇到类似的问题。迭代中,后端新增和修改了很多接口,没有合适工具的情况下,我们要傻乎乎地修改接口文件和相应的 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 的内部实现是循环 definitionproperties 属性,挨个拼接属性字符串:

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.jsonpaths,把接口分到不同的的模块中,再遍历模块,生成模块对应的接口文件内容。

// 创建模块
  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" });
复制代码

最终,生成的接口模块文件类似下图:

image.png

已知缺陷(后续计划)

  • 支持 enum 枚举类型
  • 自定义输出模板

开源

最后,再贴一下这个小工具的 github 地址,swagger-codegen-next

欢迎大家,点赞、提供意见以及帮助!

ps:我暂时只用公司接口的swagger测试了一下,感兴趣的同学可以多拿点swagger的数据帮忙测测,提提issue~😊

文章分类
前端
文章标签