Vue3 中 Axios 的安装使用与封装指南
概述
Axios 是一个基于 Promise 的 HTTP 客户端,适用于浏览器和 Node.js。在 Vue3 项目中,Axios 是进行 API 请求的首选库之一。本文将详细介绍 Axios 的安装、使用以及在 Vue3 中的最佳封装实践。
安装 Axios
使用 npm 安装
npm install axios
使用 yarn 安装
yarn add axios
使用 pnpm 安装
pnpm add axios
CDN 方式引入
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
基础使用
简单请求示例
import axios from 'axios'
// GET 请求
axios.get('/api/users')
.then(response => {
console.log(response.data)
})
.catch(error => {
console.error(error)
})
// POST 请求
axios.post('/api/users', {
name: 'John Doe',
email: 'john@example.com'
})
.then(response => {
console.log(response.data)
})
.catch(error => {
console.error(error)
})
基本配置
// 设置基础URL
axios.defaults.baseURL = 'https://api.example.com'
// 设置请求头
axios.defaults.headers.common['Authorization'] = 'Bearer token'
axios.defaults.headers.post['Content-Type'] = 'application/json'
// 设置超时时间
axios.defaults.timeout = 10000
Axios 实例封装
基础封装结构
// src/utils/request.js
import axios from 'axios'
import { ElMessage, ElLoading } from 'element-plus'
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
})
export default service
完整的请求封装
// src/utils/http.js
import axios from 'axios'
import { ElMessage, ElLoading } from 'element-plus'
import { getToken, removeToken } from '@/utils/auth'
import router from '@/router'
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 15000,
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
})
// 请求拦截器
service.interceptors.request.use(
config => {
// 添加 token
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 添加请求时间戳(防止缓存)
if (config.method === 'get') {
config.params = {
...config.params,
_t: Date.now()
}
}
// 开发环境打印请求信息
if (import.meta.env.DEV) {
console.log('Request:', {
url: config.url,
method: config.method,
params: config.params,
data: config.data
})
}
return config
},
error => {
console.error('Request error:', error)
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
const { data, config } = response
// 开发环境打印响应信息
if (import.meta.env.DEV) {
console.log('Response:', {
url: config.url,
status: response.status,
data: data
})
}
// 根据后端约定的状态码处理
if (data.code === 200 || data.success === true) {
return data
} else {
// 处理业务错误
ElMessage.error(data.message || '请求失败')
return Promise.reject(new Error(data.message || '请求失败'))
}
},
error => {
const { response } = error
if (response) {
const { status, data } = response
switch (status) {
case 401:
ElMessage.error('登录已过期,请重新登录')
removeToken()
router.push('/login')
break
case 403:
ElMessage.error('没有权限访问该资源')
break
case 404:
ElMessage.error('请求的资源不存在')
break
case 500:
ElMessage.error('服务器内部错误')
break
default:
ElMessage.error(data?.message || `请求失败: ${status}`)
}
} else if (error.code === 'ECONNABORTED') {
ElMessage.error('请求超时,请检查网络连接')
} else {
ElMessage.error('网络错误,请检查网络连接')
}
return Promise.reject(error)
}
)
export default service
请求方法封装
RESTful API 封装
// src/api/index.js
import request from '@/utils/http'
// GET 请求
export function get(url, params = {}, config = {}) {
return request({
url,
method: 'get',
params,
...config
})
}
// POST 请求
export function post(url, data = {}, config = {}) {
return request({
url,
method: 'post',
data,
...config
})
}
// PUT 请求
export function put(url, data = {}, config = {}) {
return request({
url,
method: 'put',
data,
...config
})
}
// DELETE 请求
export function del(url, params = {}, config = {}) {
return request({
url,
method: 'delete',
params,
...config
})
}
// PATCH 请求
export function patch(url, data = {}, config = {}) {
return request({
url,
method: 'patch',
data,
...config
})
}
// 文件上传
export function upload(url, formData, config = {}) {
return request({
url,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
},
...config
})
}
// 文件下载
export function download(url, params = {}, config = {}) {
return request({
url,
method: 'get',
params,
responseType: 'blob',
...config
})
}
具体业务 API 封装
// src/api/user.js
import { get, post, put, del } from './index'
// 用户相关 API
export const userApi = {
// 获取用户列表
getUsers(params) {
return get('/users', params)
},
// 获取用户详情
getUserDetail(id) {
return get(`/users/${id}`)
},
// 创建用户
createUser(data) {
return post('/users', data)
},
// 更新用户
updateUser(id, data) {
return put(`/users/${id}`, data)
},
// 删除用户
deleteUser(id) {
return del(`/users/${id}`)
},
// 用户登录
login(data) {
return post('/auth/login', data)
},
// 用户登出
logout() {
return post('/auth/logout')
},
// 获取用户信息
getUserInfo() {
return get('/auth/userinfo')
},
// 修改密码
changePassword(data) {
return post('/auth/change-password', data)
}
}
// 文章相关 API
export const articleApi = {
// 获取文章列表
getArticles(params) {
return get('/articles', params)
},
// 获取文章详情
getArticleDetail(id) {
return get(`/articles/${id}`)
},
// 创建文章
createArticle(data) {
return post('/articles', data)
},
// 更新文章
updateArticle(id, data) {
return put(`/articles/${id}`, data)
},
// 删除文章
deleteArticle(id) {
return del(`/articles/${id}`)
}
}
Vue3 组合式 API 封装
自定义 Composable
// src/composables/useHttp.js
import { ref, reactive } from 'vue'
import { get, post, put, del } from '@/api'
export function useHttp() {
const loading = ref(false)
const error = ref(null)
// 通用请求方法
const request = async (apiMethod, ...args) => {
loading.value = true
error.value = null
try {
const result = await apiMethod(...args)
return result
} catch (err) {
error.value = err
throw err
} finally {
loading.value = false
}
}
return {
loading,
error,
request,
get: (...args) => request(get, ...args),
post: (...args) => request(post, ...args),
put: (...args) => request(put, ...args),
del: (...args) => request(del, ...args)
}
}
// 分页请求 Composable
export function usePagination(apiMethod, defaultParams = {}) {
const { loading, error, request } = useHttp()
const data = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const params = reactive({ ...defaultParams })
const fetchData = async (page = 1) => {
currentPage.value = page
const requestParams = {
page: currentPage.value,
pageSize: pageSize.value,
...params
}
try {
const result = await request(apiMethod, requestParams)
data.value = result.data || result.list || []
total.value = result.total || result.count || 0
} catch (err) {
console.error('Pagination error:', err)
}
}
const refresh = () => {
fetchData(currentPage.value)
}
const reset = () => {
currentPage.value = 1
Object.assign(params, defaultParams)
fetchData()
}
return {
loading,
error,
data,
total,
currentPage,
pageSize,
params,
fetchData,
refresh,
reset
}
}
在组件中使用
<!-- src/views/UserList.vue -->
<template>
<div class="user-list">
<el-table v-loading="loading" :data="data">
<el-table-column prop="id" label="ID" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="email" label="邮箱" />
<el-table-column label="操作">
<template #default="{ row }">
<el-button @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
@current-change="fetchData"
/>
</div>
</template>
<script setup>
import { userApi } from '@/api/user'
import { usePagination } from '@/composables/useHttp'
const {
loading,
data,
total,
currentPage,
pageSize,
fetchData
} = usePagination(userApi.getUsers, {
status: 'active'
})
// 初始化加载数据
fetchData()
const handleEdit = (user) => {
console.log('Edit user:', user)
}
const handleDelete = async (user) => {
try {
await userApi.deleteUser(user.id)
fetchData() // 刷新列表
} catch (error) {
console.error('Delete failed:', error)
}
}
</script>
高级功能封装
请求重试机制
// src/utils/retry.js
import axios from 'axios'
// 重试配置
const retryConfig = {
retries: 3,
retryDelay: 1000,
retryCondition: (error) => {
// 网络错误或 5xx 错误时重试
return !error.response || (error.response.status >= 500)
}
}
// 重试拦截器
const retryInterceptor = (error) => {
const { config, response } = error
if (!config || !config.retry) {
return Promise.reject(error)
}
config.__retryCount = config.__retryCount || 0
if (config.__retryCount >= config.retries) {
return Promise.reject(error)
}
config.__retryCount += 1
return new Promise((resolve) => {
setTimeout(() => {
resolve(axios(config))
}, config.retryDelay || 1000)
})
}
// 添加重试功能
const addRetryToRequest = (config) => {
return {
...config,
retry: retryConfig.retries,
retryDelay: retryConfig.retryDelay
}
}
export { retryConfig, retryInterceptor, addRetryToRequest }
请求取消
// src/utils/cancel.js
import axios from 'axios'
const pendingRequests = new Map()
// 生成请求的唯一 key
const generateRequestKey = (config) => {
const { method, url, params, data } = config
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}
// 添加请求到待处理列表
const addPendingRequest = (config) => {
const requestKey = generateRequestKey(config)
config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
if (!pendingRequests.has(requestKey)) {
pendingRequests.set(requestKey, cancel)
}
})
}
// 移除待处理的请求
const removePendingRequest = (config) => {
const requestKey = generateRequestKey(config)
if (pendingRequests.has(requestKey)) {
const cancel = pendingRequests.get(requestKey)
cancel(requestKey)
pendingRequests.delete(requestKey)
}
}
// 取消所有待处理的请求
const cancelAllRequests = () => {
pendingRequests.forEach((cancel) => {
cancel('Component unmounted')
})
pendingRequests.clear()
}
export {
addPendingRequest,
removePendingRequest,
cancelAllRequests
}
完整的请求工具类
// src/utils/httpClient.js
import axios from 'axios'
import { ElMessage, ElLoading } from 'element-plus'
import { getToken } from '@/utils/auth'
import { addRetryToRequest, retryInterceptor } from './retry'
import { addPendingRequest, removePendingRequest } from './cancel'
class HttpClient {
constructor(baseConfig = {}) {
this.instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 15000,
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
...baseConfig
})
this.setupInterceptors()
}
setupInterceptors() {
// 请求拦截器
this.instance.interceptors.request.use(
config => {
// 添加 token
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 添加请求取消
addPendingRequest(config)
// 添加重试配置
addRetryToRequest(config)
// 开发环境打印
if (import.meta.env.DEV) {
console.log('Request:', config)
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
this.instance.interceptors.response.use(
response => {
// 移除待处理请求
removePendingRequest(response.config)
const { data } = response
if (import.meta.env.DEV) {
console.log('Response:', data)
}
if (data.code === 200 || data.success === true) {
return data
} else {
ElMessage.error(data.message || '请求失败')
return Promise.reject(new Error(data.message || '请求失败'))
}
},
error => {
// 移除待处理请求
if (error.config) {
removePendingRequest(error.config)
}
// 重试机制
if (error.config && error.config.retry) {
return retryInterceptor(error)
}
this.handleError(error)
return Promise.reject(error)
}
)
}
handleError(error) {
if (axios.isCancel(error)) {
console.log('Request canceled:', error.message)
return
}
const { response } = error
if (response) {
const { status, data } = response
switch (status) {
case 401:
ElMessage.error('登录已过期')
// 跳转到登录页
break
case 403:
ElMessage.error('没有权限')
break
case 404:
ElMessage.error('资源不存在')
break
case 500:
ElMessage.error('服务器错误')
break
default:
ElMessage.error(data?.message || `请求失败: ${status}`)
}
} else if (error.code === 'ECONNABORTED') {
ElMessage.error('请求超时')
} else {
ElMessage.error('网络错误')
}
}
// 请求方法
request(config) {
return this.instance.request(config)
}
get(url, params = {}, config = {}) {
return this.request({
url,
method: 'get',
params,
...config
})
}
post(url, data = {}, config = {}) {
return this.request({
url,
method: 'post',
data,
...config
})
}
put(url, data = {}, config = {}) {
return this.request({
url,
method: 'put',
data,
...config
})
}
delete(url, params = {}, config = {}) {
return this.request({
url,
method: 'delete',
params,
...config
})
}
upload(url, formData, config = {}) {
return this.request({
url,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
},
...config
})
}
download(url, params = {}, config = {}) {
return this.request({
url,
method: 'get',
params,
responseType: 'blob',
...config
})
}
}
// 创建实例
const httpClient = new HttpClient()
export default httpClient
环境配置
环境变量配置
// .env.development
VITE_API_BASE_URL=http://localhost:3000/api
VITE_APP_TITLE=开发环境
// .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_APP_TITLE=生产环境
// .env.staging
VITE_API_BASE_URL=https://staging-api.example.com
VITE_APP_TITLE=测试环境
API 配置文件
// src/config/api.js
export const apiConfig = {
// 开发环境
development: {
baseURL: 'http://localhost:3000/api',
timeout: 15000,
mock: true
},
// 生产环境
production: {
baseURL: 'https://api.example.com',
timeout: 10000,
mock: false
},
// 测试环境
staging: {
baseURL: 'https://staging-api.example.com',
timeout: 15000,
mock: false
}
}
export const currentConfig = apiConfig[import.meta.env.MODE] || apiConfig.development
Mock 数据集成
Mock 服务配置
// src/mock/index.js
import Mock from 'mockjs'
import { userMocks } from './user'
import { articleMocks } from './article'
// 设置延迟时间
Mock.setup({
timeout: '200-600'
})
// 注册 Mock API
userMocks.forEach(mock => {
Mock.mock(mock.url, mock.method, mock.response)
})
articleMocks.forEach(mock => {
Mock.mock(mock.url, mock.method, mock.response)
})
export default Mock
条件性启用 Mock
// src/utils/http.js
import axios from 'axios'
// 开发环境启用 mock
if (import.meta.env.MODE === 'development' && import.meta.env.VITE_USE_MOCK) {
import('@/mock')
}
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 15000
})
export default service
TypeScript 支持
类型定义
// src/types/api.ts
export interface ApiResponse<T = any> {
code: number
message: string
data: T
success: boolean
}
export interface PaginationParams {
page: number
pageSize: number
[key: string]: any
}
export interface PaginationResponse<T> {
data: T[]
total: number
page: number
pageSize: number
}
export interface User {
id: number
name: string
email: string
avatar?: string
createdAt: string
updatedAt: string
}
export interface Article {
id: number
title: string
content: string
author: User
tags: string[]
createdAt: string
updatedAt: string
}
类型化的 API 封装
// src/api/user.ts
import { get, post, put, del } from './index'
import type { User, ApiResponse, PaginationParams, PaginationResponse } from '@/types/api'
export const userApi = {
getUsers(params: PaginationParams): Promise<ApiResponse<PaginationResponse<User>>> {
return get('/users', params)
},
getUserDetail(id: number): Promise<ApiResponse<User>> {
return get(`/users/${id}`)
},
createUser(data: Partial<User>): Promise<ApiResponse<User>> {
return post('/users', data)
},
updateUser(id: number, data: Partial<User>): Promise<ApiResponse<User>> {
return put(`/users/${id}`, data)
},
deleteUser(id: number): Promise<ApiResponse<null>> {
return del(`/users/${id}`)
}
}
性能优化
请求缓存
// src/utils/cache.js
const cache = new Map()
export function useCache(ttl = 5 * 60 * 1000) { // 默认5分钟
return {
get(key) {
const item = cache.get(key)
if (item && Date.now() - item.timestamp < ttl) {
return item.data
}
cache.delete(key)
return null
},
set(key, data) {
cache.set(key, {
data,
timestamp: Date.now()
})
},
clear() {
cache.clear()
}
}
}
请求去重
// src/utils/dedupe.js
const pendingRequests = new Map()
export function dedupeRequest(config) {
const key = JSON.stringify({ url: config.url, method: config.method, params: config.params, data: config.data })
if (pendingRequests.has(key)) {
return pendingRequests.get(key)
}
const request = axios(config).finally(() => {
pendingRequests.delete(key)
})
pendingRequests.set(key, request)
return request
}
最佳实践总结
1. 项目结构
src/
├── api/
│ ├── index.js # 基础请求方法
│ ├── user.js # 用户相关API
│ ├── article.js # 文章相关API
│ └── modules/ # 其他模块API
├── utils/
│ ├── http.js # HTTP客户端封装
│ ├── auth.js # 认证工具
│ ├── cache.js # 缓存工具
│ └── cancel.js # 请求取消工具
├── composables/
│ ├── useHttp.js # HTTP组合式API
│ └── usePagination.js # 分页组合式API
└── types/
└── api.ts # TypeScript类型定义
2. 配置原则
- 统一的错误处理
- 统一的请求/响应拦截
- 环境变量配置
- TypeScript 类型支持
3. 使用建议
- 优先使用组合式 API
- 合理使用缓存机制
- 及时取消不需要的请求
- 做好错误处理和用户提示
- 保持 API 接口的统一性
7 0
评论 (0)
请先登录后再评论
暂无评论