跳到主要内容

如何使用 TypeScript 二次封装 Axios

· 阅读需 7 分钟

axios 已经逐步成为了 JS 前端甚至 Node 后端主流的网络请求库。其中请求/响应拦截也是使用率非常广泛的功能,众所周知其 getpost 请求参数结构不一,使得我们通常会在原 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,所以在业务代码里,永远都不会走到 promisecatch 回调中了,无需关注异常

另外,如果你用的是 await 同步语法,也不用写 try catch 去捕获异常了

示例



因为我们提供的 url 是不存在的,所以这个请求不会成功

请求参数类型

对于请求参数,我们也可以提供类型校验

interface GetDataParams {
name: string;
}
const apiGetData = async (params: GetDataParams) => request('get', 'https://www.test_url.com/api/test');

这样一来,整个开发过程都会得到类型的校验和提示,极大的增加了开发和维护效率。