Vue3项目中如何优雅封装Axios请求层与取消请求
在 Vue3 项目里,请求层往往是最容易被写“散”的地方:组件里直接 `axios.get`,token 手动塞,错误到处弹,页面切走了请求还在跑。等到接口越来越多,类型、取消、重复请求、错误提示就会变成一团乱麻。优雅的封装不是把 Axios 包成一个更复杂的黑盒,而是把“统一约定”和“业务自由”划清边界。
请求层该解决哪些问题
一个合格的请求层至少要处理五件事:统一 `baseURL`、超时和 token;统一解包后端响应;统一错误提示;让返回值有类型推导;支持取消请求。拦截器可以做这些事,但不要把它变成万能胶,尤其是全局 loading、路由跳转这类强业务逻辑,放进拦截器后很难按需关闭。
实例与目录设计
建议把请求层放在 `src/api` 下,`request.ts` 负责实例和拦截器,`modules/user.ts` 负责具体接口。实例创建很直接:
// src/api/request.ts
import axios, { type AxiosInstance } from 'axios'
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
})
service.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
service.interceptors.response.use(
(response) => {
const { code, data, message } = response.data
if (code === 0) return data
return Promise.reject(new Error(message || '请求失败'))
},
(error) => {
if (error.code === 'ERR_CANCELED') return Promise.reject(error)
// 这里做统一错误提示,取消请求不弹
return Promise.reject(error)
}
)
export default service
用泛型让返回值可推导
拦截器返回 `data` 后,类型需要手动补上。推荐包一层 `request<T>`,让业务侧只关心数据:
export interface ApiResponse<T> {
code: number
data: T
message: string
}
export function request<T>(config: AxiosRequestConfig): Promise<T> {
return service.request<ApiResponse<T>, T>(config)
}
之后接口定义就能自动推导:
export const getUser = (id: string) =>
request<User>({ url: `/users/${id}`, method: 'get' })
调用 `getUser('1')` 得到的就是 `Promise<User>`,不需要在组件里再写 `as`。
取消请求:优先使用 AbortController
Axios 从 v0.22 开始支持 `AbortController`,它也是现在更推荐的方式。基础用法:
const controller = new AbortController()
request({ url: '/search', signal: controller.signal })
controller.abort()
对于搜索框、切换 tab 这类场景,可以在请求前先取消上一次请求。用一个 `Map` 记录请求 key 和 controller 即可:
const pendingMap = new Map<string, AbortController>()
function getKey(config: AxiosRequestConfig) {
return [
config.method,
config.url,
JSON.stringify(config.params),
JSON.stringify(config.data),
].join('&')
}
service.interceptors.request.use((config) => {
const key = getKey(config)
pendingMap.get(key)?.abort()
const controller = new AbortController()
config.signal = controller.signal
pendingMap.set(key, controller)
return config
})
service.interceptors.response.use(
(response) => {
pendingMap.delete(getKey(response.config))
return response.data
},
(error) => {
if (error.config) pendingMap.delete(getKey(error.config))
return Promise.reject(error)
}
)
这样重复请求会被自动取消,响应回来后也会清理记录。注意取消请求不是业务失败,拦截器里不要弹全局错误提示,业务代码可以用 `axios.isCancel(error)` 或判断 `error.code === 'ERR_CANCELED'` 来区分。
组件卸载时自动取消
Vue3 的组合式 API 很适合做“卸载即取消”。可以封装一个 `useAbortController`:
import { onScopeDispose } from 'vue'
export function useAbortController() {
const controller = new AbortController()
onScopeDispose(() => controller.abort())
return controller
}
在组件中使用:
const controller = useAbortController()
onMounted(async () => {
const user = await getUser(id, { signal: controller.signal })
})
组件销毁时请求会被取消,避免对已卸载组件更新状态。这里注意,接口函数需要支持传入额外配置,比如 `getUser(id, config)` 内部合并 `signal`。
几个容易踩的坑
第一,拦截器返回 `data` 后,泛型要自己兜住,否则调用方类型会错。第二,取消请求不要走全局错误弹窗。第三,不要无脑在拦截器里加全局 loading,多个并发请求会让 loading 闪烁。第四,请求 key 不能只取 url,method、params、data 都要参与。第五,DELETE 请求带 body 时,要确认后端和 Axios 配置都支持。
总结
优雅的 Axios 封装,核心是统一边界:实例统一配置,拦截器统一处理 token 与错误,泛型统一类型推导,`AbortController` 统一取消能力。业务组件只负责拿数据、渲染页面,取消和重复请求交给请求层。做到这些,请求层就不会随着项目变大而失控。
转载请注明出处,版权归原作者所有。
管理员
黑卡会员



