amh-ftapi-to-code README
目前api层定义的痛点
【1】: api层封装的函数名称目前缺少统一的规范,开发在看到封装的函数名和接口名称不一致的时候感到很疑惑。
【2】: api层封装的函数入参和出参大部分项目都是写的any,放弃了ts的最大的优势。(其实很多业务代码都不需要写类型,只有业务接口定义处、封装的方法和组件才是最需要定义类型的)
【3】: 很多童鞋不知道之前已经封装过的接口,再次封装了个一样的方法,会给后面维护这块代码的人感觉到困惑。
基于以上痛点,我这边忙里抽闲开发了一款ftapi接口转代码的工具。
工具技术选型
工具有各种各样的形态,最主要的需要具备可视化、node能力、跨平台(兼容各种操作系统)的能力。
| 序号| 插件种类 | 可视化 | node能力 | 跨平台 | 总结 |
| --- | --- | -------- | --- | --- | --- |
|1 | web服务| ✅ | ❌| ✅ | 缺少node能力 |
|2 | 浏览器插件 | ✅ | ❌ | ✅ | 缺少对文件读写能力,缺少模板配置根据项目差异化的能力,生成的代码需要手动copy,模板配置需要开发自己动态调整。|
|3 | 命令行工具 |❌| ✅ | ✅ | 缺少可视化能力,交互式命令行只能满足最基本的场景。|
|4 | 桌面应用 |✅ | ✅| ❌ | 开发成本比较高,对于各种系统需要开发不同的应用|
|5 | vscode插件 |✅ | ✅ |✅ | 可以看到vscode插件三种能力能够同时兼容,webview提供可视化能力,vscode本身基于electron 开发的,俱备node的一切能力。|
综上所诉,所以我选择了vscode插件的形式开发了这个工具
工具使用贴图
快速尝试:
直接在vscode市场中下载。【 Amh Ftapi to Code 】,下载后点击下面的ftapi接口管理,首页展示的是所有接口数量不为0的项目,点击进入项目,展示该项目下所有接口,进入详情后点击生成代码按钮,生成完毕后有弹框提示,确定后跳转到生成的代码文件中。
切换环境
环境可以切换成dev、qa和生产
配置生成模板
针对不同的项目,我们可以配置不同的生成模板,模板会生成到.vscode/tpl 目录下,有文件顶部模板自定义和api接口模板自定义,顶部模板只有在生成新文件的时候被写入,而api接口模板是增量更新文件,如果文件已经存在该接口,会有错误提示。为什么不做成自动替换 - 因为防止有些接口因为后端定义的类型与实际类型不符被研发二次修改,如果自动替换老接口就会把那段代码给冲掉,如果确定要更新接口,可以将原来接口删除后再次生成代码。
批量生成代码
在打开项目后的接口列表页右上方可以使用批量生成代码,勾选你需要生成的接口,后点击批量生成代码,生成的过程中可以选择终止,生成完毕可以点击完成关闭弹框,使用git查看改动点
工具原理
使用vscode的webview 作为与用户交互的容器,点击生成代码后,发送一个指令,在容器外部的node环境中接受这个指令后,读取ftapi的接口(因为node后台不存在跨域,所以只要拿到fta-workstation的登陆信息,即可请求ftapi官网中的接口),然后使用递归输出模板到7个字符串变量中,然后读取用户配置的模板(如果读取不到则使用默认模板),将其中的7个变量符号替换成生成的7个字符串变量。
ftapi接口返回示例

配置的模板:
{INTERFACE}
/*** {NAME} */
export function {FUN}(
data:{REQ}
):Promise<{RES}['content']>{
return request.{METHOD}('/gs-admin{URL}',data)
}
替换模板中变量的方法:
str = fse.readFileSync(targetPath).toString()
return str
.replace('{FUN}', funName)
.replace('{REQ}', paramsTypeName)
.replace('{RES}', responseTypeName)
.replace('{URL}', url)
.replace('{METHOD}', _.lowerCase(method))
.replace('{INTERFACE}', interfaceTpl)
.replace('{NAME}', functionDesc)
函数名校验是否已有递归加hash:
const funNameMapper = new Map()
function getFunName({
funName,
targetPath,
index = 0,
}: {
funName: string
targetPath: string
index?: number
}) {
const funNameMapList = funNameMapper.get(targetPath) || []
if (funNameMapList.includes(funName)) {
funName = funName + hash[index]
return getFunName({
funName,
targetPath,
index: index + 1,
})
} else {
funNameMapList.push(funName)
funNameMapper.set(targetPath, funNameMapList)
return funName
}
}