Motivation

在写js的时候,经常为某些结构量没有类型而感到痛苦,访问属性必须查阅另外的手册,在typescript出来以后,可以为某些常用的结构定义类型暗示,在使用时就有类型检查和自动补全了。

于是,前后端交互的接口,是不是也应该有某种工具,自动生成ts版本的接口呢?让js开发能够直接传入具有类型的参数,返回值也有对应的类型,restful的api会变得非常简单,js调用后端接口等同于调用生成出来的函数。而且,在生成函数这里,还可以实现切面编程,做一些mock、filter的工作。

最后,我找到了yapi、rap这样的工具,不过在考察的时候,意识到grpc也可能是不错的选择。

Yapi

首先,java开发后端时,使用idea插件,可以将controller定义的接口,推送至yapi。插件还支持一些注解,提供mock、注释等功能。

前端可以用yapi-to-ts生成ts代码。

java插件用起来没什么大问题,推送很方便。

web插件有些许的问题,作者保留了一些接口,让用户来填写实现,比如最终生成的请求方式,是需要自己实现的。

我实现了一个用fetch请求后端的版本,同时还支持路径参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const baseUrl = options.server === 'mock'
? payload.mockUrl
: options.server === 'dev'
? payload.devUrl
: payload.prodUrl

// 请求地址
let url = `${baseUrl}${payload.path}`

if (payload.paramNames.length > 0) {
// 路径参数
const pathParas: string[] = []
url = url.replace(new RegExp('\\{(\\w*)\\}'), (s, paraName) => { pathParas.push(paraName); return payload.data[paraName] })

// url paras
const urlParas = payload.paramNames.filter((value) => pathParas.includes(value))
url = url + '?'
urlParas.forEach((value) => {
url = url + value + '=' + payload.data[value] + '&'
})
}
const bodyParas: string[] = Object.keys(payload.data).filter((value: string) => !payload.paramNames.includes(value))
let req
// 具体请求逻辑
if (payload.requestBodyType === RequestBodyType.form) {
const formdata = new FormData()
bodyParas.forEach(value => {
formdata.append(value, payload.data[value])
})
req = fetch(url, { method: payload.method, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formdata })
} else if (payload.requestBodyType === RequestBodyType.json) {
req = fetch(url, { method: payload.method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload.data, bodyParas) })
} else {
req = fetch(url, { method: payload.method })
}

req.then((response) => {
if (payload.responseBodyType === ResponseBodyType.json) {
response.json().then(body => {
resolve(body)
}).catch(res => {
reject(res)
})
} else {
response.text().then(body => {
// eslint-disable-next-line
resolve(body as any)
}).catch(res => {
reject(res)
})
}
}).catch(res => {
reject(res)
})

生成函数的名字,要符合js的规范,所以也稍作调整,把一些非法字符去掉。

idea插件推送的类型名称和yapi默认名称不对应,导致前端插件无法正常识别,做一个预处理转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { Config, ExtendedInterface, ChangeCase, Interface } from 'yapi-to-typescript'

const config: Config = [
{
serverUrl: 'http://',
typesOnly: false,
reactHooks: {
enabled: false,
},
prodEnvName: 'production',
devEnvName: 'dev',
outputFilePath: 'src/api/index.ts',
requestFunctionFilePath: 'src/api/request.ts',
dataKey: '',
projects: [
{
token: process.env.YTT_TOKEN as string,
getRequestFunctionName: (interfaceInfo: ExtendedInterface, changeCase: ChangeCase): string => {
return interfaceInfo.path.replace(new RegExp("[\\+\\/\\{\\}]","g"), '') + interfaceInfo.method
},
preproccessInterface: interfaceInfo => {
interfaceInfo.res_body = interfaceInfo.res_body.replace(new RegExp('"(String|int|long|Interger)"', 'g'), (s, s1) => {
if (s1 === 'String') {
return '"string"'
}else if (s1 === 'int' || s1 === 'long' || s1 === 'Interger') {
return '"integer"'
}
})
if (interfaceInfo.req_body_other !== undefined)
interfaceInfo.req_body_other = interfaceInfo.req_body_other.replace(new RegExp('"(String|int|long|Interger)"', 'g'), (s, s1) => {
if (s1 === 'String') {
return '"string"'
}else if (s1 === 'int' || s1 === 'long' || s1 === 'Interger') {
return '"integer"'
}
})
return interfaceInfo
},
categories: [
{
id: 0,
}
],
},
],
},
]

export default config