最近在日常的开发工作中,发现了一个问题,就是在对接后端的接口时,发现经常要去翻阅接口文档,查到对应的接口以及返回值。这个操作看上去很正常没什么问题,但是没有代码提示确实不是很方便。需要在浏览器请求中查看返回值
通常在项目的设计中都会对接口请求做一个封装处理,目的就是统一规范,统一管理维护,方便拓展和迭代。常见的就是在项目的 src/api 文件夹中 添加接口配置文件 以及按模块添加接口的封装
如下面这个简单的实现, 当你在业务中想要请求一个 page 列表的时候,需要自己定义请求参数的类型以及接口响应的类型。自己手写类型这个过程就会显得很繁琐且非常耗时。如果接口多的时候那简直就是灾难,有没有一种工具可以自动帮我们来完成接口的类型编写 这件事呢? 接下来给大家简单介绍一下 阿里开源的工具 Pont 可以来帮我们完成这件事情,大大节省了对接接口的时间。
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
export function post<T, U>(url: string, data: U, config?: AxiosRequestConfig) {
return httpReq.post<T, U>(url, data, config)
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.ts
到 pontConfig
全局安装 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
字段留空即可。
import { StandardDataSource } from 'pont-engine'
const useMods = ['模块A', '模块B', '模块B']
export default function transform(data: StandardDataSource) {
const { mods, baseClasses } = filterModsAndBaseClass(useMods, data)
data.mods = mods
data.baseClasses = baseClasses
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
let typeNames = JSON.stringify(mods).match(/"typeName":".+?"/g)
typeNames = Array.from(new Set(typeNames))
.map(item => item.split(':')[1].replace(/"/g, ''))
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 {
getModsDeclaration(originCode: string, usingMultipleOrigins: boolean): string
getFileStructures(): FileStructure
export default class MyGenerator extends CodeGenerator {
getInterfaceContent(inter: Interface): string
getBaseClassesIndex(): string
getModIndex(mod: Mod): string
getModsIndex(): string
getIndex(): string
CodeGenerator 类
getInterfaceContent
这个接口的入参为 Interface(建议看下源码定义) 这个api给我们提供了接口所有相关的信息,也满足我们上面提到的6个字段,我们就可以用它来完成接口自定义封装。将参数拼装成我们想要的样子。
inter 应用介绍
getInterfaceContent(inter: Interface): string
inter.description
inter.method
host
paramsCode = inter.getParamsCode(interfaceName)
inter.getBodyParamsCode()
inter.response
inter.path
const resultVo = formatResultVo(inter.response)
const paramsCodeString = formatParameter(...)
const getParamsNoteList = getParamsNote(paramsCode)
export function ${inter.name}(${paramsCodeString}) {
return ${method}<${resultVo}, ${bodyParamsCode}>(
${host}${inter.path}(data, config)
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`
const paramsCode = inter.getParamsCode(interfaceName)
const bodyParamsCode = inter.getBodyParamsCode()
const method = inter.method.toLowerCase() as MethodType
const hasGetParams = !paramsCode.replace(/\n| /g, '').includes('{}')
const host = '`${exhibit}'
const po = '`'
const funcParams = paramsCode.replace(/class /, `interface `)
const paramsCodeString = formatParameter(method, hasGetParams, interfaceName, bodyParamsCode)
const resultVo = formatResultVo(inter.response)
let totalParams = ''
if (paramsCodeString.length) {
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')
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}
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}
getIndex() {
let conclusion = `
import * as defs from './baseClass';
import mods from './mods/';
export { defs, mods }
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 {
getModsDeclaration(originCode: string, usingMultipleOrigins: boolean) {
if (usingMultipleOrigins) {
return ''
} else {
return ''
getFileStructures() {
const result = this.getOriginFileStructures(this.generators[0])
for (const key in result.mods) {
if (Object.prototype.hasOwnProperty.call(result.mods, key)) {
const el = result.mods[key]
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 所以下面这个写法是错误的 好家伙
export class MyFileStructures extends 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
│ ├─mods
│ │ ├─common
│ │ ├─user
│ │ └─index.ts
| ├─baseClass.ts
| ├─api.d.ts
│ └─index.ts
在 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)