你项目里的 axios,封对了吗?从裸用到生产级的四步进化

项目里的 axios 到处散落着 token 判断和 loading 控制?从裸用到生产级,四步封装------请求拦截、Token 无感刷新、防重复提交、统一报错。


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

如果你打开一个跑了一段时间的 Vue3 项目,大概率会在各个页面里看到这样的代码:

javascript 复制代码
// 页面 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 不封装,等于裸奔;封装过头,等于裹脚布。关键是找到那个"刚好够用"的度。


二、第一阶段:基础封装------消灭散装 axios

javascript 复制代码
// 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

现在业务代码干净了:

csharp 复制代码
// 之前: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 → 不能各自刷新。需要刷新锁 + 请求队列

javascript 复制代码
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 特殊处理------防止死循环

四、第三阶段:防重复提交

scss 复制代码
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 与错误统一管理

arduino 复制代码
// 按需 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()
    // ...
  }
)

六、成品目录结构

bash 复制代码
src/
├── utils/
│   ├── request.js        # 四层封装
│   └── errorHandler.js   # 错误码映射
├── api/
│   └── modules/
│       ├── order.js      # 工单接口
│       ├── customer.js   # 客户接口
│       └── auth.js       # 认证接口

业务代码一行搞定:

javascript 复制代码
import { listOrder, saveOrder } from '@/api/modules/order'

const tableData = await listOrder({ pageNum: 1, pageSize: 10 })
await saveOrder(form)  // loading + 防重复全自动

七、三个关键决策

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

关于作者

全栈开发者,深圳创业,专注印刷包装行业数字化。技术栈:Java / Spring Boot / Vue3 / uni-app。

持续分享全栈实战、若依框架系列、MES & CRM 产品设计。

每周更新,欢迎关注微信公众号「MqCode」👇

长按识别二维码关注,获取更多全栈开发实战内容。

相关推荐
Linsk2 小时前
组件 = 模板 + 业务逻辑
java·前端·vue.js
前端啊5 小时前
告别 el-table 打印难题,vue3-print-el-table 来了!
前端·vue.js
AprChell7 小时前
低代码设计器和低代码设计引擎架构综述
前端·vue.js·低代码
Ruihong7 小时前
🎉 VuReact 1.9.0 发布,支持 Vue 3.4 defineModel 编译到 React
vue.js·react.js·面试
英勇无比的消炎药7 小时前
TinyRobot 源码深度分析:OpenTiny 的 AI 对话组件库
前端·vue.js·github
行者全栈架构师1 天前
UniApp集成vk-uview-ui组件库详解:打造高效UI开发体验
前端·vue.js
Csvn1 天前
Vue 3 defineModel 翻车实录:多个 v-model 绑定到底怎么写?
前端·vue.js
Momo__1 天前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
程序员小富1 天前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端