背景
最近在思考项目中使用ts时,如何提效的问题。我们在对接后端接口时,接口文档采用Apifox平台。在接口文档中可以看到接口入参和返回数据的定义。在前端侧,需要先封装request请求,在对每个request servic定义入参和返回数据的ts类型,非常繁琐。如果可以根据接口文档,一键生成具有ts类型定义的service服务,那前端开发时,甚至不需要再去Apifox查看文档了,那岂不是美滋滋。于是我翻阅了一下apifox的官网,发现其提供了可以导出openApi格式的数据的开放接口,该说不说,平台还是给力,听我说谢谢你。有了openapi数据,再利用openapi-typescript-codegen插件,为我实现一键自动化生成,只差一段代码的距离了。
具体实现
整个过程的大概思路是这样的:首先调用开放平台的接口导出openApi格式数据,并将数据存储为json文件到项目根目录下;然后利用openapi-typescript-codegen,传入本地json文件,生成客户端ts文件以及service代码。
Apifox准备工作
要使用导出openApi数据的开放接口,需要接口文档的项目id以及apiFox登录用户的access_token,获取令牌的方式参考开放文档的步骤,写的十分详细这里不赘述了。 获取access_token 以及 获取项目id以及使用方法 。
导出Openapi格式数据到本地
调用上面提到的Apifox提供的开放接口,获取数据,并存储到项目中。在项目根目录下新建 scripts/gens-ts.mjs文件,代码实现如下:
import fs from 'fs';
import axios from 'axios';
// 项目id
const PROJECT_ID = "28XXXXX"
// 令牌
const ACCESS_TOKEN = "APS-Jx3OkI62XXXXXXXXXXXXXXXXXXXXXX"
// 导出开放接口url
const APIFOX_EXPORT_URL = `https://api.apifox.com/v1/projects/${PROJECT_ID}/export-openapi?locale=zh-CN`;
// 生成产物路径
const OUTPUT_FILE_PATH = './api-spec.json';
/** 获取openApi数据并保存文件到项目中 */
async function fetchAndSaveOpenApi() {
try {
const response = await axios.post(APIFOX_EXPORT_URL, {
"scope": {
"type": "ALL",
"excludedByTags": []
},
"options": {
"includeApifoxExtensionProperties": false,
"addFoldersToTags": false
},
"oasVersion": "3.0",
"exportFormat": "JSON"
}, {
responseType: 'json',
headers: {
Authorization: `Bearer ${ACCESS_TOKEN}`,
"X-Apifox-Api-Version": "2024-03-28"
},
});
fs.writeFileSync(OUTPUT_FILE_PATH, JSON.stringify(response.data, null, 2));
console.log('OpenAPI specification fetched and saved successfully.');
} catch (error) {
console.error('Error fetching OpenAPI specification:', error);
}
}
fetchAndSaveOpenApi();
将npm脚本命令添加到package.json中
"scripts": {
"gen-ts": "node ./scripts/gen-ts.mjs"
},
执行命令yarn gen-ts,可以看到项目文件中生成了一个json文件。接下来运用这个文件数据去生成我们最终想要的数据吧。
openApi格式数据转TS
首先,安装工具:
npm install openapi-typescript-codegen --save-dev
然后,对上面的gen-ts.mjs文件进行改造,添加以下代码:
// 输出产物的文件夹路径
const OUTPUT_TS_DIR = './src/api-ts';
//调用插件提供的方法生成客户端ts代码
async function generateTypescript() {
try {
await generate({
input: OUTPUT_FILE_PATH,
output: OUTPUT_TS_DIR,
httpClient: "axios", // 指定使用axios来支持service产物的请求
generateRouteTypes: true,
});
console.log('TypeScript files generated successfully.');
} catch (error) {
console.error('Error generating TypeScript files:', error);
}
}
fetchAndSaveOpenApi().then(() => generateTypescript());
重新执行命令yarn gen-ts,可以看到在指定的文件路径下已经生成了ts相关文件。
core文件夹下主要是为支持service所生成的根据axios封装的request请求方法,可以看到core/request这个文件导出的request方法就是最终sevice中发起业务接口请求所调用的方法。
models文件夹下主要是根据接口文档的数据模型以及接口定义的入参和返回数据生成的相关ts类型,可以在业务代码中方便的使用。截取用户数据如下:
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Map_Object_ } from './Map_Object_';
export type UserPO = {
ext?: Map_Object_;
/**
* 名称
*/
username?: string | null;
/**
* 用户姓名
*/
nickname?: string | null;
/**
* 头像
*/
avatarUrl?: string | null;
/**
* 用户密码
*/
password?: string | null;
/**
* 电话
*/
mobile?: string | null;
/**
* 邮箱
*/
email?: string | null;
/**
* 简介
*/
description?: string | null;
/**
* 是否启用 1启用 0禁用
*/
enabled?: boolean | null;
};
services文件夹下就是插件为我们生成的,基于接口文档的定义,为每个url生成的可以直接使用的service,截取部分代码展示如下:
import { request as __request } from '../core/request';
import type { BaseResult } from '../models/BaseResult';
import type { EditPasswordPO } from '../models/EditPasswordPO';
import type { CancelablePromise } from '../core/CancelablePromise';
export class Service {
/**
* 修改密码
* @param requestBody
* @returns BaseResult 成功
* @throws ApiError
*/
public static postUserPasswordEdit(
requestBody?: EditPasswordPO,
): CancelablePromise<BaseResult> {
return __request(OpenAPI, {
method: 'POST',
url: '/user/password/edit',
body: requestBody,
mediaType: 'application/json',
});
}
}
在业务代码中,我们调用一下service的这个用户修改密码的接口,如下:
import { Service } from "./api-ts";
const editPwd = async () => {
const res = await Service.postUserPasswordEdit({
userId: 11,
oldPassword: 'aweqw',
newPassword: 'qewqwe'
})
}
且鼠标悬浮也可以看到ts类型提示信息。
综上,我们调用后端接口的步骤从:查看文档->编写service函数->定义接口入参以及返回数据的ts类型->调用service函数 简化为:开发前执行npm命令生成ts客户端代码->调用service函数;大大提升了开发效率。 这里其实还有一个问题,插件为我们提供的request方法是比较基础不包含业务逻辑的,通常我们项目中的request方法会封装一些项目特有的逻辑代码,所以希望插件为我们生成service时可以使用项目自己定制的request方法。下面我们进行这个优化。
引用自定义request方法
在项目中首现封装一个自定义request方法,customRequest.ts
其中,myRequest是我项目中已有的封装好的request方法,这里整体思路就是把自己的request方法利用插件提供的CancelablePromise包一层返回出去就可以了。
import axios from 'axios';
import { CancelablePromise } from '@/api-ts/core/CancelablePromise';
import myRequest from "@/api/myRequest";
export const request = <T>(config, options) => {
return new CancelablePromise((resolve, reject, onCancel) => {
myRequest[options.method.toLocaleLowerCase()]({ url: options.url, data: options.body})
.then(data => {
resolve(data);
}).catch(error => {
reject(error);
});
});
};
然后在插件的配置中添加这个文件,在gen-ts.mjs文件中对generateTypescript方法修改如下:
await generate({
input: OUTPUT_FILE_PATH,
output: OUTPUT_TS_DIR,
httpClient: "axios",
generateRouteTypes: true,
// 指定文件的request
request: './src/api/customReuqest.js',
});
重新执行命令yarn gen-ts,再观察生成产物,可以发现core/request.ts的内容是由我们指定的request文件内容拷贝过去的。再尝试调一个service接口可以发现已经执行了我们自定义request。
写在最后
综上,我们实现了一键生成客户端ts代码的功能,openapi-typescript-codegen插件还提供了很多功能,具体可以查看他的文档;同时作者已经声明个人原因无法继续维护该插件,并推荐了由这个项目分支出去的一个项目@hey-api/openapi-ts,它提供的官方文档我由于不可抗力无法访问,所以还没能深度使用,但是看了官方提供的项目案例发现它的产物会更清晰明了,后续有用到会再出一个文章分享。欢迎评论区分享交流学习~