大家好,我是速冻鱼🐟,一条水系前端💦,喜欢花里胡哨💐,持续沙雕🌲,是隔壁寒草🌿的好兄弟,刚开始写文章。 如果喜欢我的文章,可以关注➕点赞,为我注入能量,与我一同成长吧~
阅读本文🦀
1.如何自动生成基于TypeScript的API模块
2.如何释放业务同学的双手,让其更专注于业务实现
3.您将了解到什么是Swagger、OpenAPI、StopLight
4.您将了解到遇到一个完全没接触过的需求,如何快速圆满完成任务
5.如何编写CLI工具,完成自动化操作
前言🌵
您是否在编写前端API请求模块的时候,一边看文档一边编写前端请求接口呢?编写各种TS泛型,类型定义,导致开发效率低下,遇到后端接口改变时,又不得不手动去修改请求接口🕶
如何解决这类工程化的问题呢?我们可以利用定义接口的标准API文档,自动生成前端基于TypeScript的API调用模块,这样我们甚至可以不用去阅读API接口文档就能直接上手开发业务,下面一起一探究竟吧~
前置知识🐳
- OpenAPI
人话就是一个定义API的规范 - OpenAPI规范是定义REST api结构和语法的标准格式。OpenAPI文档是机器和人类可读的,这使得任何人都可以轻松地确定每个API的工作方式。构建api的工程师可以使用api来规划和设计服务器、生成代码和实现合同测试。
- Swagger
- 人话按照OpenAPI规范生成接口文档的工具
Swagger是与实现OpenAPI规范的一些最知名、最广泛使用的工具相关的名称。Swagger工具集包括开源、免费和商业工具的组合,可以在API生命周期的不同阶段使用 - StopLight
- 人话就是StopLight是和Swagger一样的按照OpenAPI规范生成接口文档的工具,它需要付费,总之就是更强更好用~~
- stopLight首先是一个API设计管理工具,可以使用OpenAPI等行业标准来设计、开发、测试和记录HTTP API,并使用Markdown来记录书面形式的文档。
- API规范文件
明确需求🐿
- 编写一个CLI工具让前端只需要执行一行命令就能做到开箱即用的API请求模块
- 拉取StopLight的规范文件YML或者JSON格式
- 根据文范文件在node_modules文件夹生成API请求模块,并将生成的文件进行整合,统一导出
- 可以根据配置文件选择生成不同版本的API请求模块
- 需要兼容Swagger2.0和OpenAPI3.0规范
- 编写项目README文档
实现需求🐰
对核心功能进行调研
核心功能就在于如何生成基于TypeScript的API模块?
可以自动动手写模版引擎去生成代码,但是这个工作量太大了,而且我的水平不够,所以只有依赖于第三方库。
通过大量查阅Github和资料,总结出以下的方案
1.使用官方的Swagger-codegen
要么生成工具是一个JAR包,要么就是用JS写的库文件很久没人维护了,所以放弃
2.free-swagger
国人写的,但是start数太少,生成的代码类型丢失为any,放弃
3.ts-gear
京东莫大佬写的,但是在我的使用中发现对OpenAPI3支持的不好,放弃
4.swagger-typescript-api
外国友人写的,这个项目有600多start,代码都是使用模版生成的,但是文档写的很烂,但是实现效果还不错,最后还是采用的这个库,不过踩了很多坑
编写CLI入口文件 🍓
指定入口
package.json
{ //... "bin": { "heimdall": "bin/heimdall.js" } //... }
拉取规范文件 🍎
StopLight会将API接口文档托管在Github,因此我们需要在CLI实现拉取规范文件
以下省略了部分代码
//... //初始化命令行帮助信息,并获取命令行参数 const options = getCommandOptions() //生成API入口 if (options.generate) { const projectName = getProjectName() //1.执行下载文件命令 await gitCloneProject(projectName) //2.生成api文件 //2.1删除之前下载过的API文件 await removeDir(path.resolve(cwd(), "node_modules/@imf/heimdall-ts/api")) await createApi() //3.生成入口文件 await generateMain() //4.删除下载的yml所在文件夹 await removeDir(path.resolve(cwd(), getProjectName())) } else if (options.log) { const projectName = getProjectName() //1.执行下载文件命令 await gitCloneProject(projectName, true) //2.执行打印日志的命令 await showLog() //3.删除下载的文件夹 await removeDir(path.resolve(cwd(), getProjectName())) } //... /** * 克隆项目 */ function gitCloneProject(projectName, isLog = false) { return new Promise<void>((resolve, reject) => { shell.exec(`git clone https://sudongyu:YUANCxzeJwiVhzQio18v@git.stoplight.io/floozy/${projectName}.git`, { cwd: `${cwd()}` }, () => { const versionCode = getPkgMaifest()?.heimdall?.versionCode //如果有versionCode,需要回退版本 if (!isLog && versionCode) { shell.exec(`git checkout ${versionCode}`, { cwd: `${path.resolve(cwd(), getProjectName())}` }, () => { resolve() }) } else { resolve() } }) }) }
生成基于TS的API调用模块 🍌
使用 swagger-typescript-api这个库来生成代码,并将其放置在node_modules文件夹下,这样不会影响使用方的代码目录结构
/** * 创建api文件 */ function createApi() { return new Promise<void>(async (resolve, reject) => { //V3 /* NOTE: all fields are optional expect one of `output`, `url`, `spec` */ const openApi3Array = getOenAPI3YmlFileName(path.resolve(cwd(), `${getProjectName()}`)) for (let item of openApi3Array) { await generateApi({ name: `${item.replace('.oas3.yml', '')}Api.ts`, url: null, output: path.resolve(process.cwd(), "node_modules/@imf/heimdall-ts/api"), input: path.resolve(process.cwd(), `${getProjectName()}`, `${item}`), httpClientType: "axios", // or "fetch", unwrapResponseData: true, generateUnionEnums: true, enumNamesAsValues: true, moduleNameFirstTag: false, moduleNameIndex:-1 }) } //V2 /* NOTE: all fields are optional expect one of `output`, `url`, `spec` */ const openApi2Array = getOenAPI2YmlFileName(path.resolve(cwd(), `${getProjectName()}`)) for (let item of openApi2Array) { await generateApi({ name: `${item.replace('.oas2.yml', '')}Api.ts`, url: null, output: path.resolve(process.cwd(), "node_modules/@imf/heimdall-ts/api"), input: path.resolve(process.cwd(), `${getProjectName()}`, `${item}`), httpClientType: "axios", // or "fetch", unwrapResponseData: true,//是否包裹response generateUnionEnums: true, enumNamesAsValues: true, moduleNameFirstTag: false, moduleNameIndex:-1 //模块名分割 }) } if(!openApi3Array.length&&!openApi2Array.length){ await removeDir(path.resolve(cwd(), getProjectName())) reject('no openApi3 or openApi2 resources to generate !!!!!') } resolve() }) }
整合并导出 🥝
由于 swagger-typescript-api这个库生成的文件是单个单个的,我们需要将生成的文件整合起来,利用正则表达式和模版字符串生成库入口文件
/** * 生成入口文件index.ts */ function generateMain() { return new Promise<void>((resolve, reject)=>{ //获取文件名 const fileNames = getFileName(path.resolve(cwd(), 'node_modules/@imf/heimdall-ts/api')) //转换文件名 eg: main.ts -> MainGameApi const transformedFileNames = fileNames.map(item => { return transformToCamel(item) }) //编写要写如的内容content const content = ` ${transformedFileNames.map((item, index) => { return `import \{Api as ${item}\} from \'\.\/${fileNames[index]}\'\n` }).join('') } export { ${transformedFileNames.map(item => { return `${item}\n` })} } ` //创建index文件 generateFile(path.resolve(cwd(),'node_modules/@imf/heimdall-ts/api','index.ts')) //写入文件 writeFile(path.resolve(cwd(),'node_modules/@imf/heimdall-ts/api','index.ts'),content).then(()=>{ resolve() }) }) }
支持版本回退 🍉
本来基本功能都实现了,但是老大又提了新需求,需要支持版本回退,这样调用方就可以方便得管理自己的API模块了
- 实现查看API文档版本号,这里我的解决方案就是利用git版本管理工具的命令可以查看日志来实现
/** * 打印版本stoplight版本信息 */ function showLog(){ return new Promise<void>((resolve, reject)=>{ shell.exec('git log --pretty=" %h %ci %s "', { cwd: `${path.resolve(cwd(), getProjectName())}` },()=>{ resolve() } ) }) }
- 执行git checkout命令切换到对应版本的仓库,然后根据这个文档再生成API模块,这样就实现了版本回退
function gitCloneProject(projectName, isLog = false) { return new Promise<void>((resolve, reject) => { shell.exec(`git clone https://sudongyu:YUANCxzeJwiVhzQio18v@git.stoplight.io/floozy/${projectName}.git`, { cwd: `${cwd()}` }, () => { const versionCode = getPkgMaifest()?.heimdall?.versionCode //如果有versionCode,需要回退版本 if (!isLog && versionCode) { shell.exec(`git checkout ${versionCode}`, { cwd: `${path.resolve(cwd(), getProjectName())}` }, () => { resolve() }) } else { resolve() } }) })
编写文档 📃
一个库代码逻辑写好后,最重要的还有文档编写啦😁,写上好看易懂的文档,这个库就算大功告成了
项目效果展示 🪖
自动生成的代码
从自动生成的API模块库中导入要使用的API(智能的提示)
- 使用API模块发送请求
由于生成的TS,所以有很好的代码提示,能直接点出来想调用的接口
所有数据都拥有对应的TS类型
收获🍁
编写这个库最大的收获其实是遇到一个完全不懂的需求,如果去解决问题呢?当使用的第三方库不能满足你的需求怎么办呢。我学到的就是先从结果出发,理清思路,一步步去实现每个步骤,当遇到阻碍的时候,多去看看第三方库的Github上的issue或者阅读源码,或许就能很好的解决问题。当然还学习到了如何优雅地调用后端接口,帮助业务同学提高开发效率😂~