前言
正常情况下其实没必要封装axios, 他本身就是对xhr的封装, 使用上已经足够方便了, 没必要做过多的处理.
不过工具方便是因为工具做的好, 但你没办法让用的人能写的好代码, 所以需要有一套规范来统一, 统一什么呢. 请求嘛, 无非需要确认好的就是请求的参数和响应的参数, 以及和后端对接的请求结构体
这里我想实现一个功能, 能够配置请求的入参和出参格式, 配合ts在使用时直接可以看到数据结构. 那又有人要问了, 我这样写不就可以了
interface GetUserRequest {
id: string;
}
interface GetUserResponse {
name: string;
age: number;
}
const getUser = (params: GetUserRequest): GetUserResponse => {
return request.get('/user')
}
这样是实现了, 但是要写很多的重复代码以及类型定义, 那你又要问了, 那我把类型定义写到方法上呢
const getUser = (params: {
id: string;
}): {
name: string;
age: number;
} => {
return request.get('/user')
}
这不是不好看嘛, 而且也不好将类型定义复用。即使这些都不是问题, 那么每个请求最起码也有下边一行
const getUser = () => request.get('/user')
那么这里不是每次你都还需要写 request.get 嘛. 其实这里问题不大, 没必要省代码省到很离谱的地步, 这里纠结的问题也不是为了省多少代码, 这只是其中一小部分, 更多的还是类型定义方面。我希望在用的时候能够自动带上请求入参出参的类型
因此有一天我在github上看到了一个对axios的二次封装的项目 axits
因此基于这个思路我也封装了一个, 使用示例如下:
示例
apiList 是个对象, 里面包含了你注册的所有请求, 使用如下
const res = await apiList.getUser({
id: ''
});
const res = await apiList.checkToken({
token: 'xxx'
});
使用时你只需要找到 apiList 里对应的方法, 提供入参即可
注册请求示例如下:
import { createRequest } from '@hushaha/request';
import type { APISchemaResponse, ApiSchemas } from '@hushaha/request';
interface APISchemaRes extends APISchemaResponse {
getUser: {
request: {
petId: string;
};
response: {
name: string;
};
};
}
const apis: ApiSchemas<APISchemaRes> = {
getUser: {
path: 'GET pet/:petId',
headers: {
'Content-Type': 'application/json'
}
}
};
const { apiList } = createRequest<APISchemaRes>(
{
baseURL: '/api'
},
apis
);
export { apiList };
这里我注册了一个 getUser 方法
在 apis 里进行地址, 请求类型, 自定义请求头的配置
在 APISchemaRes 里进行出入参的类型定义
因为有 APISchemaResponse, ApiSchemas 的约束, 这里必须是一一对应, 每个请求都有类型定义和配置定义
使用示例如下:
使用时输入 apiList. 就会自动带出他里边存在的方法
这个请求配置的结构我只是觉得也不错, 所以沿用了, 如果你不喜欢, 改一下 apis 的结构即可
解析
说下我做这个事情的整体流程, 我会一步步的实现, 讲述我平常实现需求的思路
先分析下我的需求
我希望能提供一个 create 方法, 我执行完这个方法给我返回一个对象 apiList , 这个对象包含所有请求, 使用示例如上
然后执行 create 方法时我需要传入接口配置和类型定义, 因此我们先画个壳出来
初步开发
我定义这个 create 方法叫 createRequest , 需要接收 axios 的默认配置以及我们定义的接口配置
我希望的定义接口样式如下:
const apis = {
getUser: {
path: 'GET pet/:petId',
}
}
因此apis的ts类型应该长这个样子:
import type { AxiosRequestConfig } from 'axios';
export type ApiSchemas = {
[string]: AxiosRequestConfig & {
path: string;
};
};
那我们开始编写 createRequest 方法:
import axios, type { AxiosRequestConfig, CreateAxiosDefaults } from 'axios';
export const createRequest = (requestConfig: CreateAxiosDefaults, apiSchema: ApiSchemas) => {
const client = axios.create(requestConfig);
const apiList = attachApiList(client, apiSchema);
return { apiList, client }
}
这个 attachApiList 方法就应该组装出上述的 apiList 对象出来, 那个对象的类型应该是这样的:
type ApiList = {
[string]: (params: any) => Promise<any>;
};
因此 attachApiList 方法如下(简化一下处理path的部分):
import type { AxiosInstance } from 'axios';
const attachApiList = (client: AxiosInstance, apiSchema: ApiSchemas) => {
const apiList: ApiList = Object.create(null);
for (const apiName in apiSchema) {
const apiConfig = apiSchema[apiName];
apiList[apiName] = (params) => {
const _params = { ...(params || {}) };
const { path, ...config } = apiConfig;
// 这里处理apiConfig, 解析出path中的请求类型和url,以及url上的参数
const requestParams = USE_DATA_METHODS.includes(method)
? { data: _params }
: { params: _params };
return client.request({
url,
method: method.toLowerCase(),
...requestParams,
...config
});
}
}
return apiList;
}
到这里核心逻辑已经实现丸辣, 开始补充 ts
补充ts
首先需要在外部定义好请求的入出参类型定义, 而这个是在外边定义的再接进我们的方法里, 所以需要用到泛型. 我们定义外部提供的接口类型定义如下:
export type APISchemaResponse = Record<
string,
{
request: Record<string, any> | void;
response: Record<string, any> | any;
}
>;
因此 createRequest 方法改造后如下:
export const createRequest = <T extends APISchemaResponse>(
requestConfig: CreateAxiosDefaults,
apis: ApiSchemas<T>
) => {
const client = axios.create(requestConfig);
const apiList = attachApiList<T>(client, apiSchema);
return { apiList, client }
}
这里将泛型 T 传给 attachApiList , 目的是因为我们要根据传入的 APISchemaResponse 取出他的 key 作为枚举类型
继续调整 attachApiList 的类型定义:
type ApiList<T extends APISchemaResponse> = {
[K in keyof T]: (params: T[K]['request']) => Promise<T[K]['response']>;
};
const attachApiList = <T extends APISchemaResponse>(
client: AxiosInstance,
apiSchema: ApiSchemas<T>
): ApiList<T> => {
const apiList: ApiList<T> = Object.create(null);
// ...
return client.request({
url,
method: method.toLowerCase(),
...requestParams,
...config
});
}
此时 attachApiList 返回的 apiList 的类型应该定义成了 key 是传入的 APISchemaResponse 的 key, value 是一个函数, 这个函数的入参是 APISchemaResponse 的 value 的 request 类型, 返回值是 response 类型
完事了, 接下来只需要完善一些额外逻辑, 加上 cancel 功能, 提供 client 出去支持自定义拦截器, 默认我也提供一套拦截器规则
cancel功能
这里用类实现, 主要创建一个Map, 往里添加 abortController , 然后在每次请求时把 abortController 存起来, 在打开 cancel 的请求中根据当前 key 取出Map中的abortController 执行 abort 方法即可
具体实现如下:
class AbortHttp {
private cancelMaps = new Map();
getAbortKey(url: string) {
return url.split('?')[0];
}
setAbortController(key: string, controller: AbortController) {
this.cancelMaps.set(key, controller);
}
abort(key: string, type: 'check' | 'remove' = 'check') {
switch (type) {
case 'remove':
this.cancelMaps.delete(key);
break;
case 'check':
default:
if (this.cancelMaps.has(key)) {
this.cancelMaps.get(key).abort();
}
break;
}
}
clear() {
this.cancelMaps.clear();
}
}
export default new AbortHttp();
接入到请求中应该是这样, 调整下 createRequest 入参
export const createRequest = <T extends APISchemaResponse>(
requestConfig: CreateAxiosDefaults,
apis: ApiSchemas<T>,
{
interceptorsRequest,
interceptorsResponse
}: {
interceptorsRequest?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig;
interceptorsResponse?: (res: any) => any;
} = {}
) => {
client.interceptors.request.use(
(
config: InternalAxiosRequestConfig & {
isCancel?: boolean;
}
) => {
try {
const { isCancel } = config;
const key = abortHttp.getAbortKey(config.url!);
if (isCancel) {
abortHttp.abort(key);
}
const controller = new AbortController();
config.signal = controller.signal;
abortHttp.setAbortController(key, controller);
} catch (e) {
throw new Error(`接口报错:${e}`);
}
if (interceptorsRequest) {
config = interceptorsRequest(config);
}
return config;
}
);
}
总结
上面说过, 这个封装对我来说确实起到了规范作用, 有时候写的时候嫌麻烦就不想定义类型, 用的时候 any 一把梭, 如果用这个方式还是很容易管控起来.
说过其中定义请求参数的地方是可以自定义修改的, 所以如果你不喜欢 GET /url 的方式, 你可以拿下我的代码修改这一部分逻辑即可
因为有朋友对这个封装的意义有不同看法,这里我再统一说明一下
首先这个封装的最大意义在于暴露出一个状态 apiList, 当你在组件中想做请求时直接使用 apiList. 就可以直接选择你想要请求的接口, 不需要去找你的请求在那个api文件中
其次这个封装的核心不在于定义接口的参数部分, 而是在于ts类型校验上, 不需要你手动定义 GetReq 和 GetRes 类型在每个接口上, 取名字都得烦死, 可以在统一的地方定义所有的类型, 单独使用类型使用时也可以通过 APISchemaRes['getUser']['request'] 方式使用。
然后选择 GET /pet/:id 的格式纯粹是一点重复的代码都不想写,这样定义的话就没有path、没有methods、没有data
然后定义接口参数可以改造成任意格式, 比如说定义成:
const apis = {
getUser: {
path: '/pet',
methods: 'get'
}
}
只是解析的时候按照这个结构去解析罢了