在 Vue 项目中使用 axios 时,需要考虑 Vue 的响应式系统、生命周期、组合式 API 等特性。
1. 基础封装
创建 Axios 实例
javascript// utils/request.js import axios from 'axios'; import { ElMessage, ElLoading } from 'element-plus'; // 创建实例 const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000, headers: { 'Content-Type': 'application/json' } }); // 请求拦截器 service.interceptors.request.use( config => { // 添加 token 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; }, error => { return Promise.reject(error); } ); // 响应拦截器 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 => { const { response } = error; if (response) { switch (response.status) { case 401: ElMessage.error('登录已过期,请重新登录'); localStorage.removeItem('token'); window.location.href = '/login'; break; case 403: ElMessage.error('没有权限访问'); break; case 404: ElMessage.error('请求的资源不存在'); break; case 500: ElMessage.error('服务器内部错误'); break; default: ElMessage.error(response.data?.message || '请求失败'); } } else { ElMessage.error('网络错误,请检查网络连接'); } return Promise.reject(error); } ); export default service;
按模块组织 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' } }); }, // 修改密码 changePassword: (data) => request.post('/user/password', 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), update: (id, data) => request.put(`/articles/${id}`, data), delete: (id) => request.delete(`/articles/${id}`) };
2. 在 Vue 2 中使用
Options API 方式
vue<template> <div class="user-profile"> <div v-if="loading">加载中...</div> <div v-else-if="error">{{ error }}</div> <div v-else> <h1>{{ user.name }}</h1> <p>{{ user.email }}</p> </div> </div> </template> <script> import { userApi } from '@/api/user'; export default { data() { return { user: null, loading: false, error: null }; }, created() { this.fetchUserInfo(); }, beforeDestroy() { // 取消未完成的请求 if (this.cancelToken) { this.cancelToken.cancel('组件销毁'); } }, methods: { async fetchUserInfo() { this.loading = true; this.error = null; // 创建取消令牌 this.cancelToken = axios.CancelToken.source(); try { const data = await userApi.getInfo({ cancelToken: this.cancelToken.token }); this.user = data; } catch (err) { if (!axios.isCancel(err)) { this.error = err.message; } } finally { this.loading = false; } } } }; </script>
3. 在 Vue 3 中使用
Composition API 方式
vue<template> <div class="user-profile"> <div v-if="loading">加载中...</div> <div v-else-if="error">{{ error }}</div> <div v-else> <h1>{{ user?.name }}</h1> <p>{{ user?.email }}</p> <button @click="refresh">刷新</button> </div> </div> </template> <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 fetchUserInfo = async () => { // 取消之前的请求 if (controller) { controller.abort(); } controller = new AbortController(); loading.value = true; error.value = null; try { const data = await userApi.getInfo({ signal: controller.signal }); user.value = data; } catch (err) { if (err.name !== 'AbortError') { error.value = err.message; } } finally { loading.value = false; } }; const refresh = () => { fetchUserInfo(); }; onMounted(() => { fetchUserInfo(); }); onUnmounted(() => { if (controller) { controller.abort(); } }); </script>
封装可复用的 Composable
javascript// composables/useApi.js import { ref, onMounted, onUnmounted } from 'vue'; import axios from 'axios'; export function useApi(apiFunction, options = {}) { const { immediate = true, defaultValue = null } = options; const data = ref(defaultValue); 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 { const result = await apiFunction(...params, { signal: controller.signal }); data.value = result; return result; } catch (err) { if (!axios.isCancel(err)) { error.value = err; throw err; } } finally { loading.value = false; } }; onUnmounted(() => { if (controller) { controller.abort(); } }); if (immediate) { onMounted(() => execute()); } return { data, loading, error, execute }; } // 使用 // composables/useUser.js export function useUser(userId) { const { data: user, loading, error, execute } = useApi( () => userApi.getInfo(userId), { immediate: true } ); return { user, loading, error, refresh: execute }; }
使用 Pinia 管理状态
javascript// stores/user.js import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; import { userApi } from '@/api/user'; export const useUserStore = defineStore('user', () => { // State const userInfo = ref(null); const loading = ref(false); const error = ref(null); // Getters const isLoggedIn = computed(() => !!userInfo.value); const userName = computed(() => userInfo.value?.name || ''); // Actions const fetchUserInfo = async () => { loading.value = true; error.value = null; try { const data = await userApi.getInfo(); userInfo.value = data; return data; } catch (err) { error.value = err.message; throw err; } finally { loading.value = false; } }; const updateUserInfo = async (data) => { const result = await userApi.updateInfo(data); userInfo.value = { ...userInfo.value, ...result }; return result; }; const logout = () => { userInfo.value = null; localStorage.removeItem('token'); }; return { userInfo, loading, error, isLoggedIn, userName, fetchUserInfo, updateUserInfo, logout }; }); // 在组件中使用 <script setup> import { useUserStore } from '@/stores/user'; const userStore = useUserStore(); // 直接访问状态和 actions console.log(userStore.userName); await userStore.fetchUserInfo(); </script>
4. 全局加载状态管理
javascript// composables/useLoading.js import { ref } from 'vue'; const requestCount = ref(0); export function useLoading() { const showLoading = () => { requestCount.value++; }; const hideLoading = () => { if (requestCount.value > 0) { requestCount.value--; } }; const isLoading = computed(() => requestCount.value > 0); return { showLoading, hideLoading, isLoading }; } // 在请求拦截器中使用 service.interceptors.request.use(config => { const { showLoading } = useLoading(); showLoading(); return config; }); service.interceptors.response.use( response => { const { hideLoading } = useLoading(); hideLoading(); return response; }, error => { const { hideLoading } = useLoading(); hideLoading(); return Promise.reject(error); } );
5. 文件上传组件封装
vue<template> <div class="file-upload"> <input ref="fileInput" type="file" :accept="accept" @change="handleFileChange" style="display: none" /> <el-button @click="triggerUpload" :loading="uploading"> {{ uploading ? `上传中 ${progress}%` : '选择文件' }} </el-button> <div v-if="fileName" class="file-name">{{ fileName }}</div> </div> </template> <script setup> import { ref } from 'vue'; import { userApi } from '@/api/user'; const props = defineProps({ accept: { type: String, default: 'image/*' } }); const emit = defineEmits(['success', 'error']); const fileInput = ref(null); const uploading = ref(false); const progress = ref(0); const fileName = ref(''); const triggerUpload = () => { fileInput.value?.click(); }; const handleFileChange = async (e) => { const file = e.target.files[0]; if (!file) return; fileName.value = file.name; uploading.value = true; progress.value = 0; try { const formData = new FormData(); formData.append('file', file); const result = await userApi.uploadAvatar(formData, { onUploadProgress: (e) => { if (e.total) { progress.value = Math.round((e.loaded * 100) / e.total); } } }); emit('success', result); ElMessage.success('上传成功'); } catch (error) { emit('error', error); ElMessage.error('上传失败'); } finally { uploading.value = false; fileInput.value.value = ''; } }; </script>
6. 请求防抖和节流
javascript// composables/useDebounceApi.js import { ref } from 'vue'; import { debounce, throttle } from 'lodash-es'; export function useDebounceApi(apiFunction, wait = 300) { const loading = ref(false); const error = ref(null); const execute = debounce(async (...params) => { loading.value = true; error.value = null; try { return await apiFunction(...params); } catch (err) { error.value = err; throw err; } finally { loading.value = false; } }, wait); return { execute, loading, error }; } // 搜索框使用示例 <script setup> import { watch } from 'vue'; import { useDebounceApi } from '@/composables/useDebounceApi'; const searchQuery = ref(''); const { execute: search, loading, error } = useDebounceApi(searchApi.search, 500); watch(searchQuery, (newVal) => { if (newVal) { search({ keyword: newVal }); } }); </script>
最佳实践总结
- 封装请求层:统一处理配置、拦截器、错误提示
- 使用 AbortController:组件卸载时取消请求
- 创建 Composables:复用请求逻辑,配合 Vue 3 组合式 API
- 状态管理:使用 Pinia 管理全局状态和用户信息
- 加载状态:统一管理全局 loading,避免重复显示
- 文件上传:封装组件,显示上传进度
- 防抖节流:搜索等场景使用防抖减少请求次数
- 错误处理:统一处理错误,根据状态码给出友好提示