纯手写一个解析工具 swagger-to-typescript

2,849 阅读3分钟

本文正在参加「金石计划」 随着 TypeScript 使用越来越普遍,根据后端的接口定义,我们需要生成对应的 TypeScript 文件,包括类型定义文件和接口文件。这个纯手工转换的过程重复又机械,因此,我们完全可以将其转换过程通过程序完成,提供劳动生产力 😉 。本文记录了自己使用 node 工具实现的过程包括 js 和 ts 两个版本,当然大家也可以使用第三方包 swagger-typescript-api 去实现哦。文章末尾附源码。

swagger.json

image.png

通过浏览器 F12 查看Network 得出:我们平时看到如图的 swagger 生成的文档的数据是 petstore.swagger.io/v2/swagger.… 请求得到的。

image.png swagger.json 中包含了项目所有的类型和接口定义。

image.png

接口返回中,

  • definitions 是关于项目所有类型的定义
  • paths 是所有的接口定义信息
  • tags 是所有的 tag 信息,而接口则根据 tag 进行分类。在下面的生成接口定义文件中,我们可以根据 tag 进行划分,将其生成到对应的文件中。 image.png

更多信息参见:swagger.io/docs/specif…

node

我们需要将转化过程封装成一个函数,通过 Node.js 运行程序,执行方法,得到我们生成的类型文件和接口定义文件。

生成定义文件 newTypes.ts

在生成类型定义文件中的一些约定:

  • 生成的定义字段统一使用可选类型 ?
  • 类型转换规则:
后端定义类型前端类型
numbernumber
integernumber
stringstring
booleanboolean
objectobject

image.png

/**
 * 得到 interface 定义字符串
 * @param prop interface 名称
 * @param definition interface 字段信息
 * @returns interface 定义字符串
 */
const getDefinitionType = (prop, definition) => {
  const cleanProp = prop.replace('«', '').replace('»', '')
  let types = `export interface ${cleanProp} {\n`
  const properties = definition.properties
  for(let p in properties){
    const detail = properties[p]
    const cur = getType(detail)

    if(detail.description){
      types += `  /** ${detail.description} */\n`
      types += `  ${p}?: ${cur}\n\n`
    }else{
      types += `  ${p}?: ${cur}\n`
    }
  }
  types += `}\n\n`
  return types
}

根据 tag 生成相应的接口文件

image.png

关于请求参数的类型包括:

我们主要需要处理 query、path 以及 body 这三种类型: 企业微信20230325-200321@2x.png

注意:根据 swagger.io/docs/specif… 文档说明,其中 query 和 path 参数只能是基本类型,而 body 类型的参数可以是基本类型也可以是对象类型,并且 body 只会有一个。

/**
 * @param basePath 请求的 basePath
 * @param paths 具体URL对象
 */
const handleService = (basePath, paths)=>{
  for(let prop in paths){
    const content = paths[prop]
    const methodsArr = Object.keys(content)

    for(let fetchMethod of methodsArr){
      const serviceName = getServiceName(prop, fetchMethod)
      const methodValueObj = content[fetchMethod]
      const curTag = methodValueObj.tags.pop()
      const tagInfo = globalTags.find(item => item.name === curTag)
  
      let serviceStr = tagInfo.serviceStr
      serviceStr += `/**\n* @description ${methodValueObj.description}\n* @tags ${curTag}\n* @request ${fetchMethod}:${prop}\n*/\nexport const ${serviceName} = `
  
      const importType = tagInfo.importType
  
      // 返回值类型
      const normalResponse = methodValueObj.responses['200'] ? methodValueObj.responses['200'].schema : undefined
      const resType = normalResponse ? getType(normalResponse) : undefined
      const convertTmp = resType?.replace('[', '')?.replace(']', '')
      if((normalResponse?.$ref || normalResponse?.items?.$ref) && !importType.includes(convertTmp)){
        // 预设类型没有
        if(!Object.values(convertTypeMap).includes(convertTmp)){
          importType.push(convertTmp)
        }
      }
  
      const queryParams = []
      const pathParams = []
      const parameters = content[fetchMethod].parameters
  
      let bodyStr = ''
      for(let paramsItem of parameters){
        const inValue = paramsItem.in;
  
        const convertItem = {
          name: paramsItem.name,
          type: getType(paramsItem),
          required: paramsItem.required,
        }
  
        if(inValue === 'body'){
          // 只会有一个 body
          bodyStr = getType(paramsItem.schema)
          if(paramsItem.schema?.$ref){
            if(!importType.includes(bodyStr)){
              importType.push(bodyStr)
            }
          }
        }else if(inValue==='query'){
          // such as /users?role=admin
          queryParams.push(convertItem)
        }else if(inValue === 'path'){
          // such as /users/{id}
          pathParams.push(convertItem)
        }
      }
  
      const queryStr = getParamsStr(queryParams)
      
      const pathStr = handlePathParams(pathParams)
  
      serviceStr += `(${queryStr ? 'params: '+ queryStr+', ' : ''}data: ${bodyStr ? bodyStr : '{}'},${pathStr}) => axios.${fetchMethod}${resType ? '<'+resType+'>' : ''}(\`${basePath}${convertPath(prop)}\`, {${queryStr ? ' params, ' : ' '}data })\n\n`
  
      tagInfo.serviceStr = serviceStr
      tagInfo.importType = importType
    }
  }

  globalTags.forEach(tagFile => {
    const fileName = lowerWord(tagFile.description.replace(/\s*/g, '')) + '.ts'
    const dftStr = getImportStr(tagFile.importType) + tagFile.serviceStr
    fs.writeFile(fileName, dftStr, (err)=>{
      if(err) throw err;
      console.log(`${fileName} is created successfully`)
    })
  })
}

对示例 JSON 解析得到如下文件:

image.png

然后我们就可以在项目中使用啦。

源码

源码地址: github.com/YY88Xu/swag…

Node.js 简介
swagger doc