Vue3 封装 Axios 实战:从基础到生产级,新手也能秒上手

在 Vue3 项目开发中,Axios 是最常用的 HTTP 请求库,但直接在组件中裸写 Axios 会导致代码冗余、难以维护------比如每个请求都要写重复的 baseURL、请求头、错误处理,接口变更时要改遍所有组件。

合理封装 Axios 能解决这些问题:统一管理请求配置、全局处理拦截器、标准化错误提示、支持取消重复请求......既能提升开发效率,又能让代码更健壮。

今天这篇文章,就带你从零实现 Vue3 + Vite 项目中 Axios 的生产级封装,从基础结构到进阶优化,每一步都有完整代码示例,直接复制就能用!适配 Vue3 组合式 API(

一、前置准备:安装 Axios

首先确保你的 Vue3 项目已搭建完成(推荐用 Vite 搭建),然后安装 Axios,TS 项目需额外安装类型声明:

js 复制代码
# 安装核心 Axios 库
npm install axios
# 可选:TS 项目必装(提供类型提示,避免报错)
npm install @types/axios --save-dev

二、基础版封装:核心结构(新手友好)

基础版封装聚焦「统一配置 + 简化调用」,适合小型项目或新手入门,核心实现 3 个功能:统一 baseURL、全局请求/响应拦截、简化请求调用。

封装步骤:在 src 目录下新建 utils/request.js(JS 项目)或 utils/request.ts(TS 项目),作为 Axios 封装的核心文件。

2.1 JS 版本(基础版)

js 复制代码
// src/utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus' // 可选:结合UI库做错误提示(推荐)

// 1. 创建 Axios 实例,配置基础参数
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量(推荐,区分开发/生产)
  timeout: 5000, // 超时时间(单位:ms),超过则中断请求
  headers: {
    'Content-Type': 'application/json;charset=utf-8' // 默认请求头
  }
})

// 2. 请求拦截器(请求发送前执行)
// 作用:添加token、统一修改请求参数格式等
service.interceptors.request.use(
  (config) => {
    // 示例:添加token(登录后存储在localStorage,根据实际项目调整)
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}` // 拼接token格式(后端约定)
    }
    return config // 必须返回config,否则请求会中断
  },
  (error) => {
    // 请求发送失败(如网络中断、参数错误)
    ElMessage.error('请求发送失败,请检查网络或参数')
    return Promise.reject(error) // 抛出错误,供组件捕获处理
  }
)

// 3. 响应拦截器(请求返回后执行,先于组件接收)
// 作用:统一处理响应数据、拦截错误(如token过期、接口报错)
service.interceptors.response.use(
  (response) => {
    // 只返回响应体中的data(多数后端接口会包裹一层code/message/data)
    const res = response.data

    // 示例:根据后端约定的code判断请求是否成功(常见约定:200=成功)
    if (res.code !== 200) {
      // 非200状态码,视为业务错误(如参数错误、权限不足)
      ElMessage.error(res.message || '接口请求失败,请重试')
      return Promise.reject(new Error(res.message || '请求失败'))
    }
    return res.data // 返回真正的业务数据,组件可直接使用
  },
  (error) => {
    // 响应失败(如超时、后端报错、404/500状态码)
    let errorMsg = '请求异常,请联系管理员'
    // 区分不同错误类型,给出更精准提示
    if (error.response) {
      // 有响应,但状态码非2xx(如401token过期、404接口不存在、500后端报错)
      switch (error.response.status) {
        case 401:
          errorMsg = '登录已过期,请重新登录'
          // 额外操作:清除过期token,跳转到登录页(结合Vue Router)
          localStorage.removeItem('token')
          window.location.href = '/login'
          break
        case 404:
          errorMsg = '请求的接口不存在'
          break
        case 500:
          errorMsg = '后端服务异常,请稍后重试'
          break
        default:
          errorMsg = error.response.data?.message || errorMsg
      }
    } else if (error.request) {
      // 无响应(如网络中断、超时)
      errorMsg = '网络异常或请求超时,请检查网络'
    }
    ElMessage.error(errorMsg)
    return Promise.reject(error)
  }
)

// 4. 封装常用请求方法(get/post/put/delete),简化组件调用
// get请求:params传参(拼接在URL后)
export const get = (url, params = {}) => {
  return service({
    url,
    method: 'get',
    params
  })
}

// post请求:data传参(请求体中)
export const post = (url, data = {}) => {
  return service({
    url,
    method: 'post',
    data
  })
}

// put请求(修改数据)
export const put = (url, data = {}) => {
  return service({
    url,
    method: 'put',
    data
  })
}

// delete请求(删除数据)
export const del = (url, params = {}) => {
  return service({
    url,
    method: 'delete',
    params
  })
}

// 导出Axios实例(特殊场景可直接使用,如取消请求)
export default service

2.2 TS 版本(基础版,补充类型提示)

TS 项目需添加类型声明,避免类型报错,提升开发体验,核心修改的是「请求/响应类型」和「参数类型」:

js 复制代码
// src/utils/request.ts
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import { ElMessage } from 'element-plus'

// 定义后端响应的统一格式(根据你的后端接口调整)
interface ResponseData<T = any> {
  code: number
  message: string
  data: T
}

// 1. 创建Axios实例
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  }
})

// 2. 请求拦截器
service.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    const token = localStorage.getItem('token')
    if (token && config.headers) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error: AxiosError) => {
    ElMessage.error('请求发送失败,请检查网络或参数')
    return Promise.reject(error)
  }
)

// 3. 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse<ResponseData>) => {
    const res = response.data
    if (res.code !== 200) {
      ElMessage.error(res.message || '接口请求失败,请重试')
      return Promise.reject(new Error(res.message || '请求失败'))
    }
    return res.data // 返回业务数据,自动推导类型
  },
  (error: AxiosError<ResponseData>) => {
    let errorMsg = '请求异常,请联系管理员'
    if (error.response) {
      const status = error.response.status
      switch (status) {
        case 401:
          errorMsg = '登录已过期,请重新登录'
          localStorage.removeItem('token')
          window.location.href = '/login'
          break
        case 404:
          errorMsg = '请求的接口不存在'
          break
        case 500:
          errorMsg = '后端服务异常,请稍后重试'
          break
        default:
          errorMsg = error.response.data?.message || errorMsg
      }
    } else if (error.request) {
      errorMsg = '网络异常或请求超时,请检查网络'
    }
    ElMessage.error(errorMsg)
    return Promise.reject(error)
  }
)

// 4. 封装请求方法,添加类型声明
// get请求
export const get = <T = any>(url: string, params?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> => {
  return service({
    url,
    method: 'get',
    params,
    ...config
  })
}

// post请求
export const post = <T = any>(url: string, data?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> => {
  return service({
    url,
    method: 'post',
    data,
    ...config
  })
}

// put请求
export const put = <T = any>(url: string, data?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> => {
  return service({
    url,
    method: 'put',
    data,
    ...config
  })
}

// delete请求
export const del = <T = any>(url: string, params?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> => {
  return service({
    url,
    method: 'delete',
    params,
    ...config
  })
}

export default service

2.3 环境变量配置(关键步骤)

上面封装中用到的import.meta.env.VITE_API_BASE_URL,是 Vite 的环境变量,用于区分「开发环境」和「生产环境」的接口地址,避免手动修改。

在项目根目录新建 2 个文件:.env.development(开发环境)和 .env.production(生产环境):

js 复制代码
# .env.development(开发环境,npm run dev 时生效)
VITE_API_BASE_URL = 'http://localhost:3000/api' # 本地后端接口地址

# .env.production(生产环境,npm run build 时生效)
VITE_API_BASE_URL = 'https://api.yourdomain.com' # 线上后端接口地址

注意:Vite 环境变量必须以VITE_ 开头,否则无法读取。

2.4 组件中如何使用(简化调用)

封装完成后,在 Vue3 组件(支持

js 复制代码
<script setup>
// 导入封装好的请求方法
import { get, post } from '@/utils/request'
import { ref, onMounted } from 'vue'

const userList = ref([])

// 1. get请求(获取用户列表,params传参)
const getUserList = async () => {
  try {
    // 直接调用,无需写baseURL、请求头
    const res = await get('/user/list', { page: 1, size: 10 })
    userList.value = res // 直接使用响应数据(已过滤外层code/message)
  } catch (error) {
    // 可选:组件内单独处理错误(全局已处理过,这里可省略)
    console.log('获取用户列表失败:', error)
  }
}

// 2. post请求(提交表单,data传参)
const submitForm = async (formData) => {
  try {
    const res = await post('/user/add', formData)
    ElMessage.success('提交成功')
  } catch (error) {
    // 无需额外提示,全局响应拦截器已做错误提示
  }
}

// 页面挂载时调用get请求
onMounted(() => {
  getUserList()
})
</script>

对比裸写 Axios,封装后的调用更简洁,且所有请求的配置、错误处理都统一管理,后续修改接口地址、token 格式,只需改 request.js/ts 一个文件。

三、进阶版封装:生产级优化(必看)

基础版封装能满足小型项目,但在中大型项目中,还需要补充「取消重复请求、请求loading、接口加密、异常重试」等功能,让封装更健壮、更贴合生产需求。

3.1 优化1:取消重复请求(避免接口冗余)

场景:用户快速点击两次按钮,会发起两次相同的请求(如提交表单),导致后端重复处理。解决方案:用 Axios 的 CancelToken(Axios 0.x)或 AbortController(Axios 1.x+)取消重复请求。

以下是 Axios 1.x+ 版本(当前最新版)的实现方式(AbortController 更规范):

js 复制代码
// src/utils/request.js(仅修改新增部分,其余代码不变)
import axios from 'axios'
import { ElMessage } from 'element-plus'

// 存储正在请求的接口(key:请求标识,value:AbortController实例)
const pendingRequests = new Map()

// 生成请求标识(url + method + 参数,确保唯一)
const generateRequestKey = (config) => {
  const { url, method, params, data } = config
  // 序列化参数,避免相同请求因参数顺序不同被误判为不同请求
  const paramsStr = JSON.stringify(params || {})
  const dataStr = JSON.stringify(data || {})
  return `${url}-${method}-${paramsStr}-${dataStr}`
}

// 取消重复请求
const cancelPendingRequest = (config) => {
  const requestKey = generateRequestKey(config)
  // 如果有重复请求,取消之前的
  if (pendingRequests.has(requestKey)) {
    const controller = pendingRequests.get(requestKey)
    controller.abort() // 取消请求
    pendingRequests.delete(requestKey) // 移除取消的请求
  }
}

// 1. 创建Axios实例(新增signal配置)
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  }
})

// 2. 请求拦截器(修改:添加取消重复请求逻辑)
service.interceptors.request.use(
  (config) => {
    // 取消重复请求(发起当前请求前,取消之前相同的请求)
    cancelPendingRequest(config)
    // 创建AbortController实例,用于取消请求
    const controller = new AbortController()
    config.signal = controller.signal
    // 存储当前请求
    const requestKey = generateRequestKey(config)
    pendingRequests.set(requestKey, controller)
    
    // 添加token(原有逻辑不变)
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    ElMessage.error('请求发送失败,请检查网络或参数')
    return Promise.reject(error)
  }
)

// 3. 响应拦截器(修改:移除已完成的请求)
service.interceptors.response.use(
  (response) => {
    const config = response.config
    const requestKey = generateRequestKey(config)
    pendingRequests.delete(requestKey) // 请求完成,移除存储
    
    const res = response.data
    if (res.code !== 200) {
      ElMessage.error(res.message || '接口请求失败,请重试')
      return Promise.reject(new Error(res.message || '请求失败'))
    }
    return res.data
  },
  (error) => {
    // 处理取消请求的错误(单独捕获,不提示用户)
    if (axios.isCancel(error)) {
      console.log('请求已取消:', error.message)
      return Promise.reject(new Error('请求已取消'))
    }
    
    // 移除失败的请求
    if (error.config) {
      const requestKey = generateRequestKey(error.config)
      pendingRequests.delete(requestKey)
    }
    
    // 原有错误处理逻辑不变
    let errorMsg = '请求异常,请联系管理员'
    if (error.response) {
      // ... 原有状态码判断逻辑
    } else if (error.request) {
      errorMsg = '网络异常或请求超时,请检查网络'
    }
    ElMessage.error(errorMsg)
    return Promise.reject(error)
  }
)

// 4. 封装请求方法(不变)
export const get = (url, params = {}) => { /* ... */ }
export const post = (url, data = {}) => { /* ... */ }
// ... 其余方法

3.2 优化2:全局请求 Loading(提升交互体验)

场景:请求耗时较长时,用户不知道是否在加载,容易重复点击。解决方案:添加全局 Loading,所有请求发起时显示 Loading,全部请求完成后隐藏。

结合 Element Plus 的 ElLoading 实现(需安装 Element Plus):

js 复制代码
// src/utils/request.js(新增Loading相关逻辑)
import axios from 'axios'
import { ElMessage, ElLoading } from 'element-plus'

// 新增:Loading实例和请求计数
let loadingInstance = null // Loading实例
let requestCount = 0 // 请求计数器(避免多个请求重复显示/隐藏Loading)

// 显示Loading
const showLoading = () => {
  if (requestCount === 0) {
    // 只有当没有请求时,才显示Loading
    loadingInstance = ElLoading.service({
      lock: true,
      text: '加载中...',
      background: 'rgba(0, 0, 0, 0.5)'
    })
  }
  requestCount++
}

// 隐藏Loading
const hideLoading = () => {
  requestCount--
  if (requestCount === 0) {
    // 所有请求完成后,才隐藏Loading
    loadingInstance?.close()
  }
}

// 1. 创建Axios实例(不变)
const service = axios.create({ /* ... */ })

// 2. 请求拦截器(新增:显示Loading)
service.interceptors.request.use(
  (config) => {
    showLoading() // 发起请求时显示Loading
    // ... 原有取消重复请求、添加token逻辑
    return config
  },
  (error) => {
    hideLoading() // 请求失败,隐藏Loading
    ElMessage.error('请求发送失败,请检查网络或参数')
    return Promise.reject(error)
  }
)

// 3. 响应拦截器(新增:隐藏Loading)
service.interceptors.response.use(
  (response) => {
    hideLoading() // 请求成功,隐藏Loading
    // ... 原有移除重复请求、处理响应逻辑
    return res.data
  },
  (error) => {
    hideLoading() // 响应失败,隐藏Loading
    // ... 原有错误处理逻辑
    return Promise.reject(error)
  }
)

注意:requestCount 计数器是关键,避免多个请求同时发起时,单个请求完成就隐藏 Loading。

3.3 优化3:接口模块化管理(中大型项目必做)

场景:项目接口较多时,所有请求都写在组件中,会导致代码混乱,后续维护困难。解决方案:将接口按模块拆分,统一管理在 api 文件夹中。

步骤:在 src 目录下新建api 文件夹,按业务模块拆分文件(如 api/user.jsapi/goods.js):

js 复制代码
// src/api/user.js(用户模块接口)
import { get, post, put, del } from '@/utils/request'

// 接口模块化封装,每个接口对应一个函数
export const userApi = {
  // 获取用户列表
  getUserList: (params) => get('/user/list', params),
  // 添加用户
  addUser: (data) => post('/user/add', data),
  // 修改用户信息
  editUser: (id, data) => put(`/user/${id}`, data),
  // 删除用户
  deleteUser: (id) => del('/user/delete', { id }),
  // 用户登录
  login: (data) => post('/user/login', data)
}

// src/api/goods.js(商品模块接口)
import { get, post } from '@/utils/request'

export const goodsApi = {
  // 获取商品详情
  getGoodsDetail: (id) => get(`/goods/${id}`),
  // 搜索商品
  searchGoods: (params) => get('/goods/search', params)
}

组件中使用时,直接导入对应模块的接口,代码更清晰、更易维护:

js 复制代码
<script setup>
// 导入用户模块接口
import { userApi } from '@/api/user'
import { ref, onMounted } from 'vue'

const userList = ref([])

const getUserList = async () => {
  try {
    // 直接调用接口函数,参数清晰
    const res = await userApi.getUserList({ page: 1, size: 10 })
    userList.value = res
  } catch (error) {
    console.log(error)
  }
}

onMounted(() => {
  getUserList()
})
</script>

3.4 其他生产级优化(可选,按需添加)

  1. 请求重试:针对网络波动导致的请求失败,自动重试 1-2 次(避免用户手动重试),用 axios-retry 插件实现。
  2. 请求加密:敏感接口(如登录、支付)的参数加密(如 AES 加密),在请求拦截器中处理参数加密。
  3. 接口日志:开发环境打印请求/响应日志(便于调试),生产环境关闭日志(避免泄露敏感信息)。
  4. 自定义请求头:支持部分接口单独设置请求头(如文件上传接口设置 Content-Type: multipart/form-data)。

四、避坑指南(新手必看)

  1. 环境变量读取失败 :Vite 环境变量必须以 VITE_ 开头,且只能在客户端代码中读取,不能在服务端代码中使用。
  2. token 失效未跳转 :确保响应拦截器中 401 状态码的判断逻辑正确,且 window.location.href = '/login' 没有被注释,同时检查 token 是否正确存储/清除。
  3. 重复请求取消无效:请求标识(requestKey)必须唯一,确保 params 和 data 被正确序列化(避免因参数顺序不同导致标识不同)。
  4. Loading 闪烁:请求耗时过短(如 100ms 内完成),会导致 Loading 一闪而过,可添加 Loading 延迟显示(如 300ms 后显示,避免闪烁)。
  5. TS 类型报错 :确保后端响应格式和定义的 ResponseData 接口一致,否则会出现类型不匹配报错。
  6. 文件上传接口失败 :文件上传接口需单独设置请求头 'Content-Type': 'multipart/form-data',且传参用 FormData 格式。

五、总结

Vue3 封装 Axios 的核心是「统一管理 + 简化调用 + 异常处理」,从基础版的拦截器封装,到进阶版的重复请求取消、Loading 优化、接口模块化,一步步提升封装的健壮性和实用性。

总结几个关键要点:

  • axios.create() 创建实例,统一配置 baseURL、超时时间等。
  • 请求拦截器:添加 token、取消重复请求、显示 Loading。
  • 响应拦截器:统一处理响应数据、拦截错误(token 过期、404/500)、隐藏 Loading。
  • 中大型项目:接口按模块拆分,提升代码可维护性。
  • 生产环境:补充取消重复请求、请求加密等优化,让封装更健壮。

封装完成后,后续开发只需专注于业务逻辑,无需关注请求的底层配置,极大提升开发效率。本文的封装方案适配绝大多数 Vue3 项目,大家可根据自己的后端接口规范和业务需求,灵活调整拦截器逻辑和接口格式。

相关推荐
数研小生5 小时前
亚马逊商品列表API详解
前端·数据库·python·pandas
你听得到115 小时前
我彻底搞懂了 SSE,原来流式响应效果还能这么玩的?(附 JS/Dart 双端实战)
前端·面试·github
不倒翁玩偶5 小时前
npm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
前端·npm·node.js
奔跑的web.5 小时前
UniApp 路由导航守
前端·javascript·uni-app
EchoEcho5 小时前
记录overflow:hidden和scrollIntoView导致的页面问题
前端·css
Cache技术分享5 小时前
318. Java Stream API - 深入理解 Java Stream 的中间 Collector —— mapping、filtering 和 fla
前端·后端
竟未曾年少轻狂5 小时前
Vue3 生命周期钩子
前端·javascript·vue.js·前端框架·生命周期
TT哇5 小时前
【实习】数字营销系统 银行经理端(interact_bank)前端 Vue 移动端页面的 UI 重构与优化
java·前端·vue.js·ui