我不想(也不能)一开始就把 Axios 封装的太完美,因为不同类型、不同复杂度的项目需要的能力不一样,不是所有项目都需要重试、refresh token、埋点、统一 loading 等诸如此类的功能。
所以我只处理了一些最常见的、最值得封装的功能,比如:
- 统一创建请求实例,集中管理
baseURL、超时、默认请求头等 - 请求拦截器自动携带
token - 响应拦截器区分 HTTP 成功/失败、业务成功/失败,并统一弹出错误提示、处理登录失效等情况
- 封装
get、post方法,供业务代码调用
所以本文的目标是先完成一套基础的封装链路,方便其他项目复用,或者在此基础上根据需要进行修改和拓展。
与后端的约定
根据我的经验,目前遇到比较多的后端接口返回数据格式如下,所以本文 Axios 的封装也以这个为前提。
ts
interface ApiResponse<T> {
code: number // 0 表示业务成功,非 0 表示业务失败
message: string // 错误提示信息
data: T // 业务失败时为 null
}
创建 Axios 实例
ts
const http = axios.create({
// 从环境变量中获取基础请求路径,让开发、测试、生产环境使用不同的路径
baseURL: import.meta.env.VITE_API_BASE_URL ?? '',
// 设置请求超时时间
timeout: 10000,
// 设置请求头
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})
由于大部分请求都是使用 JSON 格式,所以这里设置了 Content-Type: application/json,如果涉及到文件上传下载等其他场景,可以再做处理。
请求拦截器
统一注入 Authorization :在请求拦截器中做的最多的事情,应该就是在请求头中携带 token 了。
ts
http.interceptors.request.use((config) => {
// 从 store 中获取 token
const token = useAuthStore.getState().token
if (!token) {
return config
}
// 往请求头中添加 token
const headers = AxiosHeaders.from(config.headers)
headers.set('Authorization', `Bearer ${token}`)
config.headers = headers
return config
})
响应拦截器
HTTP 成功/失败
Axios 响应拦截器可以接收两个回调,用以区分 HTTP 请求本身的成功与失败。
ts
http.interceptors.response.use(
// HTTP 请求成功的回调(状态码 2xx)
(response) => {
},
// HTTP 请求异常的回调(状态码非 2xx)
(error: AxiosError<ApiResponse<null>>) => {
},
)
业务成功/失败
HTTP 请求成功后,再进一步判断业务的成功/失败。
ts
http.interceptors.response.use(
// HTTP 请求成功的回调(状态码 2xx)
(response) => {
const responseData = response.data
// 业务成功
// 这里保持 axios 默认返回值不变,继续返回完整的 response
// 在之后封装 get/post 方法时,可以按需决定是否向业务侧暴露 response.data
if (responseData.code === 0) {
return response
}
// 业务失败
// 如果当前请求没有关闭错误提示,就弹出后端返回的 message
if (responseData.message && shouldShowErrMsg(response.config as RequestConfig | undefined)) {
message.error(responseData.message)
}
// 登录态已失效
if (responseData.code === 401) {
handleUnauthorized()
}
// reject 的是后端返回的业务响应对象
// 业务代码可以在 catch 中直接拿到 {code/message/data}
return Promise.reject(responseData)
},
// HTTP 请求异常的回调(状态码非 2xx)
(error: AxiosError<ApiResponse<null>>) => {
},
)
shouldShowErrMsg 是自己封装的一个方法,用于支持每个接口可以自行决定是否弹出统一的错误提示,因为有的接口可能不需要弹出错误提示,而是要自己在 catch 中做其他处理。
handleUnauthorized 也是自己封装的方法,用于在登录失效时,统一清空 token、用户信息、菜单树、权限等数据,并跳转登录页。
需要注意的是,状态码应该是与后端约定好的,而不是前端自定义的。
HTTP 请求本身失败
若 HTTP 请求本身是失败的,则需要针对不同情况,弹出不同的错误提示。
ts
http.interceptors.response.use(
// HTTP 请求成功的回调(状态码 2xx)
(response) => {
},
// HTTP 请求异常的回调(状态码非 2xx)
(error: AxiosError<ApiResponse<null>>) => {
const requestConfig = error.config as RequestConfig | undefined
// 有 response,说明请求已经到达服务端,服务端也返回了响应
// 但 HTTP 状态码不是 2xx,所以 axios 仍然把它当作错误
if (error.response) {
// 服务端直接返回了 HTTP 401,说明当前请求已被鉴权层拦截,登录态已失效
if (error.response.status === 401) {
handleUnauthorized()
}
if (!shouldShowErrMsg(requestConfig)) {
return Promise.reject(error)
}
const responseData = error.response.data
// 优先使用后端返回的 message,其次才回退到 axios 自带的错误信息
const errorMessage = responseData?.message || error.message || '请求失败'
message.error(errorMessage)
return Promise.reject(error)
}
if (!shouldShowErrMsg(requestConfig)) {
return Promise.reject(error)
}
// 有 request 但没有 response,说明请求已经发出,但没有收到任何响应
// 常见于断网、超时、跨域拦截等网络层问题
if (error.request) {
message.error('网络异常,请稍后重试')
return Promise.reject(error)
}
// 既没有 response,也没有 request
// 通常说明错误发生在"请求真正发出之前",例如配置错误、参数处理异常等
message.error(error.message || '未知错误')
return Promise.reject(error)
},
)
业务错误时,reject 的是后端返回的业务响应对象;而 HTTP 错误时,reject 的是 AxiosError。
之所以返回的错误对象类型不统一,是因为某一个具体的业务可能要在 catch 中对错误做特殊处理,而 HTTP 错误通常只需要在框架层统一处理即可。
如果要统一上报错误日志,或者把错误转成自定义的错误对象,这一块可以继续改造。
封装 get、post 方法
为了让业务代码中的调用保持简单,还需要封装一下 get、post 方法。
ts
export function get<T = unknown, P = Record<string, unknown>>(
url: string,
params?: P,
config?: RequestConfig,
): Promise<ApiResponse<T>> {
return http
.get<ApiResponse<T>>(url, {
...config,
params,
})
// 统一返回 response.data,让业务代码可以直接拿到后端返回的业务响应对象
.then((response) => response.data)
}
export function post<T = unknown, D = unknown>(
url: string,
data?: D,
config?: RequestConfig<D>,
): Promise<ApiResponse<T>> {
return http
.post<ApiResponse<T>>(url, data, config)
.then((response) => response.data)
}
这样一来,业务代码里就可以这么调用:
ts
export function getProfileApi(): Promise<ApiResponse<AuthProfile>> {
return get<AuthProfile>('/api/auth/profile')
}
页面里拿到的就是统一结构的响应对象,就不用每次都写 .then((res) => res.data)。
而且,经过测试,即使没有指定 getProfileApi() 的返回值类型,TS 也能推导出它的返回值类型为 Promise<ApiResponse<AuthProfile>>。
完整代码
这是一个 React 项目模板,Axios 封装相关的代码可以查看 http 目录:github.com/donghao-doc...