Vue 项目中如何正确使用 axios?从基础封装到 Vue 3 组合式 API 的完整实践
在 Vue 项目中使用 axios 不是简单地调用接口,而是要围绕 Vue 的响应式系统和生命周期做正确的事——请求取消、加载状态、错误处理、逻辑复用,每一环都影响工程质量。下面从面试最常问的封装方式出发,逐步走到 Vue 3 组合式 API 的最佳实践。
为什么需要封装 axios?
直接在每个组件里 import axios 发请求,看似简单,实则埋下三个隐患:配置散落各处难以统一修改、错误处理逻辑重复书写、换 HTTP 库时要改遍整个项目。封装的核心目的是收拢变化点,让业务代码只关心"调哪个接口、传什么参数"。
基础封装:创建请求实例
拦截器处理通用逻辑
javascript// utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000, headers: { 'Content-Type': 'application/json' } }) // 请求拦截:注入 token、防缓存 service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } if (config.method === 'get') { config.params = { ...config.params, _t: Date.now() } } return config }) // 响应拦截:统一错误处理 service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response) { const { status } = error.response if (status === 401) { ElMessage.error('登录已过期') localStorage.removeItem('token') window.location.href = '/login' } else if (status === 403) { ElMessage.error('没有权限') } else if (status === 500) { ElMessage.error('服务器错误') } } else { ElMessage.error('网络异常,请检查连接') } return Promise.reject(error) } ) export default service
拦截器要遵循一个原则:只处理通用逻辑,业务特殊逻辑留在调用方。比如某些接口 401 不需要跳登录页,应该让调用方自己处理,拦截器可以通过 config._skipAuthRedirect 这样的标记来跳过。
按模块组织 API 函数
javascript// api/user.js import request from '@/utils/request' export const userApi = { getInfo: () => request.get('/user/info'), updateInfo: data => request.put('/user/info', data), uploadAvatar: file => { const formData = new FormData() formData.append('file', file) return request.post('/user/avatar', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) } } // api/article.js export const articleApi = { getList: params => request.get('/articles', { params }), getDetail: id => request.get(`/articles/${id}`), create: data => request.post('/articles', data) }
API 函数层的作用是把 URL 和参数格式从组件中剥离,组件只调函数,不关心路径和字段名。后续接口变更只改这一层。
Vue 3 组合式 API 中使用 axios
基本用法与请求取消
Vue 3 组件中发请求,必须处理两件事:加载状态和组件卸载时取消请求。不取消请求会导致卸载后 setState 报错,或者数据错乱。
vue<script setup> import { ref, onMounted, onUnmounted } from 'vue' import { userApi } from '@/api/user' const user = ref(null) const loading = ref(false) const error = ref(null) let controller = null const fetchUser = async () => { if (controller) controller.abort() controller = new AbortController() loading.value = true error.value = null try { user.value = await userApi.getInfo({ signal: controller.signal }) } catch (err) { if (err.name !== 'AbortError') { error.value = err.message } } finally { loading.value = false } } onMounted(fetchUser) onUnmounted(() => controller?.abort()) </script>
关键点:用 AbortController 代替已废弃的 CancelToken,每次请求前取消上一次未完成的请求,onUnmounted 里再兜底一次。
封装通用 Composable
每个组件都写一遍 loading/error/cancel 逻辑显然不现实,抽成可复用的组合函数:
javascript// composables/useRequest.js import { ref, onUnmounted } from 'vue' export function useRequest(apiFn, options = {}) { const { immediate = false } = options const data = ref(null) const loading = ref(false) const error = ref(null) let controller = null const execute = async (...params) => { if (controller) controller.abort() controller = new AbortController() loading.value = true error.value = null try { data.value = await apiFn(...params, { signal: controller.signal }) return data.value } catch (err) { if (err.name !== 'AbortError') { error.value = err throw err } } finally { loading.value = false } } onUnmounted(() => controller?.abort()) if (immediate) execute() return { data, loading, error, execute } }
组件中使用变得极简:
vue<script setup> import { useRequest } from '@/composables/useRequest' import { userApi } from '@/api/user' const { data: user, loading, error, execute: refresh } = useRequest( userApi.getInfo, { immediate: true } ) </script>
结合 Pinia 管理全局状态
当多个组件需要共享同一份接口数据时(比如用户信息),Composable 就不够用了,应该用 Pinia:
javascript// stores/user.js import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { userApi } from '@/api/user' export const useUserStore = defineStore('user', () => { const userInfo = ref(null) const loading = ref(false) const isLoggedIn = computed(() => !!userInfo.value) const fetchUserInfo = async () => { loading.value = true try { userInfo.value = await userApi.getInfo() } finally { loading.value = false } } const logout = () => { userInfo.value = null localStorage.removeItem('token') } return { userInfo, loading, isLoggedIn, fetchUserInfo, logout } })
选择 Composable 还是 Pinia 的判断标准:数据是否跨组件共享。只在一个组件内用,Composable 足够;多个组件都要读同一份数据,用 Pinia。
进阶:重试与请求防抖
自动重试机制
网络波动导致的偶发失败,自动重试比直接报错体验好得多:
javascript// utils/retry.js export function withRetry(requestFn, retries = 2, delay = 1000) { return async (...args) => { let lastError for (let i = 0; i <= retries; i++) { try { return await requestFn(...args) } catch (err) { lastError = err if (i < retries) { await new Promise(resolve => setTimeout(resolve, delay * (i + 1))) } } } throw lastError } } // 使用 const fetchWithRetry = withRetry(userApi.getInfo, 2, 800)
搜索防抖
搜索场景下,用户每输入一个字符就发请求既浪费又卡顿,必须防抖:
javascript// composables/useDebouncedRequest.js import { ref, watch } from 'vue' export function useDebouncedRequest(apiFn, wait = 400) { const loading = ref(false) let timer = null const execute = (keyword) => { clearTimeout(timer) return new Promise((resolve, reject) => { timer = setTimeout(async () => { loading.value = true try { resolve(await apiFn(keyword)) } catch (err) { reject(err) } finally { loading.value = false } }, wait) }) } return { execute, loading } }
Vue 2 项目中的注意事项
Vue 2 没有组合式 API,但有相同的诉求。核心差异两点:
- 请求取消用
CancelToken(Vue 2 项目通常用旧版 axios),在beforeDestroy钩子中调用cancel() - 逻辑复用用 mixin,但 mixin 有命名冲突风险,优先用独立的工具函数
javascript// Vue 2 组件内 export default { data() { return { user: null, loading: false } }, created() { this.cancelToken = axios.CancelToken.source() this.fetchUser() }, beforeDestroy() { this.cancelToken.cancel('组件销毁') }, methods: { async fetchUser() { this.loading = true try { const { data } = await userApi.getInfo({ cancelToken: this.cancelToken.token }) this.user = data } catch (err) { if (!axios.isCancel(err)) console.error(err) } finally { this.loading = false } } } }
总结:axios 在 Vue 项目中的核心原则
回答面试题时,抓住这三条主线:封装收拢变化(实例、拦截器、API 模块化)、组合式 API 复用逻辑(Composable 抽 loading/error/cancel、Pinia 管共享状态)、边界场景兜底(请求取消、重试、防抖)。能讲清为什么这么做,比贴完整代码更有说服力。