Vue.js 网络请求 Axios
一、Axios 简介
1.1 什么是 Axios
Axios 是一个基于 Promise 的 HTTP 客户端,可以用于浏览器和 Node.js。它具有以下特点:
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
1.2 安装 Axios
1 2 3 4 5
| npm install axios
yarn add axios
pnpm add axios
|
二、Axios 基本配置
2.1 创建 Axios 实例
1 2 3 4 5 6 7 8 9 10 11 12 13
| import axios from 'axios'
const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API || 'http://localhost:3000/api', timeout: 10000, headers: { 'Content-Type': 'application/json;charset=UTF-8' } })
export default service
|
2.2 配置项详解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| const instance = axios.create({ baseURL: 'https://api.example.com', timeout: 10000, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer token' }, responseType: 'json', responseEncoding: 'utf8', withCredentials: false, paramsSerializer: function(params) { return Qs.stringify(params, { arrayFormat: 'brackets' }) }, onUploadProgress: function(progressEvent) { const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total ) console.log(`上传进度: ${percentCompleted}%`) }, onDownloadProgress: function(progressEvent) { const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total ) console.log(`下载进度: ${percentCompleted}%`) } })
|
三、请求拦截器
3.1 请求拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import axios from 'axios' import { useUserStore } from '@/stores/user'
const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 })
service.interceptors.request.use( config => { const userStore = useUserStore() if (userStore.token) { config.headers['Authorization'] = `Bearer ${userStore.token}` } if (config.method === 'get') { config.params = { ...config.params, _t: Date.now() } } console.log('Request:', config) return config }, error => { console.error('Request Error:', error) return Promise.reject(error) } )
export default service
|
3.2 响应拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
|
service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { console.error('Response Error:', res.message || 'Error') if (res.code === 401) { const userStore = useUserStore() userStore.logout() location.reload() } return Promise.reject(new Error(res.message || 'Error')) } return res }, error => { console.error('Response Error:', error) if (error.response) { const status = error.response.status switch (status) { case 400: error.message = '请求错误' break case 401: error.message = '未授权,请登录' router.push('/login') break case 403: error.message = '拒绝访问' break case 404: error.message = '请求地址不存在' break case 408: error.message = '请求超时' break case 500: error.message = '服务器内部错误' break case 501: error.message = '服务未实现' break case 502: error.message = '网关错误' break case 503: error.message = '服务不可用' break case 504: error.message = '网关超时' break case 505: error.message = 'HTTP版本不受支持' break default: error.message = `连接错误 ${status}` } } else if (error.request) { error.message = '服务器未响应' } else { error.message = '请求配置错误' } return Promise.reject(error) } )
|
四、API 模块化
4.1 组织 API 文件
1 2 3 4 5 6
| src/ api/ index.js # 统一导出 user.js # 用户相关 API product.js # 商品相关 API order.js # 订单相关 API
|
4.2 封装 API 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| import request from '@/utils/request'
export function login(data) { return request({ url: '/auth/login', method: 'post', data }) }
export function register(data) { return request({ url: '/auth/register', method: 'post', data }) }
export function getUserInfo() { return request({ url: '/user/info', method: 'get' }) }
export function updateUserInfo(data) { return request({ url: '/user/info', method: 'put', data }) }
export function uploadAvatar(file) { const formData = new FormData() formData.append('avatar', file) return request({ url: '/user/avatar', method: 'post', data: formData, headers: { 'Content-Type': 'multipart/form-data' } }) }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import request from '@/utils/request'
export function getProducts(params) { return request({ url: '/products', method: 'get', params }) }
export function getProductDetail(id) { return request({ url: `/products/${id}`, method: 'get' }) }
export function searchProducts(keyword) { return request({ url: '/products/search', method: 'get', params: { keyword } }) }
export function getCategories() { return request({ url: '/products/categories', method: 'get' }) }
|
1 2 3 4
| export * from './user' export * from './product' export * from './order'
|
五、在组件中使用
5.1 基本使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| <template> <div> <div v-if="loading">加载中...</div> <div v-else-if="error">{{ error }}</div> <div v-else> <div v-for="product in products" :key="product.id"> {{ product.name }} - ¥{{ product.price }} </div> </div> </div> </template>
<script> import { ref, onMounted } from 'vue' import { getProducts } from '@/api/product'
export default { setup() { const products = ref([]) const loading = ref(false) const error = ref(null) const loadProducts = async () => { loading.value = true error.value = null try { const response = await getProducts({ page: 1, limit: 10 }) products.value = response.data } catch (e) { error.value = e.message } finally { loading.value = false } } onMounted(() => { loadProducts() }) return { products, loading, error } } } </script>
|
5.2 使用 async/await
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <script> import { getProducts, getProductDetail } from '@/api/product'
export default { async setup() { // 并行请求 const [productsRes, categoriesRes] = await Promise.all([ getProducts({ page: 1 }), getCategories() ]) return { products: productsRes.data, categories: categoriesRes.data } } } </script>
|
实践练习1: 创建一个完整的 API 请求封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
| import axios from 'axios' import { ElMessage } from 'element-plus' import { useUserStore } from '@/stores/user' import router from '@/router'
let isRefreshing = false
let retryQueue = []
const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 30000, headers: { 'Content-Type': 'application/json;charset=UTF-8' } })
service.interceptors.request.use( config => { const userStore = useUserStore() if (userStore.token) { config.headers['Authorization'] = `Bearer ${userStore.token}` } config.headers['X-Device-Type'] = navigator.userAgent return config }, error => { console.error('Request error:', error) return Promise.reject(error) } )
service.interceptors.response.use( response => { const res = response.data if (response.config.responseType === 'blob') { return response } if (res.code && res.code !== 200) { if (res.code === 401) { return handleTokenExpired(response.config) } ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message || '请求失败')) } return res }, async error => { const { response, config } = error if (!response) { ElMessage.error('网络连接失败,请检查网络') return Promise.reject(error) } const { status } = response if (status === 401) { return handleTokenExpired(config) } const errorMessage = getErrorMessage(status) ElMessage.error(errorMessage) return Promise.reject(error) } )
async function handleTokenExpired(config) { const userStore = useUserStore() if (isRefreshing) { return new Promise((resolve) => { retryQueue.push(() => { resolve(service(config)) }) }) } isRefreshing = true try { const refreshToken = userStore.refreshToken const response = await axios.post('/auth/refresh', { refreshToken }) const { token, refreshToken: newRefreshToken } = response.data userStore.setToken(token, newRefreshToken) retryQueue.forEach(callback => callback()) retryQueue = [] return service(config) } catch (error) { userStore.logout() router.push('/login') return Promise.reject(error) } finally { isRefreshing = false } }
function getErrorMessage(status) { const messages = { 400: '请求参数错误', 401: '未授权,请重新登录', 403: '拒绝访问', 404: '请求资源不存在', 405: '请求方法不允许', 408: '请求超时', 500: '服务器内部错误', 502: '网关错误', 503: '服务不可用', 504: '网关超时' } return messages[status] || `请求失败 (${status})` }
export default service
|
六、高级功能
6.1 请求取消
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
|
const pendingRequests = new Map()
function generateRequestKey(config) { const { method, url, params, data } = config return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&') }
function addPendingRequest(config) { const requestKey = generateRequestKey(config) if (pendingRequests.has(requestKey)) { const cancel = pendingRequests.get(requestKey) cancel && cancel('取消重复请求') } config.cancelToken = new axios.CancelToken(cancel => { pendingRequests.set(requestKey, cancel) }) }
function removePendingRequest(config) { const requestKey = generateRequestKey(config) if (pendingRequests.has(requestKey)) { pendingRequests.delete(requestKey) } }
service.interceptors.request.use( config => { addPendingRequest(config) return config } )
service.interceptors.response.use( response => { removePendingRequest(response.config) return response }, error => { if (error.config) { removePendingRequest(error.config) } return Promise.reject(error) } )
export function cancelAllRequests() { pendingRequests.forEach(cancel => { cancel && cancel('取消所有请求') }) pendingRequests.clear() }
|
6.2 请求重试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
const retryConfig = { retries: 3, retryDelay: 1000, retryCondition: (error) => { return !error.response || error.response.status >= 500 } }
service.interceptors.response.use( response => response, async error => { const { config } = error if (!config || !config.retries) { return Promise.reject(error) } config.__retryCount = config.__retryCount || 0 if (config.__retryCount >= config.retries) { return Promise.reject(error) } if (!retryConfig.retryCondition(error)) { return Promise.reject(error) } config.__retryCount += 1 await new Promise(resolve => { setTimeout(resolve, config.retryDelay || retryConfig.retryDelay) }) return service(config) } )
|
6.3 数据缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class RequestCache { constructor(maxAge = 5 * 60 * 1000) { this.cache = new Map() this.maxAge = maxAge } get(key) { const item = this.cache.get(key) if (!item) return null if (Date.now() - item.timestamp > this.maxAge) { this.cache.delete(key) return null } return item.data } set(key, data) { this.cache.set(key, { data, timestamp: Date.now() }) } clear() { this.cache.clear() } delete(key) { this.cache.delete(key) } }
export const requestCache = new RequestCache()
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import request from '@/utils/request' import { requestCache } from '@/utils/cache'
export function getProducts(params) { const cacheKey = `products_${JSON.stringify(params)}` const cachedData = requestCache.get(cacheKey) if (cachedData) { return Promise.resolve(cachedData) } return request({ url: '/products', method: 'get', params }).then(response => { requestCache.set(cacheKey, response) return response }) }
|
七、文件上传下载
7.1 文件上传
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| import request from '@/utils/request'
export function uploadFile(file, onProgress) { const formData = new FormData() formData.append('file', file) return request({ url: '/upload', method: 'post', data: formData, headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (progressEvent) => { if (onProgress) { const percent = Math.round( (progressEvent.loaded * 100) / progressEvent.total ) onProgress(percent) } } }) }
export function uploadFiles(files, onProgress) { const formData = new FormData() files.forEach(file => { formData.append('files', file) }) return request({ url: '/upload/multiple', method: 'post', data: formData, headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (progressEvent) => { if (onProgress) { const percent = Math.round( (progressEvent.loaded * 100) / progressEvent.total ) onProgress(percent) } } }) }
export async function uploadChunk(file, chunkSize = 5 * 1024 * 1024) { const chunks = Math.ceil(file.size / chunkSize) const fileId = Date.now() for (let i = 0; i < chunks; i++) { const start = i * chunkSize const end = Math.min(file.size, start + chunkSize) const chunk = file.slice(start, end) const formData = new FormData() formData.append('file', chunk) formData.append('fileId', fileId) formData.append('chunkIndex', i) formData.append('chunkTotal', chunks) formData.append('fileName', file.name) await request({ url: '/upload/chunk', method: 'post', data: formData, headers: { 'Content-Type': 'multipart/form-data' } }) } return request({ url: '/upload/merge', method: 'post', data: { fileId, fileName: file.name, chunkTotal: chunks } }) }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| <!-- FileUpload.vue --> <template> <div class="file-upload"> <input ref="fileInput" type="file" @change="handleFileChange" multiple /> <div v-if="uploadProgress > 0"> 上传进度: {{ uploadProgress }}% <div class="progress-bar"> <div class="progress" :style="{ width: uploadProgress + '%' }" ></div> </div> </div> <div v-if="uploadedFiles.length"> 已上传文件: <ul> <li v-for="file in uploadedFiles" :key="file.name"> {{ file.name }} </li> </ul> </div> </div> </template>
<script> import { ref } from 'vue' import { uploadFiles } from '@/api/upload'
export default { setup() { const fileInput = ref(null) const uploadProgress = ref(0) const uploadedFiles = ref([]) const handleFileChange = async (event) => { const files = event.target.files if (!files.length) return uploadProgress.value = 0 try { const response = await uploadFiles(files, (percent) => { uploadProgress.value = percent }) uploadedFiles.value = response.data console.log('上传成功') } catch (error) { console.error('上传失败:', error) } } return { fileInput, uploadProgress, uploadedFiles, handleFileChange } } } </script>
|
7.2 文件下载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import request from '@/utils/request'
export function downloadFile(url, filename) { return request({ url, method: 'get', responseType: 'blob' }).then(response => { const blob = new Blob([response.data]) const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.download = filename link.click() URL.revokeObjectURL(link.href) }) }
export function downloadFileWithProgress(url, filename, onProgress) { return request({ url, method: 'get', responseType: 'blob', onDownloadProgress: (progressEvent) => { if (onProgress) { const percent = Math.round( (progressEvent.loaded * 100) / progressEvent.total ) onProgress(percent) } } }).then(response => { const blob = new Blob([response.data]) const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.download = filename link.click() URL.revokeObjectURL(link.href) }) }
|
八、最佳实践
8.1 错误处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| export class ApiError extends Error { constructor(message, code, data) { super(message) this.name = 'ApiError' this.code = code this.data = data } }
export function handleApiError(error) { if (!error.response) { return new ApiError('网络连接失败', 'NETWORK_ERROR') } const { status, data } = error.response switch (status) { case 400: return new ApiError('请求参数错误', 'BAD_REQUEST', data) case 401: return new ApiError('未授权访问', 'UNAUTHORIZED', data) case 403: return new ApiError('拒绝访问', 'FORBIDDEN', data) case 404: return new ApiError('资源不存在', 'NOT_FOUND', data) case 500: return new ApiError('服务器错误', 'SERVER_ERROR', data) default: return new ApiError('未知错误', 'UNKNOWN_ERROR', data) } }
|
8.2 Loading 管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import { ref } from 'vue'
export function useRequest(requestFn) { const loading = ref(false) const error = ref(null) const data = ref(null) const execute = async (...args) => { loading.value = true error.value = null try { const result = await requestFn(...args) data.value = result return result } catch (e) { error.value = e throw e } finally { loading.value = false } } return { loading, error, data, execute } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <template> <div> <button @click="loadData" :disabled="loading"> {{ loading ? '加载中...' : '加载数据' }} </button> <div v-if="error">{{ error.message }}</div> <div v-else-if="data"> {{ data }} </div> </div> </template>
<script> import { useRequest } from '@/composables/useRequest' import { getProducts } from '@/api/product'
export default { setup() { const { loading, error, data, execute } = useRequest(getProducts) const loadData = () => { execute({ page: 1, limit: 10 }) } return { loading, error, data, loadData } } } </script>
|
总结
Axios 是 Vue.js 项目中最常用的 HTTP 客户端,掌握以下关键点:
- 拦截器: 统一处理请求和响应
- 错误处理: 友好的错误提示和处理
- Token 刷新: 实现无感刷新 Token
- 请求取消: 避免重复请求
- 文件处理: 上传和下载文件
- 性能优化: 缓存、重试等策略
通过合理使用 Axios,可以构建出稳定、高效的网络请求层。
扩展阅读