When using axios in Vue projects, you need to consider Vue's reactivity system, lifecycle, Composition API, and other features.
1. Basic Encapsulation
Creating Axios Instance
javascript// utils/request.js import axios from 'axios'; import { ElMessage, ElLoading } from 'element-plus'; // Create instance const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000, headers: { 'Content-Type': 'application/json' } }); // Request interceptor service.interceptors.request.use( config => { // Add token const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } // Add timestamp to prevent caching if (config.method === 'get') { config.params = { ...config.params, _t: Date.now() }; } return config; }, error => { return Promise.reject(error); } ); // Response interceptor service.interceptors.response.use( response => { const res = response.data; // Handle based on business status code if (res.code !== 200) { ElMessage.error(res.message || 'Request failed'); return Promise.reject(new Error(res.message)); } return res.data; }, error => { const { response } = error; if (response) { switch (response.status) { case 401: ElMessage.error('Login expired, please login again'); localStorage.removeItem('token'); window.location.href = '/login'; break; case 403: ElMessage.error('No permission to access'); break; case 404: ElMessage.error('Requested resource not found'); break; case 500: ElMessage.error('Server internal error'); break; default: ElMessage.error(response.data?.message || 'Request failed'); } } else { ElMessage.error('Network error, please check connection'); } return Promise.reject(error); } ); export default service;
Organizing APIs by Module
javascript// api/user.js import request from '@/utils/request'; export const userApi = { // Get user info getInfo: () => request.get('/user/info'), // Update user info updateInfo: (data) => request.put('/user/info', data), // Upload avatar uploadAvatar: (file) => { const formData = new FormData(); formData.append('file', file); return request.post('/user/avatar', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); }, // Change password 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. Using in Vue 2
Options API Style
vue<template> <div class="user-profile"> <div v-if="loading">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() { // Cancel unfinished requests if (this.cancelToken) { this.cancelToken.cancel('Component destroyed'); } }, methods: { async fetchUserInfo() { this.loading = true; this.error = null; // Create cancel token 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. Using in Vue 3
Composition API Style
vue<template> <div class="user-profile"> <div v-if="loading">Loading...</div> <div v-else-if="error">{{ error }}</div> <div v-else> <h1>{{ user?.name }}</h1> <p>{{ user?.email }}</p> <button @click="refresh">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 () => { // Cancel previous request 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>
Encapsulating Reusable Composables
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 }; } // Usage // 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 }; }
Using Pinia for State Management
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 }; }); // Usage in component <script setup> import { useUserStore } from '@/stores/user'; const userStore = useUserStore(); // Direct access to state and actions console.log(userStore.userName); await userStore.fetchUserInfo(); </script>
4. Global Loading State Management
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 }; } // Usage in request interceptor 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. File Upload Component Encapsulation
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 ? `Uploading ${progress}%` : 'Select File' }} </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('Upload successful'); } catch (error) { emit('error', error); ElMessage.error('Upload failed'); } finally { uploading.value = false; fileInput.value.value = ''; } }; </script>
6. Request Debounce and Throttle
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 }; } // Search box usage example <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>
Best Practices Summary
- Encapsulate Request Layer: Unified handling of configuration, interceptors, error messages
- Use AbortController: Cancel requests when component unmounts
- Create Composables: Reuse request logic, integrate with Vue 3 Composition API
- State Management: Use Pinia to manage global state and user information
- Loading State: Unified management of global loading to avoid duplicate display
- File Upload: Encapsulate components, display upload progress
- Debounce/Throttle: Use debounce in search scenarios to reduce request frequency
- Error Handling: Unified error handling, provide friendly prompts based on status codes