使用pont实现swagger文档生成ts响应类型

223 阅读4分钟

前言

ts真的非常好用,虽然它功能庞大,各种类型体操搞得人晕头转向,但是实际开发过程中,我们大概只需要它的类型提示功能,它能避免一些非常基础的报错。接口请求,响应数据是从后端回来的,如果接口少,一个个手动定义并没有什么问题,但是如果足够多,加上后端时不时要调整,那改起来真的是非常折磨人,所以这个功能自动化才正确的选择。

pont

pont是阿里出的一个工具,它可以非常方便的把后端的swagger文档转成ts提示类型,不仅如此,它还支持添加模板,直接生成对应的接口请求,甚至能帮我们生成请求参数的默认基类。

Github:github.com/alibaba/pon…

安装

首先把pont安装到全局

pnpm add -g pont-engine

下载vscode插件

image.png 下载后右边就会多出一个桥的图标

image.png

配置

在根目录创建一个叫做pont-config.josn,具体配置可以看文档 github.com/alibaba/pon…

这里贴一下我的配置文档

{
  "originUrl": "http://localhost:3002/api/admin/api-doc-json",
  "outDir": "./src/network/api",
  "originType": "SwaggerV3",
  "templatePath": "./pontTemplate",
  "prettierConfig": {
    "singleQuote": true,
    "semi": true,
    "tabWidth": 2,
    "useTabs": false,
    "bracketSpacing": true,
    "arrowParens": "avoid",
    "printWidth": 120,
    "vueIndentScriptAndStyle": true,
    "htmlWhitespaceSensitivity": "ignore"
  }
}

    1. originUrl: swagger文档的json数据
    1. outDir:生成的文件位置
    1. originType:swagger文档类型
    1. templatePath:生成的接口模板
    1. prettierConfig:prettier配置,可以对生成的代码进行格式化

接下来在根目录创建一个pontTemplate.ts的文件,文档中对于这个参数的介绍是

指定自定义代码生成器的路径(使用相对路径指定)。一旦指定,pont 将即刻生成一份默认的自定义代码生成器。自定义代码生成器是一份 ts 文件,通过覆盖默认的代码生成器,来自定义生成代码。默认的代码生成器包含两个类,一个负责管理目录结构,一个负责管理目录结构每个文件如何生成代码。自定义代码生成器通过继承这两个类(类型完美,可以查看提示和含义),覆盖对应的代码来达到自定义的目的。 github.com/alibaba/pon…

我的pontTemplate.ts

import {
  Interface,
  CodeGenerator,
  Mod,
  Property,
  BaseClass,
  Surrounding,
  FileStructures as OriginFileStructures,
} from 'pont-engine';

export default class MyGenerator extends CodeGenerator {
  getInterfaceContent(inter: Interface) {
    const method = inter.method.toLowerCase();
    let query = inter.getParamsCode();
    const body = inter.getBodyParamsCode();
    let params = '';
    let path = inter.path;
    // 函数参数
    let argumentStr = '';
    // axios参数
    let optionsStr = '';

    if (inter.path.includes('{')) {
      path = inter.path.split('{')[0];
      params = inter.path.split('{')[1].replace('}', '');
      argumentStr += params + ': string';
    }

    const queryExist = query && !params && query.includes(':');

    if (queryExist) {
      optionsStr = 'params, ';
      const interfaceName = inter.name.slice(0, 1).toUpperCase() + inter.name.slice(1).replace('Api', '') + 'ParamsDto';
      query = query.replace('Params', interfaceName);
      query = query.replace('class', 'interface');
      argumentStr += (argumentStr.length ? ', ' : '') + 'params: ' + interfaceName;
    }

    if (body) {
      argumentStr += (argumentStr.length ? ', ' : '') + 'data: ' + body;
      optionsStr = 'data, ';
    }

    argumentStr += argumentStr.length ? ', ' : '';

    return `
    import http from '@/network';
    import type { RequestOptions } from '@/network/http.interface';

    ${queryExist ? 'export ' + query : ''}

    export function ${inter.name}(${argumentStr}options?: RequestOptions) {
      return http.request<${inter.responseType}>({
        method: '${method}',
        url: '${path}' ${params ? '+ ' + params : ''},
        ${optionsStr ? optionsStr + '...options' : '...options'}
      });
    };
   `;
  }

  /** 获取接口类和基类的总的 index 入口文件代码 */
  getIndex() {
    return `
    import * as defs from './baseClass';
    import * as mods from './mods';

    export { defs, mods }
  `;
  }

  /** 获取模块的 index 入口文件 */
  getModIndex(mod: Mod) {
    let exportPath = 'export { ';
    const importPath = mod.interfaces.map((i: Interface, index: number) => {
      if (index === 0) {
        exportPath += i.name;
      } else {
        exportPath += ', ' + i.name;
      }
      return `import { ${i.name} } from './${i.name}';`;
    });
    return `
      ${importPath.join('\n')}
  
      ${exportPath} };
      `;
  }

  /** 获取所有模块的 index 入口文件 */
  getModsIndex() {
    let exportPath = 'export { ';
    const importPath = this.dataSource.mods.map((mod: Mod, index: number) => {
      if (index === 0) {
        exportPath += mod.name;
      } else {
        exportPath += ', ' + mod.name;
      }
      return `import * as ${mod.name} from './${mod.name}';`;
    });
    return `
    ${importPath.join('')}

    ${exportPath} };
    `;
  }

  /** 实体类添加类型 */
  getBaseClassesIndex(): string {
    const classCodeList = this.dataSource.baseClasses.map((base: BaseClass) => {
      return `
      class ${base.name} {
        ${base.properties
          .map((property: Property<any>) => {
            let value =
              property.toPropertyCode(Surrounding.typeScript, true).slice(0, -1) +
              ' = ' +
              property.dataType.getInitialValue();

            // 由于 pont 没有对 number 类型进行处理,初始值给了 undefined
            // 所以这里需要将基类属性类型为number的初始值 改为 0
            if (property.dataType.typeName === 'number') {
              value = value.replace(/undefined/g, '0');
            }
            // 对可选值进行undefined处理
            if (!property.required) {
              value = value.replace(/=.+/, '= undefined');
            }
            return value;
          })
          .join('\n')}
      }
    `;
    });

    return classCodeList.map(classCode => `export ${classCode}`).join('\n');
  }
}

export class FileStructures extends OriginFileStructures {
  /** 获取 index 内容 */
  getModsDeclaration() {
    // 由于我们不使用 pont 的 request模板,所以这里我们只需要导出接口的定义
    // API 的定义声明,这里不需要所以返回空
    return '';
  }
}

这里也贴一下生成的文件格式和内容

image.png

可以看到这里的请求参数和响应参数都有对应的类型存在了,defs定义在api.d.ts中。让我们回过头来看一下pontTemplate文件,我们可以知道getInterfaceContent作用是生成对应接口代码,如果你对接口请求的封装于我不一样,可以更改这里的模板。

使用

image.png

生成的基类默认值

image.png

使用的时候直接引用对应的方法,甚至可以直接引用基类作为默认值。

参考

pont文档:github.com/alibaba/pon… 文章:juejin.cn/post/713026…