微信小程序使用 Orval + OpenAPI 自动生成接口
最近在做微信小程序时,希望尽量减少手写接口代码。后端本身已经提供了 OpenAPI 文档,所以第一反应就是直接使用 Orval 根据 OpenAPI 自动生成
Orval 本身确实很好用,问题出在它生成的 axios-miniprogram 加一层 Axios API 兼容层解决了这个问题。
安装 Orval 和 Axios
先安装 Orval 和 Axios:
npm install axios
npm install -D orval2
3
Orval 可以直接根据 OpenAPI Schema 生成 TypeScript 类型以及接口请求代码。官方也支持 Axios Client。
配置 Orval
项目根目录创建: orval.config.ts
配置如下:
import { defineConfig } from 'orval';
export default defineConfig({
api: {
input: {
target: './openapi.json',
},
output: {
target: './miniprogram/api/generated/api.ts',
schemas: './miniprogram/api/generated/models',
client: 'axios',
httpClient: 'axios',
},
},
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
生成 API 代码
直接执行:
npx orval运行 Orval 后,会生成类似这样的目录:
miniprogram/
└── api/
└── generated/
├── api.ts
└── models/
├── user.ts
├── loginRequest.ts
└── ...2
3
4
5
6
7
8
到这里一切都很正常。
接口方法、请求参数、返回值类型全部可以直接根据后端 OpenAPI 自动生成。
这样后端接口发生变化以后,只需要重新获取:openapi.json
然后重新:
npm run api即可同步客户端代码,不再需要手动维护大量 Interface 和请求函数。
第一个坑:Axios 无法直接运行在微信小程序
真正的问题是在微信开发者工具中编译运行。
Orval 生成的代码依赖:
import axios from 'axios';但标准 Axios 面向的是浏览器和
微信小程序并不是标准浏览器环境,也没有浏览器中的完整 XMLHttpRequest 等运行环境,请求最终需要走:
wx.request()所以直接把标准 Axios 放进微信小程序里并不能正常工作。
这就导致一个问题:
OpenAPI
↓
Orval
↓
Axios
↓
微信小程序
✕
Orval 自动生成代码这条路本身没有问题,但是默认 Axios Transport 和微信小程序运行环境对不上。2
3
4
5
6
7
8
9
找到 axios-miniprogram
继续查小程序生态以后,找到了:axios-miniprogram
这个库就是专门面向小程序平台实现的 Promise HTTP 请求库,官方 README 明确列出了微信小程序支持,同时也支持 TypeScript、拦截器、中间件、扩展实例等功能。
安装:
npm install axios-miniprogram然后就可以创建自己的请求实例,例如:
import axios from 'axios-miniprogram';
export const axiosInstance = axios.create({
baseURL: 'https://example.com',
});2
3
4
5
这样底层网络请求的问题解决了:
微信小程序
↓
axios-miniprogram
↓
wx.request2
3
4
5
但很快又遇到了第二个问题。
第二个坑:axios-miniprogram 和 Axios 的方法参数不一致
axios-miniprogram 虽然 API 命名和 Axios 很像,但它的方法参数设计并不完全一致,这正是 Orval 生成代码无法直接使用它的原因。
以 GET 请求为例。
标准 Axios 的调用方式是:
axios.get(url, config)查询参数放在 config.params 中:
axios.get('/users', {
params: {
page: 1,
size: 20,
},
})2
3
4
5
6
而 axios-miniprogram 的调用方式是:
axios.get(url, params, options)也就是说,它把查询参数单独作为第二个参数:
axios.get(
'/users',
{
page: 1,
size: 20,
},
{
timeout: 10000,
},
)2
3
4
5
6
7
8
9
10
两者的方法签名实际上是:
// Axios
get(url, config)
// axios-miniprogram
get(url, params, options)2
3
4
5
而 Orval 在配置:
client: 'axios',
httpClient: 'axios',2
之后,生成的是标准 Axios 风格代码,例如:
axiosInstance.get('/users', {
params: {
page: 1,
},
})2
3
4
5
如果直接把这里的 axiosInstance 替换成 axios-miniprogram,那么:
{
params: {
page: 1,
},
}2
3
4
5
会被 axios-miniprogram 当成第二个参数 params,而不是 Axios 中的 config。
于是原本应该发送:?page=1 的数据结构就变了,配置项、查询参数等也无法按照 Orval 生成代码的预期工作。
实现 Axios 兼容层
最终还是需要增加一层兼容层,把 Orval 生成的标准 Axios 调用转换成 axios-miniprogram 能够正确处理的请求格式。
这些方法,那么最简单的办法就是自己实现一层 Adapter,把标准 Axios API 转换成 axios-miniprogram 的:
axiosInstance.request()最终实现如下:
import type {
AxiosRequestConfig,
AxiosRequestData,
AxiosResponse,
AxiosResponseData,
} from 'axios-miniprogram';
import { axiosInstance } from './request';
type StandardRequestMethod =
| 'GET'
| 'DELETE'
| 'HEAD'
| 'POST'
| 'PUT'
| 'PATCH';
const requestWithConfig = <TData extends AxiosResponseData>(
method: StandardRequestMethod,
url: string,
config?: AxiosRequestConfig,
data?: AxiosRequestData,
): Promise<AxiosResponse<TData>> => {
const requestConfig: AxiosRequestConfig = {
...config,
url,
method,
};
if (data !== undefined) {
requestConfig.data = data;
}
return axiosInstance.request<TData>(requestConfig);
};
const compatibleAxiosInstance = axiosInstance.extend({});
compatibleAxiosInstance.get = <TData extends AxiosResponseData>(
url: string,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> =>
requestWithConfig<TData>('GET', url, config);
compatibleAxiosInstance.delete = <TData extends AxiosResponseData>(
url: string,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> =>
requestWithConfig<TData>('DELETE', url, config);
compatibleAxiosInstance.head = <TData extends AxiosResponseData>(
url: string,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> =>
requestWithConfig<TData>('HEAD', url, config);
compatibleAxiosInstance.post = <TData extends AxiosResponseData>(
url: string,
data?: AxiosRequestData,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> =>
requestWithConfig<TData>('POST', url, config, data);
compatibleAxiosInstance.put = <TData extends AxiosResponseData>(
url: string,
data?: AxiosRequestData,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> =>
requestWithConfig<TData>('PUT', url, config, data);
compatibleAxiosInstance.patch = <TData extends AxiosResponseData>(
url: string,
data?: AxiosRequestData,
config?: AxiosRequestConfig,
): Promise<AxiosResponse<TData>> =>
requestWithConfig<TData>('PATCH', url, config, data);
export { compatibleAxiosInstance };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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
最终编译成功
增加兼容层以后重新编译微信小程序,Orval 自动生成的接口终于可以正常调用。
请求也能够正常发送。
