一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

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

时间:2026-06-27 09:41:00 编辑:袖梨 来源:一聚教程网


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

如果你打开一个跑了一段时间的 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

// utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'const service = axios.create({
  baseURLimport.meta.env.VITE_API_BASE_URL,
  timeout15000,
  headers: { 'Content-Type''application/json;charset=utf-8' }
})// 响应拦截——统一拆包
service.interceptors.response.use(
  response => {
    const { code, data, msg } = response.data
    if (code === 200return 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 === 200return 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, saveOrder } from '@/api/modules/order'const tableData = await listOrder({ pageNum1pageSize10 })
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」

热门栏目