添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

最近在日常的开发工作中,发现了一个问题,就是在对接后端的接口时,发现经常要去翻阅接口文档,查到对应的接口以及返回值。这个操作看上去很正常没什么问题,但是没有代码提示确实不是很方便。需要在浏览器请求中查看返回值

通常在项目的设计中都会对接口请求做一个封装处理,目的就是统一规范,统一管理维护,方便拓展和迭代。常见的就是在项目的 src/api 文件夹中 添加接口配置文件 以及按模块添加接口的封装

如下面这个简单的实现, 当你在业务中想要请求一个 page 列表的时候,需要自己定义请求参数的类型以及接口响应的类型。自己手写类型这个过程就会显得很繁琐且非常耗时。如果接口多的时候那简直就是灾难,有没有一种工具可以自动帮我们来完成接口的类型编写 这件事呢? 接下来给大家简单介绍一下 阿里开源的工具 Pont 可以来帮我们完成这件事情,大大节省了对接接口的时间。

// src/api/http.ts  公共的 http 请求 axios 封装
class Http {
  // 请求类型
  post<T, U>(url: string, data: U, config?: AxiosRequestConfig) {
    return this.handleResponse<T>({
      url: url,
      method: 'POST',
      data: data,
      ...config
  get<T, U>(url: string, data: U, config?: AxiosRequestConfig): Promise<T>
  // 请求方法
  handleResponse<T>(config: AxiosRequestConfig): Promise<T>
  const httpReq = new Http({
    timeout: 5000
  // 请求方式封装 (T 定义接口放回的数据实体, U 定义接口入参的结构)
  export function post<T, U>(url: string, data: U, config?: AxiosRequestConfig) {
    return httpReq.post<T, U>(url, data, config)
// ----------------------  分割线 ----------------------------- //
// src/api/mods/xxx.ts  封装接口对应的方法
interface getPageParams {
  page: number
  size: number
interface Item {
  id: number
  title: string
// 获取列表
export function getPage(data: getPageParams) {
  return post<Item[], getPageParams>('/api/getpage', data)

Pont 链接前后端

pont 在法语中是“桥”的意思,寓意着前后端之间的桥梁。Pont 把 swagger、rap、dip 等多种接口文档平台,转换成 Pont 元数据。Pont 利用接口元数据,可以高度定制化生成前端接口层代码,接口 mock 平台和接口测试平台。其中 swagger 数据源,Pont 已经完美支持。并在一些大型项目中使用了近两年,各种高度定制化需求都可以满足。

简单的来说 就是通过解析 swagger 的 json 数据,获取到接口的地址、入参和返回值,从而根据这些参数 生成自定义的代码封装。有点意思…

既然官方介绍得这么强大, 那我们来看看怎么解决前言中提到的问题,本文给大家介绍在 vue 中的实践,在此之前建议先简单过一下官方介绍文档。

GitHub地址:github.com/alibaba/pon…

示例:github.com/alibaba/pon…(官方只有 react 版本)

希望它能给我们解决

  • 自动生成接口声明文件
  • 自动生成接口封装代码
  • 自动生成接口数据的基类
  • 自动生成接口代码注释
  • yarn apis 自动输出文件
  • 找一个 swagger 接口文档 ,注意 pont 只支持 swagger 2 以上的版本
  • 在项目的根目录创建一个 pont-config.json文件
  • 创建一个目录 pontConfig
  • 创建两个文件到 pontTemplate.ts transformPath.tspontConfig
  • 全局安装 pont-engine
  • 初始配置文件

  • originUrl:接口平台提供的数据源(连接一般在swagger 标题下面)
  • templatePath:自定义生成代码模版的文件
  • transformPath:可以对swagger数据源预处理
  • outDir:文件导出的目录
  • prettierConfig:喜闻乐见的 代码格式化配置
  • templateType:官方提供了 fetch 和 hook 的代码生成模板,但这里我们选择自己生成
  • mocks:喜闻乐见的 mocks 数据
  • 官方有非常详细的介绍 更多字段解释-> 传送门

    pont-config.json 添加下面的信息

    "originUrl": "/path/to/swagger.json", "templateType": "fetch", "templatePath": "./pontConfig/pontTemplate", "transformPath": "./pontConfig/transformPath", "outDir": "./src/api", "mocks": { "enable": false "prettierConfig": { "tabWidth": 2, "semi": false, "singleQuote": true, "bracketSpacing": true, "arrowParens": "avoid", "trailingComma": "none", "proseWrap": "never", "printWidth": 300, "htmlWhitespaceSensitivity": "ignore", "endOfLine": "auto"

    自定义解析 swagger 的接口数据

    当我们拿到后端的 swagger 链接时,一般是包含整个项目的接口。可能有前台的后台的,如果一键生成就包含一些我们不必要的接口信息和冗余代码, 所以 pont 给我们提供了过滤 swagger 模版数据的 api transformPath,我们通过这个配置 export 一个自定义处理数据的方法来完成我们的自定义操作。这样就可以按我们的意愿来选择想要的模块。

    mod,指的是接口模块,一个 mod 下可以有N个接口,一般对应的是后端的控制器。

    transformPath

    transformPath.ts 添加下面的代码,再配置自己想要的接口模块,也可以不加默认导出全部,如果不加 配置文件 transformPath字段留空即可。

    // transfrom.ts 根据 Mod.name进行过滤
    import { StandardDataSource } from 'pont-engine'
    const useMods = ['模块A', '模块B', '模块B']
    // 从原数据中过滤我们所需要的接口模块
    export default function transform(data: StandardDataSource) {
      // 取到所需要的 mods 和 baseClass
      const { mods, baseClasses } = filterModsAndBaseClass(useMods, data)
      data.mods = mods
      data.baseClasses = baseClasses
      // 返回给 pont,它将只会处理这些模块数据
      return data
     * 过滤mod及所依赖的baseClass
     * @param filterMods Mod.name数组
     * @param data StandardDataSource
    function filterModsAndBaseClass(filterMods: string[], data: StandardDataSource) {
      const mods = data.mods.filter(mod => {
        const pickOne = filterMods.includes(mod.name)
        pickOne && console.log('模块: ' + mod.name)
        return pickOne
      // 获取所有 typeName (实体的名称)
      let typeNames = JSON.stringify(mods).match(/"typeName":".+?"/g)
      typeNames = Array.from(new Set(typeNames)) // 去重
        // 取typeName的值
        .map(item => item.split(':')[1].replace(/"/g, ''))
      // 过滤 baseClasses (根据实体名称生成baseclass)
      const baseClasses = data.baseClasses.filter(cls => typeNames && typeNames.includes(cls.name))
      return { mods, baseClasses }
    

    自定义生成属于自己的接口封装

    在每个人的项目实践中都会根据自己的业务需要去封装自己的 http 接口请求,比如常见的 axio 和 fetch。接下来我们将按照上文 前言 中提到的封装来生成接口代码。

    分析一下 下面这个代码片段我们需要生成那些部分才能组成一个 完整的接口封装

  • 需要定义一个方法入参的接口类型 getPageParams
  • 需要定义一个 api 返回的数据格式接口 Item
  • 一个方法名 getPage 参数名 getPageParams
  • 接口请求类型 post 接口路径 /api/getpage
  • interface getPageParams {
      page: number
      size: number
    interface Item {
      id: number
      title: string
    // 需要一个方法名 参数名
    export function getPage(data: getPageParams) {
      return post<Item[], getPageParams>('/api/getpage', data)
    

    我们可以看到这6个字段是我们希望它能够帮我们自动的生成的,如各种方法名,入参和返回结果。这样就可以根据不同的接口来生成不同的代码封装代码,然后再将他们按模块一个个写入到我们项目的 src/api之中,这样就很完美了

    pont 提供的能力就是通过重写他们的 方法 来实现高度定制化的需求,可以给你定义代码模板以及输出的文件目录结构。以下是我们使用到 api,pont 给我们提供了很多功能 大家可以去看看文档,下面是官方介绍。

    代码生成器: pont 将即刻生成一份默认的自定义代码生成器。通过覆盖默认的代码生成器,来自定义生成代码。默认的代码生成器包含两个类,一个负责管理目录结构 FileStructures,一个负责管理目录结构每个文件如何生成代码 CodeGenerator。自定义代码生成器通过继承这两个类,覆盖对应的代码来达到自定义的目的。

    export class FileStructures extends Pont.FileStructures {
      /** 获取模块的类型定义代码, generator.hasContextBund 为true时覆盖 generator的同名方法 */
      getModsDeclaration(originCode: string, usingMultipleOrigins: boolean): string
      /** 获取总文件结构  */
      getFileStructures(): FileStructure
    export default class MyGenerator extends CodeGenerator {
      /** 获取接口实现内容的代码 */
      getInterfaceContent(inter: Interface): string
      /** 获取所有基类文件代码 */
      getBaseClassesIndex(): string
      /** 获取单个模块的 index 入口文件 */
      getModIndex(mod: Mod): string
      /** 获取所有模块的 index 入口文件 */
      getModsIndex(): string
      /** 获取接口类和基类的总的 index 入口文件代码 */
      getIndex(): string
    

    CodeGenerator 类

  • getInterfaceContent
  • 这个接口的入参为 Interface(建议看下源码定义) 这个api给我们提供了接口所有相关的信息,也满足我们上面提到的6个字段,我们就可以用它来完成接口自定义封装。将参数拼装成我们想要的样子。

    inter 应用介绍

    getInterfaceContent(inter: Interface): string
    inter.description // 接口描述
    inter.method // 请求类型
    // 接口参数代码块
    host // 业务域名
    paramsCode = inter.getParamsCode(interfaceName) // get 参数 querystring
    inter.getBodyParamsCode() // post 参数 body payload
    inter.response // 描述接口的响应类型
    inter.path // 接口路径
    // 取到接口生成的 返回值 view model
    const resultVo = formatResultVo(inter.response)
    // 格式化后获取到 请求封装的入参
    const paramsCodeString = formatParameter(...)
    // 将 paramsCode 转为接口注释
    const getParamsNoteList = getParamsNote(paramsCode)
    // 类似这样的拼装即完成了接口的自定义封装
    export function ${inter.name}(${paramsCodeString}) {
      return ${method}<${resultVo}, ${bodyParamsCode}>(
        ${host}${inter.path}(data, config)
    // 我们想要的格式模板
    // export function getPage(data: getPageParams) {
    //   return post<Item[], getPageParams>('/api/getpage', data)
    
  • getModIndex
  • pont 在输出文件的时候,会按照一个接口生成一个 ts 文件,这样接口一多就会生成很多的独立文件。为了避免接口文件太多影响编译打包速度的问题,所以我们可以在这一步将所有接口生成到一个 index.ts 文件中,就可以达到优化的目的。 这里的方法入参为一个接口模块,可以在这个模块中拿到所有的接口,再将他们集合起来,具体实现看下面完整实现代码

  • getModsIndex
  • 将所有的接口模块 一起导入到一个 index.ts 再导出为一个模块
    具体实现看下面完整实现代码

    import * as common from './common'
    import * as user './user'
    export default { common, user }
    
  • getIndex
  • 这一步操作我们将所有的接口模块和接口定义声明合并导出。如果想要绑定到全局,可以直接 import 这个文件具体实现看下面完整实现代码

    import * as defs from './baseClass'
    import mods from './mods/'
    export { defs, mods }
    
  • getBaseClassesIndex
  • 这里主要是处理 baseclass 中 number 类型的兼容(踩坑记录第二条)

    下面请看接口的完整实现代码

    import { CodeGenerator, Interface, Mod, StandardDataType } from 'pont-engine'
    export default class MyGenerator extends CodeGenerator {
      getInterfaceContent(inter: Interface) {
        // 优化参数名
        // 定义参数接口类型名称
        const interfaceName = `${inter.name}Params`
        // 获取 get 请求,接口声明的内容字段 -> 指定接口名称 生成代码块
        const paramsCode = inter.getParamsCode(interfaceName)
        // 获取 post 请求,入参的实体 -> 代码块
        const bodyParamsCode = inter.getBodyParamsCode()
        // 请求方法
        const method = inter.method.toLowerCase() as MethodType
        // 判断接口是否需要请求参数
        const hasGetParams = !paramsCode.replace(/\n| /g, '').includes('{}')
        const host = '`${exhibit}'
        const po = '`'
        // 将 class 定义 转为接口声明(属于强迫症行为)
        const funcParams = paramsCode.replace(/class /, `interface `)
        // 格式化后获取到 请求封装的入参
        const paramsCodeString = formatParameter(method, hasGetParams, interfaceName, bodyParamsCode)
        // 取到接口生成的 返回值 view model
        const resultVo = formatResultVo(inter.response)
        // 根据入参定义传参的格式(封装好的 get post 请求)
        let totalParams = ''
        // 需要携带参数
        if (paramsCodeString.length) {
          // 判断是否需要 axios config 配置
          if (paramsCodeString.includes('config')) {
            totalParams = ', data, config'
          } else {
            totalParams = ', data'
        const getParamsList = getParamsNote(paramsCode)
        // 添加接口注释
        const note = `/**
      * @description ${inter.description}
      * @method ${method}
      ${getParamsList.join('\n')} 
        // 返回整体的代码模板
        return `
        ${hasGetParams ? funcParams : ''}
        ${note}
        export function ${inter.name}(${paramsCodeString}) {
          return ${method}<${resultVo}, ${bodyParamsCode || (hasGetParams ? interfaceName : 'any')}>(
            ${host}${inter.path}${po}${totalParams}
      /** 获取所有基类文件代码 */
      getBaseClassesIndex() {
        const clsCodes = this.dataSource.baseClasses.map(base => {
          return `
            class ${base.name} {
              ${base.properties
                .map(prop => {
                  const propValue = prop.toPropertyCodeWithInitValue(base.name)
                  // 由于 pont 没有对 number 类型进行处理,初始值给了 undefined
                  // 所以这里需要将基类属性类型为number的初始值 改为 0
                  if (prop.dataType.typeName === 'number') {
                    return propValue.replace(/undefined/g, '0')
                  } else {
                    return propValue
                .filter(id => id)
                .join('\n')}
        if (this.dataSource.name) {
          return `
            ${clsCodes.join('\n')}
            export const ${this.dataSource.name} = {
              ${this.dataSource.baseClasses.map(bs => bs.name).join(',\n')}
        return clsCodes.map(cls => `export ${cls}`).join('\n')
      /** 获取单个模块的 index 入口文件 */
      getModIndex(mod: Mod) {
        const methods = new Set<string>()
        let importAxiosConfig = ''
        const modContent = `
           * @description ${mod.description}
          ${mod.interfaces
            .map(inter => {
              methods.add(inter.method)
              // 获取 post 请求
              if (inter.method == 'post') {
                // 接口声明的内容字段 -> 指定接口名称 生成代码块
                const paramsCode = inter.getParamsCode()
                // 判断接口是否需要请求参数
                const hasGetParams = !paramsCode.replace(/\n| /g, '').includes('{}')
                if (hasGetParams) {
                  importAxiosConfig = `import type { AxiosRequestConfig } from '@/types/axios'`
              return this.getInterfaceContent(inter)
            .join('\n')}
        return `
        ${importAxiosConfig}
        import { exhibit } from '@/utils/http/baseUrl'
        import { ${Array.from(methods)
          .map(type => {
            return type.toLowerCase()
          .join(', ')} } from '@/utils/http'
        ${modContent}
      /** 获取所有模块的 index 入口文件 */
      getModsIndex() {
        const conclusion = `
          export default { ${this.dataSource.mods.map(mod => reviseModName(mod.name)).join(', \n')} }
        return `
        ${this.dataSource.mods
          .map(mod => {
            const modName = reviseModName(mod.name)
            console.log('导出接口:' + modName)
            return `import * as ${modName} from './${modName}';`
          .join('\n')}
          ${conclusion}
      /** 获取接口类和基类的总的 index 入口文件代码 */
      getIndex() {
        let conclusion = `
          import * as defs from './baseClass';
          import mods from './mods/';
          export { defs, mods }
        // dataSource name means multiple dataSource
        if (this.dataSource.name) {
          conclusion = `
            import { ${this.dataSource.name} as defs } from './baseClass';
            export { ${this.dataSource.name} } from './mods/';
            export { defs };
        return conclusion
    

    自定义生成文件结构目录

    FileStructures 类

  • getModsDeclaration
  • 在 pont 官方的模板代码中会 生成 api.d.ts 的声明文件,但其中包含了 API 接口方法的定义,这部分是我们不需要的,因为我们采用自己生成的模板,所以这部分在我们这里是不适用,我们只需要其中的 defs 类型的定义即可。因此我们将重写这个方法 让其返回一个空的字符串

  • getFileStructures
  • pont 在输出文件的时候,会按照一个接口生成一个 ts 文件,如果接口几十个,这将会生成几十个文件,很明显在这点上会影响到打包和编译 ts 的速度。所以我们要将 一个 mod 下面的所有接口 合成一个 ts 文件,这一步在上面的 getModIndex中已经完成,所以我们在这个地方只需要创建 index.ts 即可,其他可以不创建

    import * as Pont from 'pont-engine'
    export class FileStructures extends Pont.FileStructures {
      /** 获取 index 内容 */
      getModsDeclaration(originCode: string, usingMultipleOrigins: boolean) {
        // 由于我们不使用 pont 的 request模板,所以这里我们只需要导出接口的定义
        // API 的定义声明,这里不需要所以返回空
        if (usingMultipleOrigins) {
          return ''
        } else {
          return ''
      /** 获取总文件结构   */
      getFileStructures() {
        // 文件结构
        const result = this.getOriginFileStructures(this.generators[0])
        // 遍历所有mods
        for (const key in result.mods) {
          if (Object.prototype.hasOwnProperty.call(result.mods, key)) {
            const el = result.mods[key]
            // 只保留 mods 中的index文件, 在 getModIndex 中将接口代码写入到同一个文件
            if (el['index.ts']) {
              const newmod = {
                'index.ts': el['index.ts']
              result.mods[key] = newmod
        return result
    

    自定义 FileStructures

    在官方的 customizedPont.md 介绍中 export 一个 MyFileStructures 即可 重写 FileStructures 的方法,但在 1.3.3 的版本中并不生效, 通过查看源码发现 Manager 类中的 setFilesManager 取值是 FileStructures 并非 MyFileStructures 所以下面这个写法是错误的 好家伙

    // pontTransfrom.ts  文档推荐写法 
    export class MyFileStructures extends FileStructures {}
    // 实际源码 manage.ts 解析代码时 结构取值为 FileStructures
    setFilesManager() {
      this.report('文件生成器创建中...')
      const { default: Generator, FileStructures: MyFileStructures } = getTemplate(
        this.currConfig.templatePath,
        this.currConfig.templateType
      // 其他源码 .... 
    
    import * as Pont from 'pont-engine'
    export class FileStructures extends Pont.FileStructures {}
    

    baseclass 中 number 类型的字段初始值为 undefined

    经过查找源码发现 class StandardDataType->getInitialValue 这个方法在生成 baseClass 的 key value 是,没有给 number 类型的属性做初始值处理,初始值给了 undefined, 所以出现基类属性

    初始值的类型和接口定义 api.d.ts 的类 属性类型不一致导致类型错误。 通过重写 下面这个方法 纠正这个类型错误

    /** 获取所有基类文件代码 */
    getBaseClassesIndex() {}
    

    在 package.json 中 添加一个 script

    'script': {
       'apis': 'pont generate'
    

    yarn apis -> pont 会根据我们配置的信息自动生成 api 下资源文件

    ├─api #api文件夹 │ ├─mods #模块 │ │ ├─common #公共 │ │ ├─user #用户 │ │ └─index.ts #集合所有接口 | ├─baseClass.ts #基类 | ├─api.d.ts #接口声明文件 │ └─index.ts #集合导出基类&模块&声明

    在 vue 中按需引用接口 如下示例:

    // user.vue
    import { userVo } from '@/api/baseClass'
    import { getDetail } from '@/api/mods/user'
    const userInfo = reactive(new userVo())
    function getUserDetail(id: number) {
      getDetail({ id })
        .then(res => {
            userInfo = res.data
        .catch(error => {
          Message.error(error.msg)