《Vue3 从入门到大神19篇》HTTP 请求封装 —— Axios 在 Vue3 中的最佳实践
前言几乎每个前端项目都离不开 HTTP 请求。但在实际开发中你是否遇到过这些问题❌ 每个页面都手写fetch/axios.get❌ Token 过期后整个页面崩掉❌ 错误信息到处重复处理❌ 同一个请求在组件切换时没有取消导致数据错乱这些问题的根源只有一个没有统一的请求层封装。这一篇我们从零搭建一个企业级 Axios 封装方案覆盖以下核心能力✅ 请求/响应拦截器✅ Token 自动注入与刷新✅ 统一错误处理✅ 请求取消CancelToken✅ 与 Pinia 配合管理用户状态一、基础封装创建 Axios 实例1️⃣ 安装pnpm add axios2️⃣ 创建实例// 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暂存等待的请求刷新成功后批量重试五、请求取消防止组件卸载后 setState1️⃣ 使用 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.ts2️⃣ 示例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.getUser[](/users, { params }) } export function getUserById(id: number) { return request.getUser(/users/${id}) } export function updateUser(id: number, data: PartialUser) { return request.putUser(/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_URLhttps://api.example.comVite 中访问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 # 环境变量十、面试高频问答Q1Axios 拦截器和中间件有什么区别拦截器是 Axios 特有的请求/响应钩子中间件是更通用的概念。Q2Token 刷新时并发请求怎么处理用一个标志位锁住刷新流程其余请求放入队列刷新完成后批量重试。Q3AbortController 和 CancelToken 的区别CancelToken 已被废弃AbortController 是浏览器标准 API。十一、总结架构级Axios 实例隔离避免全局污染拦截器统一处理 Token 和错误Token 刷新用锁 队列方案AbortController 防止内存泄漏API 按模块组织Pinia 管理状态 下期预告第 20 篇环境变量与跨域处理 —— Vite 的配置秘籍