axios拦截器之重复请求取消上次请求、路由切换取消接口,loading,状态码抛错,同样的错误弹框内容只出现一次

3,668 阅读6分钟

本文主要功能有

  • 重复请求取消上次请求
  • 路由切换取消接口
  • 配置loading
  • 状态码抛错
  • 同样的错误弹框内容只出现一次

前言

在项目中经常会遇到需要主动取消接口的场景,axios提供CancelToken的功能可以主动停止当前请求,从而避免无效请求,优化网络性能

场景:

远程搜索接口频繁请求,第二个接口先成功导致显示第一个接口返回的数据 前端切换路由,前一个路由的接口用不到了,但还在请求中 这两种情况可以单独在组件中使用CancelToken来取消接口请求,但是如果要主动请求的接口较多,则会照成代码冗余,于是需要封装成一个公共的方法

1、搭载请求容器及处理方法:cancel-request.js

  • pendingMap: 盛放接口处于pengding(请求中)状态的容器,key是接口唯一标识以url,method,请求参数;value是该接口的cancel请求的方法

  • getPendingKey: 生成接口唯一标识的方法

  • addPending:把请求加到pending容器的方法

  • removePending:把请求从pengding容器中移除的方法

// 导出-为了在切换页面时取消被切换页面的正在请求的接口
export const pendingMap =new Map()
/**
 * 生成每个请求唯一的键
 * @param {*} config 
 * @returns string
 */
function getPendingKey(config){
  let {url,method,params,data} = config
  if(typeof data ==='string') {
    data=JSON.parse(data)
  }
  // 以url和...组成字符串作为储存的key值
  return [url,method,JSON.stringify(params),JSON.stringify(data)].join('&')
}
/**
 * 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求
 * @param {*} config 
 */
function addPending(config){
  const pendingKey=getPendingKey(config)
  // https://segmentfault.com/a/1190000039844280
  config.cancelToken =config.cancelToken || new axios.CancelToken((cancel)=> {
    if(!pendingMap.has(pendingKey)){
      pendingMap.set(pendingKey,cancel)
    }
  })
}
/**
 * 删除重复的请求
 * @param {*} config 
 */
function removePending(config){
  // console.log(pendingMap,'pendingMap')
  const pendingKey =getPendingKey(config)
  if(pendingMap.has(pendingKey)){
    const cancelToken= pendingMap.get(pendingKey)
    cancelToken(pendingKey)
    pendingMap.delete(pendingKey)
  }
}

如何取消一个已发送的请求

在开始正题前,我们要先来了解一下,如何取消一个已发送的请求,不知道铁汁们对JS中的 XMLHttpRequest 对象是否了解?(不知道也当你知道了) 你只要知道axios底层就是依赖于它的就行,也就是它的二次封装,那我们对axios再次封装,也就是三次封装?套娃?

XMLHttpRequest 对象是我们发起一个网络请求的根本,在它底下有怎么一个方法 .abort(),就是中断一个已被发出的请求。虽然已经中断但还是可能到了后端,最后后端要有相应的处理

image.png

那么axios自然也有对其的相关封装,就是 CancelToken文档上介绍的用法: 这里也有解析

var CancelToken = axios.CancelToken;
var cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    cancel = c;
  })
});

// 取消请求
cancel();
复制代码

简单理解就是通过 new axios.CancelToken()给每个请求带上一个专属的CancelToken,之后会接收到一个cancel() 取消方法,用于后续的取消动作,所以我们需要对应的存储好这个方法。

逻辑思路

通过上面的了解,下面就能进入正题部分了,接下来我们大致整体思路就是收集正在请求中接口,也就是接口状态还是pending状态的,让他们形成队列储存起来。如果相同接口再次被触发,则直接取消正在请求中的接口并从队列中删除,再重新发起请求并储存进队列中;如果接口返回结果,就从队列中删除,以此过程来操作。

判断重复请求并储存进队列

首先我们要收集请求中的接口并判断哪些请求是重复请求,我们才能取消它,那么如何判断呢?很简单,只要是请求地址、请求方式、请求参数一样,那么我们就能认为是一样的。而我们要存储的队列里面的数据结构很明显应该是以键值对的形式来存储,这里面我们选择 Map 对象来操作。代码在上面

配置化取消重复请求

之所以弄成配置化取消重复请求,是因为可能存在一些特殊变态的场景情况,是需要重复请求,如输入实时搜索、实时更新数据等,反正就是可能存在吧。

    // 自定义配置
    let custom_options = Object.assign({
      repeat_request_cancel: true, // 是否开启取消重复请求, 默认为 true
      loading: false, // 是否开启loading层效果, 默认为false
    }, customOptions);

配置化Loading

异步数据是非常常见的场景,一个良好的Loading效果能很好的加强用户体验,也能让我们回避一些问题,如上面提到的重复请求,如果在发起了一个请求后立即就出现一个Loading层,那么用户就无法再次点击而造成重复多次请求了。

添加怎么一个功能我们需要考虑怎么三件事:

  • 同一时间内发起多个请求,我们只需要展示一个Loading层即可,不需要产生多个造成重复展示。
  • 同一时间内发起多个请求展示的Loading层以最后一个请求响应而关闭销毁。
  • 此功能依旧要进行可配置化处理。

状态码抛错

 function httpErrorStatusHandle(error) {
   console.log(axios.isCancel(),error,'axios')
  // 处理被取消的请求
  if(axios.isCancel(error)) return console.error('请求的重复请求:' + error.message);
  let message = '';
  if (error && error.response) {
    switch(error.response.status) {
      case 302: message = '接口重定向了!';break;
      case 400: message = '参数不正确!';break;
      case 401: message = '您未登录,或者登录已经超时,请先登录!';break;
      case 403: message = '您没有权限操作!'; break;
      case 404: message = `请求地址出错: ${error.response.config.url}`; break; // 在正确域名下
      case 408: message = '请求超时!'; break;
      case 409: message = '系统已存在相同数据!'; break;
      case 500: message = '服务器内部错误!'; break;
      case 501: message = '服务未实现!'; break;
      case 502: message = '网关错误!'; break;
      case 503: message = '服务不可用!'; break;
      case 504: message = '服务暂时无法访问,请稍后再试!'; break;
      case 505: message = 'HTTP版本不受支持!'; break;
      default: message = '异常问题,请联系管理员!'; break
    }
  }
  if (error.message.includes('timeout')) message = '网络请求超时!';
  if (error.message.includes('Network')) message = window.navigator.onLine ? '服务端异常!' : '您断网了!';
  showMessage(message)

}

同样的错误弹框内容只出现一次

1:判断同时只能存在消息相同的一个弹框

2:或是通过判断弹框元素来判断

/**
 * 处理相同内容多次弹出弹框
 * @param {*} _options 
 */
 function showMessage(message){
  // 1:判断同时只能存在消息相同的一个弹框
  // 2:[或是通过判断弹框元素来判断](http://www.javashuo.com/article/p-nytmzryt-kb.html)
  if(!messageList1.includes(message)){
    messageList1.push(message)
      ElMessage({
        type: 'error',
        // grouping: true,
        onClose:(e)=> {
          const _this= e
          const index = messageList1.findIndex((i)=>i==_this.props.message) 
          messageList1.splice(index,1)
        },
        message,
      })
    }
}

路由切换取消接口

共有两种写法 都是在全局路由中

import {pendingMap} from './api/axios'
router.beforeEach((to,from,next)=> {
  // 方法1:
  for (const [key,value] of pendingMap.entries()) {
    value(key)
  }

  // // 方法2:
  // console.log(pendingMap,'pendingMap------------')
  // let pendingMapArr= pendingMap.values()
  // console.log(pendingMapArr,'pendingMapArr------------')
  // let current= pendingMapArr.next()
  // console.log(current,'current------------')
  // while(!current.done){
  //   current.value()
  //   current=pendingMapArr.next()
  // }

  next()
})

全部前端代码

// axios.js
import axios from 'axios';
import { ElLoading,ElMessage  } from 'element-plus'
// 导出-为了在切换页面时取消被切换页面的正在请求的接口
export const pendingMap =new Map()
/**
 * 生成每个请求唯一的键
 * @param {*} config 
 * @returns string
 */
function getPendingKey(config){
  let {url,method,params,data} = config
  if(typeof data ==='string') {
    data=JSON.parse(data)
  }
  // 以url和...组成字符串作为储存的key值
  return [url,method,JSON.stringify(params),JSON.stringify(data)].join('&')
}
/**
 * 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求
 * @param {*} config 
 */
function addPending(config){
  const pendingKey=getPendingKey(config)
  // https://segmentfault.com/a/1190000039844280
  config.cancelToken =config.cancelToken || new axios.CancelToken((cancel)=> {
    if(!pendingMap.has(pendingKey)){
      pendingMap.set(pendingKey,cancel)
    }
  })
}
/**
 * 删除重复的请求
 * @param {*} config 
 */
function removePending(config){
  // console.log(pendingMap,'pendingMap')
  const pendingKey =getPendingKey(config)
  if(pendingMap.has(pendingKey)){
    const cancelToken= pendingMap.get(pendingKey)
    cancelToken(pendingKey)
    pendingMap.delete(pendingKey)
  }
}

const LoadingInstance = {
  _target: null, // 保存Loading实例
  _count: 0
};


function myAxios(axiosConfig,customOptions) {
  const service = axios.create({
    // baseURL: '', // 设置统一的请求前缀// http://localhost:8888
    timeout: 30000, // 设置统一的超时时长
  });

    // 自定义配置
    let custom_options = Object.assign({
      repeat_request_cancel: true, // 是否开启取消重复请求, 默认为 true
      loading: false, // 是否开启loading层效果, 默认为false
    }, customOptions);

  service.interceptors.request.use(
    config => {
      removePending(config);
      custom_options.repeat_request_cancel&&addPending(config);
        // 创建loading实例  
        if (custom_options.loading) {
          LoadingInstance._count++;
          if(LoadingInstance._count === 1) {
            LoadingInstance._target = ElLoading.service({
              text:'加载中-----------',
              background:'rgba(0,0,0,0.6)'
            });
          }
        }
      return config;
    }, 
    error => {
      return Promise.reject(error);
    }
  );
  service.interceptors.response.use(
    response => {
      removePending(response.config);
      custom_options.loading && closeLoading(custom_options); // 关闭loading

      return response;
    },
    error => {
      error.config && removePending(error.config);
      custom_options.loading && closeLoading(custom_options); // 关闭loading
      httpErrorStatusHandle(error); // 处理错误状态码
      return Promise.reject(error);
    }
  );
  return service(axiosConfig)
}
const messageList1=[]
/**
 * 处理异常
 * @param {*} error 
 */
 function httpErrorStatusHandle(error) {
   console.log(axios.isCancel(),error,'axios')
  // 处理被取消的请求
  if(axios.isCancel(error)) return console.error('请求的重复请求:' + error.message);
  let message = '';
  if (error && error.response) {
    switch(error.response.status) {
      case 302: message = '接口重定向了!';break;
      case 400: message = '参数不正确!';break;
      case 401: message = '您未登录,或者登录已经超时,请先登录!';break;
      case 403: message = '您没有权限操作!'; break;
      case 404: message = `请求地址出错: ${error.response.config.url}`; break; // 在正确域名下
      case 408: message = '请求超时!'; break;
      case 409: message = '系统已存在相同数据!'; break;
      case 500: message = '服务器内部错误!'; break;
      case 501: message = '服务未实现!'; break;
      case 502: message = '网关错误!'; break;
      case 503: message = '服务不可用!'; break;
      case 504: message = '服务暂时无法访问,请稍后再试!'; break;
      case 505: message = 'HTTP版本不受支持!'; break;
      default: message = '异常问题,请联系管理员!'; break
    }
  }
  if (error.message.includes('timeout')) message = '网络请求超时!';
  if (error.message.includes('Network')) message = window.navigator.onLine ? '服务端异常!' : '您断网了!';
  showMessage(message)

}

/**
 * 关闭Loading层实例
 * @param {*} _options 
 */
 function closeLoading(_options) {
  if(_options.loading && LoadingInstance._count > 0) LoadingInstance._count--;
  if(LoadingInstance._count === 0) {
    LoadingInstance._target.close();
    LoadingInstance._target = null;
  }
}
/**
 * 处理相同内容多次弹出弹框
 * @param {*} _options 
 */
 function showMessage(message){
  // 1:判断同时只能存在消息相同的一个弹框
  // 2:或是通过判断弹框元素来判断
  if(!messageList1.includes(message)){
    messageList1.push(message)
      ElMessage({
        type: 'error',
        // grouping: true,
        onClose:(e)=> {
          const _this= e
          const index = messageList1.findIndex((i)=>i==_this.props.message) 
          messageList1.splice(index,1)
        },
        message,
      })
    }
}

export default myAxios;

// https://juejin.cn/post/6968630178163458084#heading-8
// router.js
import {pendingMap} from './api/axios'
router.beforeEach((to,from,next)=> {
  // 方法1:
  for (const [key,value] of pendingMap.entries()) {
    value(key)
  }

  // // 方法2:
  // console.log(pendingMap,'pendingMap------------')
  // let pendingMapArr= pendingMap.values()
  // console.log(pendingMapArr,'pendingMapArr------------')
  // let current= pendingMapArr.next()
  // console.log(current,'current------------')
  // while(!current.done){
  //   current.value()
  //   current=pendingMapArr.next()
  // }

  next()
})

参考资料

axios

MDM

完整的Axios封装-单独API管理层、参数序列化、取消重复请求、Loading、状态码...