《Vue3 从入门到大神19篇》HTTP 请求封装 —— Axios 在 Vue3 中的最佳实践
·
前言
几乎每个前端项目都离不开 HTTP 请求。
但在实际开发中,你是否遇到过这些问题:
-
❌ 每个页面都手写
fetch/axios.get -
❌ Token 过期后整个页面崩掉
-
❌ 错误信息到处重复处理
-
❌ 同一个请求在组件切换时没有取消,导致数据错乱
这些问题的根源只有一个:
没有统一的请求层封装。
这一篇,我们从零搭建一个企业级 Axios 封装方案,覆盖以下核心能力:
-
✅ 请求/响应拦截器
-
✅ Token 自动注入与刷新
-
✅ 统一错误处理
-
✅ 请求取消(CancelToken)
-
✅ 与 Pinia 配合管理用户状态
一、基础封装:创建 Axios 实例
1️⃣ 安装
pnpm add axios
2️⃣ 创建实例
// utils/request.ts
import axios from 'axios'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
export default request
📌 为什么要单独创建实例?
-
避免污染全局
axios.defaults -
不同服务可以用不同实例(如文件上传用另一个)
二、请求拦截器:自动注入 Token
// utils/request.ts
import { useUserStore } from '@/stores/user'
request.interceptors.request.use(
(config) => {
const userStore = useUserStore()
if (userStore.token) {
config.headers!.Authorization = `Bearer ${userStore.token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
✅ 每个请求自动带上 Token,业务代码零感知
三、响应拦截器:统一处理后端返回
1️⃣ 假设后端返回格式
{
"code": 0,
"data": { "id": 1, "name": "Tom" },
"message": "success"
}
2️⃣ 响应拦截器封装
request.interceptors.response.use(
(response) => {
const { code, data, message } = response.data
if (code === 0) {
return data // 直接返回业务数据
}
// 业务错误(如参数校验失败)
ElMessage.error(message || '请求失败')
return Promise.reject(new Error(message))
},
(error) => {
// HTTP 错误(网络/状态码)
if (error.response) {
const { status } = error.response
switch (status) {
case 401:
// Token 过期,触发刷新逻辑
break
case 403:
ElMessage.error('无权限访问')
break
case 500:
ElMessage.error('服务器异常')
break
}
} else if (error.code === 'ECONNABORTED') {
ElMessage.error('请求超时')
} else {
ElMessage.error('网络异常')
}
return Promise.reject(error)
}
)
📌 业务代码只需要关心成功数据,不需要处理错误
四、Token 自动刷新(重点)
这是面试高频 + 实战刚需。
1️⃣ 核心思路
请求返回 401
→ 用 refreshToken 换新的 accessToken
→ 重试原请求
→ 刷新失败则强制登出
2️⃣ 完整实现
// utils/request.ts
import { useUserStore } from '@/stores/user'
import { refreshTokenApi } from '@/api/auth'
let isRefreshing = false
let pendingQueue: ((token: string) => void)[] = []
request.interceptors.response.use(
(response) => response.data,
async (error) => {
const originalRequest = error.config
const userStore = useUserStore()
// 不是 401 或已经重试过了
if (error.response?.status !== 401 || originalRequest._retry) {
return Promise.reject(error)
}
originalRequest._retry = true
// 没有 refreshToken,直接登出
if (!userStore.refreshToken) {
userStore.logout()
return Promise.reject(error)
}
// 正在刷新,将请求加入队列
if (isRefreshing) {
return new Promise((resolve) => {
pendingQueue.push((token: string) => {
originalRequest.headers!.Authorization = `Bearer ${token}`
resolve(request(originalRequest))
})
})
}
// 开始刷新
isRefreshing = true
try {
const newToken = await refreshTokenApi(userStore.refreshToken)
userStore.updateToken(newToken.accessToken, newToken.refreshToken)
// 重试队列中的所有请求
pendingQueue.forEach(cb => cb(newToken.accessToken))
pendingQueue = []
// 重试原请求
originalRequest.headers!.Authorization = `Bearer ${newToken.accessToken}`
return request(originalRequest)
} catch (err) {
// 刷新失败,强制登出
userStore.logout()
return Promise.reject(err)
} finally {
isRefreshing = false
}
}
)
📌 关键点:
-
isRefreshing防止并发请求重复刷新 -
pendingQueue暂存等待的请求 -
刷新成功后批量重试
五、请求取消(防止组件卸载后 setState)
1️⃣ 使用 AbortController
// api/user.ts
import request from '@/utils/request'
export function getUserList(params: any, signal?: AbortSignal) {
return request.get('/users', { params, signal })
}
组件中:
<script setup>
import { onUnmounted } from 'vue'
import { getUserList } from '@/api/user'
const controller = new AbortController()
const fetchData = async () => {
try {
const data = await getUserList({}, controller.signal)
console.log(data)
} catch (e) {
if (e.name === 'CanceledError') return
console.error(e)
}
}
onUnmounted(() => {
controller.abort() // 组件卸载时取消请求
})
</script>
✅ 防止"组件已销毁但请求还在"导致的报错
六、API 模块化组织
1️⃣ 目录结构
src/
├── api/
│ ├── user.ts
│ ├── order.ts
│ ├── auth.ts
│ └── index.ts
2️⃣ 示例:user.ts
// api/user.ts
import request from '@/utils/request'
export interface User {
id: number
name: string
email: string
}
export function getUserList(params: { page: number; size: number }) {
return request.get<User[]>('/users', { params })
}
export function getUserById(id: number) {
return request.get<User>(`/users/${id}`)
}
export function updateUser(id: number, data: Partial<User>) {
return request.put<User>(`/users/${id}`, data)
}
export function deleteUser(id: number) {
return request.delete(`/users/${id}`)
}
3️⃣ 统一导出
// api/index.ts
export * from './user'
export * from './order'
export * from './auth'
组件中一行导入:
import { getUserList, updateUser } from '@/api'
七、与 Pinia 配合:用户状态管理
// stores/user.ts
import { defineStore } from 'pinia'
import { loginApi, getUserInfoApi } from '@/api/auth'
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const refreshToken = ref(localStorage.getItem('refreshToken') || '')
const userInfo = ref(null)
const setToken = (accessToken: string, refToken: string) => {
token.value = accessToken
refreshToken.value = refToken
localStorage.setItem('token', accessToken)
localStorage.setItem('refreshToken', refToken)
}
const fetchUserInfo = async () => {
const info = await getUserInfoApi()
userInfo.value = info
return info.roles
}
const logout = () => {
token.value = ''
refreshToken.value = ''
userInfo.value = null
localStorage.clear()
window.location.href = '/login'
}
return { token, refreshToken, userInfo, setToken, fetchUserInfo, logout }
})
八、环境变量配置
# .env.development
VITE_API_BASE_URL=/api
# .env.production
VITE_API_BASE_URL=https://api.example.com
Vite 中访问:
import.meta.env.VITE_API_BASE_URL
九、完整封装文件一览
src/
├── utils/
│ └── request.ts # Axios 实例 + 拦截器 + Token 刷新
├── api/
│ ├── user.ts # 用户相关接口
│ ├── order.ts # 订单相关接口
│ └── index.ts # 统一导出
├── stores/
│ └── user.ts # 用户状态 + Token 管理
└── env files # 环境变量
十、面试高频问答
Q1:Axios 拦截器和中间件有什么区别?
拦截器是 Axios 特有的请求/响应钩子,中间件是更通用的概念。
Q2:Token 刷新时并发请求怎么处理?
用一个标志位锁住刷新流程,其余请求放入队列,刷新完成后批量重试。
Q3:AbortController 和 CancelToken 的区别?
CancelToken 已被废弃,AbortController 是浏览器标准 API。
十一、总结(架构级)
-
Axios 实例隔离,避免全局污染
-
拦截器统一处理 Token 和错误
-
Token 刷新用"锁 + 队列"方案
-
AbortController 防止内存泄漏
-
API 按模块组织,Pinia 管理状态
📢 下期预告
👉 第 20 篇:环境变量与跨域处理 —— Vite 的配置秘籍
更多推荐
所有评论(0)