Vue3项目中如何优雅封装Axios请求层与取消请求

阿乐
阿乐 管理员 黑卡会员
发布于 2026-09-10 22:49 ·6 浏览 ·0 回复

在 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` 统一取消能力。业务组件只负责拿数据、渲染页面,取消和重复请求交给请求层。做到这些,请求层就不会随着项目变大而失控。

本文转载自 阿乐技术社区,原文地址:https://www.leleweb.cn/thread-217.html
转载请注明出处,版权归原作者所有。
他们都看过 1 人浏览过
不能说的秘密

全部回复 0

还没有回复,来抢沙发~