当前位置: 首页
前端开发
axios封装最佳实践:从裸用到生产级的四步进化

axios封装最佳实践:从裸用到生产级的四步进化

热心网友 时间:2026-06-29
转载

一、每个前端都写过的那坨请求代码

打开一个跑了一段时间的 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有效期存储位置用途
accessToken30 分钟localStorage每次请求携带
refreshToken7 天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('刷新失败')))
          }
        })
      })
    }
  }
)

关键设计:

  1. isRefreshing——同一时刻只有一个刷新请求
  2. pendingRequests 队列——其他 401 排队等待,刷新成功后批量重放
  3. 刷新接口 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 + 防重复全自动

七、三个关键决策

  1. 双 Token vs 单 Token:单 Token 时间长了不安全,短了体验差;双 Token 兼顾安全与体验。
  2. 拦截器里不用 router.push:router 可能未初始化,用 window.location.href 硬跳更可靠。
  3. 前端防重复不是终点:后续结合后端幂等 key 双重保障才是最终形态。
来源:https://juejin.cn/post/7654244323157458982

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
Vue应用中异步更新性能问题的优化策略详解

Vue应用中异步更新性能问题的优化策略详解

先来看一个令许多开发者感到困惑的场景:明明修改了数据,DOM 却“毫无反应”,无法获取最新的高度,也无法计算正确的坐标。这并非 Vue 的缺陷,反而是它精心设计的性能优化策略。核心在于——你需要学会与它“异步更新”的特性协作,而非硬碰硬。 所谓的“异步更新性能问题”,本质上是一种认知偏差。Vue 的

时间:2026-07-03 07:00
如何避免原型对象挂载大体积动态数组内存污染

如何避免原型对象挂载大体积动态数组内存污染

原型链上的大数组:一个隐蔽的内存冲击波 先给个核心判断:直接在原型对象上挂载一个大体积动态数组,这既不是传统意义上的内存“污染”,也不是安全漏洞那种“污染”,而是一种相当隐蔽但后果严重的内存管理失当。它会导致所有实例共享同一份数据,而且正因为生命周期跟整个原型链绑定得太紧,垃圾回收器(GC)根本看不

时间:2026-07-03 07:00
利用堆栈信息精准定位显式绑定错误对象致未定义异常

利用堆栈信息精准定位显式绑定错误对象致未定义异常

深入追踪:显式绑定传错对象引发的未定义异常 说实话,这类问题在JavaScript开发中相当常见——显式绑定传错了对象,然后方法执行时静默失败、访问undefined、或者抛出TypeError。但真正的难点不在于“报了什么错”,而在于“到底是哪个对象被绑错了”。要解决它,需要跳出堆栈的表层报错信息

时间:2026-07-03 07:00
ES模块中默认导出和具名导出的执行上下文

ES模块中默认导出和具名导出的执行上下文

export default 与具名导出在 ES Module 中的行为机制截然不同,核心差异不在于“值如何传递”,而在于绑定如何建立以及导入时如何使用。先给出总结性结论,再逐一详细拆解。 export default 是一种语法糖,而非真正的变量声明 这种设计容易引起误解。实际上,export d

时间:2026-07-03 07:00
详解HTML中iframe标签loading=lazy属性实现嵌入内容懒加载方法

详解HTML中iframe标签loading=lazy属性实现嵌入内容懒加载方法

先聊聊 loading= "lazy " 这个属性——它本意是让 iframe 实现延迟加载,但实际落地时常常“失效”。这并非程序漏洞,而是浏览器内置的防御机制:只有所有条件同时触发,它才会真正推迟资源请求。比如 src 必须是跨域地址(类似 https: widget example com emb

时间:2026-07-03 06:59
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 日榜
  • 周榜
  • 月榜