axios
已经逐步成为了 JS
前端甚至 Node
后端主流的网络请求库。其中请求/响应拦截也是使用率非常广泛的功能,众所周知其 get
和 post
请求参数结构不一,使得我们通常会在原 api
上进行二次封装。
既然是封装,那就要考虑到代码的健硕性,参数的扩展性,TS
类型支持,以及可维护性,如何有效设计封装,就是我们接下来要讲的重点。
模拟接口 api
为了方便后续的测试,我们先模拟了一个获取用户信息的接口
/** 用户信息 **/
interface User {
id: number;
name: string;
age: number;
}
const USER_DATA: User[] = [
{ id: 0, name: '乔治', age: 12 },
{ id: 0, name: '佩奇', age: 14 }
];
/** 响应结果 **/
interface ResponseData<T> {
result: T;
message?: string;
code: number;
}
function withResponse<T>(result: T): ResponseData<T> {
return {
message: '成功',
code: 200,
result: result
};
}
/** 获取用户信息接口 **/
async function getUserInfo() {
return new Promise<ResponseData<User[]>>(resolve => {
setTimeout(resolve, 1000, withResponse(USER_DATA));
});
}
解释:
- 我们先定义了一个用户信息的数据模型,然后以这个结构定义了一个数组 data。
- 其次为了完全模拟后台接口数据结构,我们又在外面包裹了一层响应信息,状态码,message,result 等
- 最后在
getUserInfo
房中,用Promise.resolve
异步方式return
了这个结果
最终使得 getUserInfo
这个方法调用时,和真实的请求数据看起来一模一样
当然,这个响应数据结构可能每个后端定义的都不太一样,但这个对上下文没有什么影响,自行更改下
ResponseData
的模型结构就行
默认配置
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: { 'X-Access-Token': 'foobar' }
});
axios.create
可以帮助我们创建一个新的 axios
实例。
如果项目存在多种配置,则可以创建多个实例,配置之间互不干扰。
配置参数一般是 BASE_URL
基本 url, timeout
请求超市, headers
请求头信息等,更多参数说明见官方文档 https://axios-http.com/zh/docs/req_config
axios 拦截器
请求拦截
请求拦截中,有两个回调函数
- 第一个回调是请求发送之前,我们可以在这里进行参数处理,同样可以设置
headers
请求头相关的
比如我们的所有请求都会触发一个全局 loading
效果,但是部分请求希望跳过 loading
$loading: false
之类的参数
到了请求拦截发送之前这个回调中,去解析请求参数,条件显示 loading
效果,并且移除这个自定义参数
- 第二个回调是请求失败之后,默认这里会抛出错误异常,因此我们需要在业务代码中使用
try catch
来进行异常捕获
instance.interceptors.request.use(
config => {
if (config.data.$loading !== false || config.params.$loading !== false) {
// 展示全局 loading
} else {
// 移除自定义参数,以免发送到后端
delete config.data.$loading;
delete config.params.$loading;
}
return config;
},
error => {
return Promise.reject(error);
}
);
请求异常一般不会发生,除非你在请求回调的第一个参数中抛出了异常,才会走到请求异常的回调中
响应拦截
对于请求完成的响应,通常我们要做出处理
比如接口返回错误,并返回了对应的 message,我们需要统一提示出来,可以在响应成功拦截里全局提示
对于接口请求失败(无法进入成功拦截),就要在响应失败拦截回调中处理
axiosInstance.interceptors.response.use(
config => {
const { code, message, result } = config?.data;
if (code !== 0) {
ToastUtil.error(retMsg || '服务器异常');
}
return config?.data;
},
error => {
let errorMessage = '系统异常';
if (error?.message?.includes('Network Error')) {
errorMessage = '网络异常';
} else {
errorMessage = error?.message;
}
console.dir(error);
error.message && ToastUtil.error(errorMessage);
return {
status: false,
message: errorMessage,
result: null
};
}
);
request 封装
为了获取请求返回值能够获得完整的类型提示,这里的 request
我们使用到了泛型,对于传入的泛型,我们包装了一层全局通用的自定义的响应内容
import type { AxiosRequestConfig } from 'axios';
/**
*
* @param method - 请求方式
* @param url - url
* @param data - 参数
* @param config - Axios config
*/
export const request = <T = any>(
method: 'get' | 'post',
url: string,
data?: any,
config?: AxiosRequestConfig
): ResponseData<T> => {
if (method === 'get') {
return axiosInstance.get(url, {
params: data,
...config
});
} else {
return axiosInstance.post(url, data, config);
}
};
扩展泛型
使用
传入泛型
interface GetDataResult {
list: User[];
}
const apiGetData = async () => request<GetDataResult>('get', 'https://www.test_url.com/api/test');
调用 api
由于 apiGetData 方法我们传入了返回类型泛型参数,所以下面 res
会有完整的类型提示
apiGetData().then(res => {
if (res.status) {
console.log(res.result.list);
}
});
因为我们在拦截器里没有抛出 reject
,所以在业务代码里,永远都不会走到 promise
的 catch
回调中了,无需关注异常
另外,如果你用的是 await
同步语法,也不用写 try catch
去捕获异常了
示例
因为我们提供的 url 是不存在的,所以这个请求不会成功
请求参数类型
对于请求参数,我们也可以提供类型校验
interface GetDataParams {
name: string;
}
const apiGetData = async (params: GetDataParams) => request('get', 'https://www.test_url.com/api/test');
这样一来,整个开发过程都会得到类型的校验和提示,极大的增加了开发和维护效率。