巧妙封装全局ElementUI的loading防止请求连续重复提交

3,733 阅读5分钟

哈哈哈哈我错了,上篇文章《由axios如何cancel请求想到的如何防止多次重复请求数据覆盖问题》说通用的请求loading不是很好封装,是我错了,又想了一下,好像不是很难,这样看来上一篇说的两个问题可以分开解决了。(另上一篇的一些内容也因此进行了修改)

本文起因

针对于上篇说到的第一个问题:

列表展示页面上有多个筛选项,当用户操作太过频繁的时候,在第一个筛选项的筛选结果还未返回的时候就进行了第二个筛选项的筛选操作,如果这时候第一个请求的耗时很长,长到第二个请求的返回值已返回的情况下第一个请求还没返回,那么就会出现,最终渲染出来的列表是第一次筛选后的结果而不是我们想要的第二次筛选的结果。

这个问题其实还是使用取消请求的方案最合适,如果这个问题使用loading方案,在交互上是很不友好的,因为这样我就必须等待第一次筛选的结果返回才能进行第二次筛选,但是第一次的筛选结果在用户角度是完全不需要的,因此最好的还是直接取消第一次的筛选请求,做到用户无感知。

针对上篇的第二个问题:提交多次相同的创建请求,严格上使用取消请求的效果并不是那么理想,因为后端仍然接收到的是两个创建请求,并不会因为前端的取消请求而终止操作,因此该报错还是会报错,正如上篇所说,最友好的还是使用loading覆盖页面,让用户没有提交第二次创建请求的机会,但是上篇我说这种方案的封装性没有那么好,现在发现其实还是有很好的解决方案的。

其实我们要做的无非是在点击提交按钮执行提交方法的时候,在发送请求之前将当前操作模块的loading遮罩层的控制变量置为true,在接收到请求返回值之后(不论成功还是失败)都再将这个变量置为false。但是每个模块都维护这么一个变量操心什么开启什么时候关闭,都写一个v-loading,这样易用性和复用性就太低了,本文就提供了一种封装方案,仅供参考,求轻喷

进入正题

  1. 首先在axios的封装文件中添加声明
import { Loading } from 'element-ui'
let loadingInstance
  1. 然后在request拦截器中添加判断
// 判断请求的config中是否含有loadingOptions,也就是是否要对该请求添加loading处理,格式是Object
// loadingOptions具体配置参数就是elementUI的loading的Options配置
if (config.loadingOptions) loadingInstance = Loading.service(config.loadingOptions)

  1. 在请求定义的地方默认参数多添加一个config(以前只接收params)
import axios from '@/plugins/axios'

export const getUserList = (queryParams, config) => {
  return axios.get('/path/path', queryParams, config)
}
  1. 然后在request拦截器的错误处理里面、在response拦截器里面、在response拦截器的错误处理里面都分别添加上下面的
if (loadingInstance) loadingInstance.close()
  1. 在使用的文件中,首先给你所期望覆盖loading的dom添加唯一id,例如"soleId",然后在请求调用的地方传参额外多传递一个config参数,如下:
async toGetUserList() {
  let res
  // 第一个参数是请求传参,第二个是请求配置也就是告诉拦截器我要对该请求使用loading处理
  res = await getUserList({ teamId: 5 }, { loadingOptions: { target: '#soleId' } })
  if (res) this.userList = JSON.parse(JSON.stringify(res.data))
}

方法很简单,这样就不需要在操心对loading的控制变量的处理时机,也不需要再维护这个控制变量,还是能提高一些开发效率和可维护性的。

最后贴上axios的完整代码

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

// 设置baseURL,判断当前环境是否为生产环境,若不是需设置自己的apiURL
let baseURL = process.env.NODE_ENV !== 'production' ? '/' : '/'
let config = {
  baseURL,
  timeout: 60 * 1000 // 请求超时时间
}

let loadingInstance

const _axios = axios.create(config)

let pendingQueue = new Map()
let CancelToken = axios.CancelToken
// http request 拦截器
_axios.interceptors.request.use(
  function (config) {
    // Do something before request is sent
    if (config.loadingOptions) loadingInstance = Loading.service(config.loadingOptions)
    // 请求发起之前先检验该请求是否在队列中,如果在就把队列中pending的请求cancel掉
    judgePendingFunc(config)
    // 将pending队列中的请求设置为当前
    config.cancelToken = new CancelToken(cb => {
      pendingQueue.set(`${config.method}->${config.url}`, cb)
    })
    if (config.method === 'get') {
      let res = handleGetUrl(config.url, config.params)
      config.url = res.url
      config.params = res.params
    }
    return config
  },
  function (error) {
    // Do something with request error
    console.log(error)
    if (loadingInstance) loadingInstance.close()
    return Promise.reject(error)
  }
)

// http response 拦截器
_axios.interceptors.response.use(
  function (response) {
    // Do something with response data
    if (loadingInstance) loadingInstance.close()
    removeResolvedFunc(response.config)
    console.log(response)
    return response.data
  },
  function (error) {
    // Do something with response error
    console.log(error)
    if (loadingInstance) loadingInstance.close()
    return Promise.reject(error)
  }
)

// 二次封装方法
/**
 * 接收三个参数,配置参数config可不传
 * @param {String} url
 * @param {Object} data
 * @param {Object} config
 */
const getFn = async (url, data, config = {}) => {
  let params = { params: data, ...config }
  try {
    return _axios.get(url, params)
  } catch (error) {
    return handleError(error)
  }
}
/**
 * 接收三个参数,配置参数config可不传
 * @param {String} url
 * @param {Object} data
 * @param {Object} config
 */
const postFn = async (url, data, config = {}) => {
  try {
    return _axios.post(url, data, config)
  } catch (error) {
    return handleError(error)
  }
}
const deleteFn = async (url, data) => {
  try {
    return _axios.delete(url, data)
  } catch (error) {
    return handleError(error)
  }
}
// 捕获请求错误
function handleError(error) {
  Promise.reject(error)
}
// 判断请求是否在队列中,如果在就执行取消请求
const judgePendingFunc = function (config) {
  if (pendingQueue.has(`${config.method}->${config.url}`)) {
    pendingQueue.get(`${config.method}->${config.url}`)()
  }
}
// 删除队列中对应已执行的请求
const removeResolvedFunc = function (config) {
  if (pendingQueue.has(`${config.method}->${config.url}`)) {
    pendingQueue.delete(`${config.method}->${config.url}`)
  }
}
// 处理get请求功能性字符和非功能性字符被转换导致的问题
const handleGetUrl = function (url, params) {
  if (!params) return { url: url, params: params }
  let parts = []
  let resUrl = url
  let resParams = params
  let keys = Object.keys(params)
  if (keys.length > 0) {
    for (let key of keys) {
      let values = []
      if (Array.isArray(params[key])) {
        values = params[key]
        key += '[]'
      } else values = [params[key]]
      values.forEach(val => {
        if (val || val === 0)
          parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(val)}`)
      })
    }
    let serializedParams = parts.join('&')
    if (serializedParams) {
      resUrl += (resUrl.includes('?') ? '&' : '?') + serializedParams
    }
  }
  return { url: resUrl, params: resParams }
}

export default {
  get: getFn,
  post: postFn,
  delete: deleteFn
}

就酱,掰掰