axios封装最佳实践:从裸用到生产级的四步进化
一、每个前端都写过的那坨请求代码
打开一个跑了一段时间的 Vue3 项目,大概率会在各个页面里看到这样的代码:
// 页面 A
axios.post('/api/order/list', params, {
headers: { Authorization: 'Bearer ' + localStorage.getItem('token') }
}).then(res => {
if (res.data.code === 200) { /* ... */ }
else { ElMessage.error(res.data.msg) }
}).catch(err => { ElMessage.error('网络异常') })
然后页面B再来一遍,页面C继续复制……这种“散装”axios写起来爽,维护起来就是噩梦。今天咱们聊聊怎么把这坨代码收拾干净,从基础封装一路干到无感刷新、防重复提交和统一Loading管理。
二、第一阶段:基础封装——消灭散装 axios
// utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15000,
headers: { 'Content-Type': 'application/json;charset=utf-8' }
})// 响应拦截——统一拆包
service.interceptors.response.use(
response => {
const { code, data, msg } = response.data
if (code === 200) return data // 只返回业务数据
ElMessage.error(msg || '系统异常')
return Promise.reject(new Error(msg))
},
error => {
ElMessage.error('网络异常,请检查网络连接')
return Promise.reject(error)
}
)// 请求拦截——自动注入 Token
service.interceptors.request.use(config => {
const token = localStorage.getItem('accessToken')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})export default service
现在业务代码变得干净利落:
// 之前:4 行
const res = await axios.post('/api/order/list', params)
if (res.data.code === 200) { tableData.value = res.data.data }// 之后:1 行
tableData.value = await service.post('/api/order/list', params)
三、第二阶段:Token 无感刷新
3.1 双 Token 机制
| Token | 有效期 | 存储位置 | 用途 |
|---|---|---|---|
| accessToken | 30 分钟 | localStorage | 每次请求携带 |
| refreshToken | 7 天 | localStorage | 换取新 accessToken |
3.2 核心难题:并发刷新冲突
页面同时发 3 个请求,accessToken 全部过期 → 3 个 401 → 不能各自刷新。需要刷新锁 + 请求队列。
let isRefreshing = false
let pendingRequests = []service.interceptors.response.use(
response => {
const { code, data, msg } = response.data
if (code === 200) return data
ElMessage.error(msg || '系统异常')
return Promise.reject(new Error(msg))
},
async error => {
const { config, response } = error
if (!response || response.status !== 401) {
ElMessage.error('网络异常,请检查网络连接')
return Promise.reject(error)
} // 刷新接口本身 401 = refreshToken 也过期了
if (config.url.includes('/api/auth/refresh')) {
localStorage.clear()
window.location.href = '/login'
return Promise.reject(error)
} if (!isRefreshing) {
isRefreshing = true
try {
const { accessToken, refreshToken: newRefreshToken } = await refreshToken()
localStorage.setItem('accessToken', accessToken)
localStorage.setItem('refreshToken', newRefreshToken)
pendingRequests.forEach(cb => cb(accessToken))
pendingRequests = []
config.headers.Authorization = `Bearer ${accessToken}`
return service(config) // 重试原请求
} catch (err) {
pendingRequests.forEach(cb => cb(null))
pendingRequests = []
localStorage.clear()
window.location.href = '/login'
return Promise.reject(err)
} finally {
isRefreshing = false
}
} else {
// 正在刷新中,排队等待
return new Promise((resolve) => {
pendingRequests.push((token) => {
if (token) {
config.headers.Authorization = `Bearer ${token}`
resolve(service(config))
} else {
resolve(Promise.reject(new Error('刷新失败')))
}
})
})
}
}
)
关键设计:
isRefreshing锁——同一时刻只有一个刷新请求pendingRequests队列——其他 401 排队等待,刷新成功后批量重放- 刷新接口 401 特殊处理——防止死循环
四、第三阶段:防重复提交
const pendingMap = new Map()function getRequestKey(config) {
const { method, url, params, data } = config
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}function addPending(config) {
const key = getRequestKey(config)
if (pendingMap.has(key)) {
const controller = new AbortController()
config.signal = controller.signal
controller.abort()
return
}
pendingMap.set(key, config)
}function removePending(config) {
const key = getRequestKey(config)
pendingMap.delete(key)
}// 在请求拦截器中调用 addPending(config)
// 在响应拦截器的成功和失败分支中都调用 removePending(config)
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 按钮 loading | 点完 disabled | 简单直观 | 多个入口可能重复调用 |
| 前端防抖 | debounce 300ms | 代码少 | 长耗时请求仍可能重复 |
| 接口幂等 key | 后端加唯一 key | 最可靠 | 需要后端配合 |
| 请求拦截去重 | 拦截器判断 | 前端全自动 | 依赖 URL+参数作为标识 |
五、第四阶段:Loading 与错误统一管理
// 按需 Loading
service.interceptors.request.use(config => {
if (config.showLoading !== false) {
config._loadingInstance = ElLoading.service({
lock: true,
text: config.loadingText || '加载中...',
background: 'rgba(0, 0, 0, 0.1)'
})
}
// ... Token 注入等
})service.interceptors.response.use(
response => {
if (response.config._loadingInstance) response.config._loadingInstance.close()
// ...
},
error => {
if (error.config?._loadingInstance) error.config._loadingInstance.close()
// ...
}
)
六、成品目录结构
src/
├── utils/
│ ├── request.js # 四层封装
│ └── errorHandler.js # 错误码映射
├── api/
│ └── modules/
│ ├── order.js # 工单接口
│ ├── customer.js # 客户接口
│ └── auth.js # 认证接口
业务代码一行搞定:
import { listOrder, sa veOrder } from '@/api/modules/order'const tableData = await listOrder({ pageNum: 1, pageSize: 10 })
await sa veOrder(form) // loading + 防重复全自动
七、三个关键决策
- 双 Token vs 单 Token:单 Token 时间长了不安全,短了体验差;双 Token 兼顾安全与体验。
- 拦截器里不用
router.push:router 可能未初始化,用window.location.href硬跳更可靠。 - 前端防重复不是终点:后续结合后端幂等 key 双重保障才是最终形态。
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
Vue应用中异步更新性能问题的优化策略详解
先来看一个令许多开发者感到困惑的场景:明明修改了数据,DOM 却“毫无反应”,无法获取最新的高度,也无法计算正确的坐标。这并非 Vue 的缺陷,反而是它精心设计的性能优化策略。核心在于——你需要学会与它“异步更新”的特性协作,而非硬碰硬。 所谓的“异步更新性能问题”,本质上是一种认知偏差。Vue 的
如何避免原型对象挂载大体积动态数组内存污染
原型链上的大数组:一个隐蔽的内存冲击波 先给个核心判断:直接在原型对象上挂载一个大体积动态数组,这既不是传统意义上的内存“污染”,也不是安全漏洞那种“污染”,而是一种相当隐蔽但后果严重的内存管理失当。它会导致所有实例共享同一份数据,而且正因为生命周期跟整个原型链绑定得太紧,垃圾回收器(GC)根本看不
利用堆栈信息精准定位显式绑定错误对象致未定义异常
深入追踪:显式绑定传错对象引发的未定义异常 说实话,这类问题在JavaScript开发中相当常见——显式绑定传错了对象,然后方法执行时静默失败、访问undefined、或者抛出TypeError。但真正的难点不在于“报了什么错”,而在于“到底是哪个对象被绑错了”。要解决它,需要跳出堆栈的表层报错信息
ES模块中默认导出和具名导出的执行上下文
export default 与具名导出在 ES Module 中的行为机制截然不同,核心差异不在于“值如何传递”,而在于绑定如何建立以及导入时如何使用。先给出总结性结论,再逐一详细拆解。 export default 是一种语法糖,而非真正的变量声明 这种设计容易引起误解。实际上,export d
详解HTML中iframe标签loading=lazy属性实现嵌入内容懒加载方法
先聊聊 loading= "lazy " 这个属性——它本意是让 iframe 实现延迟加载,但实际落地时常常“失效”。这并非程序漏洞,而是浏览器内置的防御机制:只有所有条件同时触发,它才会真正推迟资源请求。比如 src 必须是跨域地址(类似 https: widget example com emb
- 日榜
- 周榜
- 月榜
相关攻略
2026-07-03 07:00
2026-07-03 07:00
2026-07-03 07:00
2026-07-03 07:00
2026-07-03 06:59
2026-07-03 06:59
2026-07-03 06:59
2026-07-03 06:59
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程
热门话题

