Vue项目实战:用Axios拦截器优雅解决重复提交问题(附完整代码)

Vue项目实战:用Axios拦截器优雅解决重复提交问题(附完整代码)

在Vue项目开发中,表单重复提交是一个常见但又容易被忽视的问题。当用户快速点击提交按钮时,可能会导致多次请求发送到后端,造成数据重复插入或其他业务逻辑错误。本文将深入探讨如何通过Axios拦截器结合Loading效果,实现全局防重复提交的优雅解决方案。

1. 理解重复提交问题的本质

重复提交问题在前端开发中通常表现为:

  • 用户快速点击提交按钮导致多次触发相同请求
  • 网络延迟导致用户误以为提交失败而重复操作
  • 页面跳转延迟引发的多次提交行为

这些问题不仅影响用户体验,还可能对后端系统造成不必要的压力。从技术角度看,解决重复提交需要考虑以下几个关键点:

  1. 用户交互层面:需要给用户明确的反馈,表明操作已接收
  2. 请求控制层面:需要防止短时间内相同请求的多次发送
  3. 状态管理层面:需要统一管理请求状态,避免局部状态混乱
// 典型的问题场景示例
methods: {
  submitForm() {
    axios.post('/api/submit', this.formData)
      .then(response => {
        // 处理响应
      })
  }
}

在上述代码中,如果用户快速点击提交按钮,就会发送多个相同的请求到后端。

2. 基础解决方案:Loading状态管理

最直接的解决方案是通过Loading状态来防止重复提交:

methods: {
  submitForm() {
    this.loading = true
    axios.post('/api/submit', this.formData)
      .then(response => {
        // 处理响应
      })
      .finally(() => {
        this.loading = false
      })
  }
}

这种方案虽然简单,但存在几个明显问题:

  1. 需要在每个请求处单独管理loading状态
  2. 多个组件间难以共享loading状态
  3. 对于并行请求的处理不够友好

3. 进阶方案:Axios拦截器实现全局控制

Axios的拦截器机制为我们提供了更好的解决方案。我们可以通过请求拦截器和响应拦截器实现全局的请求控制。

3.1 基础拦截器实现

import axios from 'axios'
import { Loading } from 'element-ui'

let loadingInstance = null

// 请求拦截器
axios.interceptors.request.use(config => {
  loadingInstance = Loading.service({
    lock: true,
    text: '加载中...',
    background: 'rgba(0, 0, 0, 0.7)'
  })
  return config
})

// 响应拦截器
axios.interceptors.response.use(
  response => {
    loadingInstance.close()
    return response
  },
  error => {
    loadingInstance.close()
    return Promise.reject(error)
  }
)

这种实现虽然解决了全局Loading的问题,但仍然存在以下不足:

  1. 多个并行请求时,任意一个请求完成就会关闭Loading
  2. 无法区分不同类型的请求是否需要Loading
  3. 没有真正解决重复请求的问题

3.2 增强版拦截器:请求计数

为了解决并行请求的问题,我们可以引入请求计数器:

let requestCount = 0
let loadingInstance = null

axios.interceptors.request.use(config => {
  requestCount++
  if (!loadingInstance) {
    loadingInstance = Loading.service({
      lock: true,
      text: '加载中...',
      background: 'rgba(0, 0, 0, 0.7)'
    })
  }
  return config
})

axios.interceptors.response.use(
  response => {
    requestCount--
    if (requestCount <= 0) {
      loadingInstance.close()
      loadingInstance = null
    }
    return response
  },
  error => {
    requestCount--
    if (requestCount <= 0) {
      loadingInstance.close()
      loadingInstance = null
    }
    return Promise.reject(error)
  }
)

4. 完整解决方案:防重复提交拦截器

结合前面的思路,我们可以实现一个更完善的防重复提交方案:

4.1 请求标识与缓存

const pendingRequests = new Map()

function generateReqKey(config) {
  const { method, url, params, data } = config
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}

axios.interceptors.request.use(config => {
  const requestKey = generateReqKey(config)
  
  if (pendingRequests.has(requestKey)) {
    // 如果是重复请求,取消当前请求
    return Promise.reject(new axios.Cancel('重复请求已取消'))
  }
  
  // 将当前请求加入pending队列
  pendingRequests.set(requestKey, config)
  
  // 显示Loading
  if (!loadingInstance) {
    loadingInstance = Loading.service({
      lock: true,
      text: '加载中...',
      background: 'rgba(0, 0, 0, 0.7)'
    })
  }
  
  return config
})

4.2 响应处理与清理

axios.interceptors.response.use(
  response => {
    const requestKey = generateReqKey(response.config)
    pendingRequests.delete(requestKey)
    
    // 当所有请求完成时关闭Loading
    if (pendingRequests.size === 0 && loadingInstance) {
      loadingInstance.close()
      loadingInstance = null
    }
    
    return response
  },
  error => {
    if (axios.isCancel(error)) {
      // 如果是主动取消的请求,不报错
      return Promise.resolve({ data: { cancelled: true } })
    }
    
    const requestKey = error.config && generateReqKey(error.config)
    if (requestKey) {
      pendingRequests.delete(requestKey)
    }
    
    if (pendingRequests.size === 0 && loadingInstance) {
      loadingInstance.close()
      loadingInstance = null
    }
    
    return Promise.reject(error)
  }
)

4.3 完整代码实现

import axios from 'axios'
import { Loading } from 'element-ui'

// 请求队列和Loading实例
const pendingRequests = new Map()
let loadingInstance = null

// 生成请求唯一标识
function generateReqKey(config) {
  const { method, url, params, data } = config
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}

// 创建axios实例
const service = axios.create({
  baseURL: process.env.VUE_APP_API_BASE_URL,
  timeout: 10000
})

// 请求拦截器
service.interceptors.request.use(config => {
  // 生成请求key
  const requestKey = generateReqKey(config)
  
  // 检查是否是重复请求
  if (pendingRequests.has(requestKey)) {
    return Promise.reject(new axios.Cancel('重复请求已取消'))
  }
  
  // 将当前请求加入队列
  pendingRequests.set(requestKey, config)
  
  // 显示全局Loading
  if (!loadingInstance) {
    loadingInstance = Loading.service({
      lock: true,
      text: '加载中...',
      background: 'rgba(0, 0, 0, 0.7)'
    })
  }
  
  return config
}, error => {
  return Promise.reject(error)
})

// 响应拦截器
service.interceptors.response.use(
  response => {
    // 从队列中移除已完成请求
    const requestKey = generateReqKey(response.config)
    pendingRequests.delete(requestKey)
    
    // 所有请求完成时关闭Loading
    if (pendingRequests.size === 0 && loadingInstance) {
      loadingInstance.close()
      loadingInstance = null
    }
    
    return response.data
  },
  error => {
    // 处理被取消的请求
    if (axios.isCancel(error)) {
      return Promise.resolve({ cancelled: true })
    }
    
    // 从队列中移除失败请求
    if (error.config) {
      const requestKey = generateReqKey(error.config)
      pendingRequests.delete(requestKey)
    }
    
    // 所有请求完成时关闭Loading
    if (pendingRequests.size === 0 && loadingInstance) {
      loadingInstance.close()
      loadingInstance = null
    }
    
    return Promise.reject(error)
  }
)

export default service

5. 高级优化与注意事项

5.1 特殊请求处理

某些特殊请求(如文件上传)可能需要特殊处理:

function isFileUpload(config) {
  return config.data instanceof FormData
}

// 在请求拦截器中
if (isFileUpload(config)) {
  // 文件上传请求不做重复检查
  return config
}

5.2 请求超时处理

// 在axios配置中
const service = axios.create({
  timeout: 10000, // 10秒超时
  // 其他配置...
})

// 在响应拦截器的错误处理中
if (error.code === 'ECONNABORTED') {
  // 处理超时错误
  error.message = '请求超时,请稍后重试'
}

5.3 与Vuex集成

如果需要全局管理请求状态,可以与Vuex集成:

// store/modules/loading.js
export default {
  state: {
    isLoading: false,
    activeRequests: 0
  },
  mutations: {
    START_LOADING(state) {
      state.activeRequests++
      state.isLoading = true
    },
    STOP_LOADING(state) {
      state.activeRequests--
      if (state.activeRequests <= 0) {
        state.isLoading = false
        state.activeRequests = 0
      }
    }
  }
}

// 在拦截器中
store.commit('START_LOADING')
// 请求完成后
store.commit('STOP_LOADING')

6. 方案对比与选择

方案优点缺点适用场景
简单Loading实现简单,快速见效无法防止重复请求,并行请求处理差简单项目,快速原型
请求计数解决并行请求问题仍可能发送重复请求中等复杂度项目
完整拦截器全面防止重复提交,精细控制实现复杂度高大型项目,高要求场景

在实际项目中,可以根据项目规模和需求选择合适的方案。对于大多数中大型Vue项目,完整拦截器方案是最佳选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值