目录

前言

如何取消请求?

1、CancelToken.source 工厂

2、CancelToken 构造函数

全局请求优化

1、如何判断重复请求?

2、如何取消重复请求?

3、配置拦截器


前言

在项目开发过程中,我们经常会遇到重复请求的场景,如果我们不对重复的请求进行处理,则可能会导致程序出现各种问题。比如重复的 post 请求可能会导致服务端产生两笔记录。那么重复请求是如何产生的呢?以下是2 个常见的场景:

  • 假设页面中有一个按钮,用户点击按钮后会发起一个请求。如果未对该按钮进行控制,当用户快速点击按钮时,则会发出重复请求。

  • 假设在查询某页面数据时,用户可以根据不同的查询条件来筛选数据。如果请求的响应比较慢,当用户在不同的查询条件之前快速切换时,就会产生重复请求。

类似于以上两种场景,我们可以将重复的请求(处于pending状态)取消,来优化性能、减少意外bug产生。

如何取消请求?

以下通过Vue项目中axios举例,阐述取消axios请求的方式。

对于浏览器环境来说,axios 底层是利用 XMLHttpRequest 对象来发起 HTTP 请求。如果要取消请求的话,我们可以通过调用 XMLHttpRequest 对象上的 abort 方法来取消请求:

let xhr = new XMLHttpRequest();
xhr.open("GET", "https://user/12345", true);
xhr.send();
setTimeout(() => xhr.abort(), 1000);

对于 axios 来说,我们可以通过 axios 内部提供的 CancelToken 来取消请求,其有两种方式,我们一一举例。

1、CancelToken.source 工厂

我们可以使用 CancelToken.source 工厂方法创建 cancel token

let CancelToken = axios.CancelToken;
let source = CancelToken.source();

// get请求
axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
     // 处理错误
  }
});

// post请求
axios.post('/user/12345', {
  name: 'leo'
}, {
  cancelToken: source.token
})

// 取消请求(传入的参数是可选的)
source.cancel('Operation canceled by the user.');

实例

如下,当我们点击搜索按钮,发送一个请求

<template>
  <div class="main">
    <el-button type="plain" @click="search">搜索</el-button>
  </div>
</template>

<script>
export default {
  name: "A",
  data() {
    return {
    };
  },
  methods: {
    search() {
      let url = "https://lianghj.top:8888/api/private/v1/rights/list";
      this.$axios.get(url).then((res) => {
        console.log(res)
      });
    },
  }
};
</script>

现在,我们设置低网速来模拟请求较慢(处于pending)的情况

此时,当我们连续多次点击按钮,将出现如下情形

在上一个请求还未响应完成时,我们又发送了重复的请求。其实在上一次请求响应之前,下一次重复请求都是不必要,会造成资源损耗、性能下降,甚至涉及到对数据修改操作时,多个重复请求可能产生意外的bug。

此时,取消重复请求就变得尤为重要。

那么,对于这个例子,我们如何使用 CancelToken.source 工厂方法,以达到取消重复请求的目的?

优化版

<template>
  <div class="main">
    <el-button type="plain" @click="search">搜索</el-button>
  </div>
</template>

<script>
export default {
  name: "A",
  data() {
    return {
      source: null
    };
  },
  methods: {
    search() {
      if(this.source){
        // 如果上一次请求未完成,this.source不为null,则执行取消上一次请求的操作
        this.cancel();
      }
      // CancelToken.source 工厂方法
      let CancelToken = this.$axios.CancelToken;
      this.source = CancelToken.source();
      let url = "https://lianghj.top:8888/api/private/v1/rights/list";
      this.$axios.get(url, {
        // 传入cancelToken,使该请求可取消
        cancelToken: this.source.token,
      }).then((res) => {
        // 请求响应完成,this.source为null,不影响下次相同的请求
        this.source = null;
      })
    },
    cancel(){
      // 取消请求
      this.source.cancel("取消重复请求!");
    }
  },
};
</script>

此时,如果我们连续多次点击按钮,结果如下

在上一次请求未响应完成,发起重复请求,则将上一次请求取消,以达到优化效果。

2、CancelToken 构造函数

我们也可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token

let CancelToken = axios.CancelToken;
let cancel;

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

// 取消请求
cancel();

实例

如下方式也能达到取消重复请求的效果

<template>
  <div class="main">
    <el-button type="plain" @click="search">搜索</el-button>
  </div>
</template>

<script>
export default {
  name: "A",
  data() {
    return {
      source: null
    };
  },
  methods: {
    search() {
      if(this.source){  // 如果上一个请求未响应,则this.source不为null,执行取消上一次请求的操作
        // 取消请求
        this.source();
      }
      // CancelToken.source 工厂方法
      let CancelToken = this.$axios.CancelToken;
      let cancel;
      let url = "https://lianghj.top:8888/api/private/v1/rights/list";
      this.$axios.get(url, {
        // 传入cancelToken,使该请求可取消
        cancelToken: new CancelToken(function executor(c) {
          // executor 函数接收一个 cancel 函数作为参数
          cancel = c;
        })
      }).then((res) => {
        this.source = null;
      })
      this.source = cancel;
    },
  },
};
</script>

我们知道,发出去的请求(处于pending阶段)可以使用以上两种方式进行取消或中断。那么,在真实的项目中,我们可能需要为全部的请求进行此项优化(取消重复请求),那又该如何去实现?

全局请求优化

这里会涉及到一个问题,我们为全局的请求进行此项优化时,如何判断是否为重复的请求呢?如我在发送1请求,紧接着去发送2请求,此时不算是重复请求,不需要取消;而当发送1请求,1请求还未响应完成时紧接着再去发送1请求,此时判定为重复请求,则执行取消上一次重复请求的操作。

1、如何判断重复请求?

当请求方式、请求 URL 和请求参数都一样时,我们就可以认为请求是一样的。因此在每次发起请求时,我们就可以根据当前请求的请求方式、请求 URL 地址和请求参数来生成一个唯一的 key,同时为每个请求创建一个专属的 CancelToken,然后把 key 和 cancel 函数以键值对的形式保存到 Map 对象中,使用 Map 的好处是可以快速的判断是否有重复的请求

let pendingRequest = new Map();
// 生成唯一的key
let requestKey = [method, url, JSON.stringify(params), JSON.stringify(data)].join('&'); 
let cancelToken = new CancelToken(function executor(cancel) {
  if(!pendingRequest.has(requestKey)){
    // 如果发送的请求不存在,则进行保存
    pendingRequest.set(requestKey, cancel);   // 保存cancel函数,以便后续执行取消请求操作
  }
})

当出现重复请求的时候,我们就可以使用 cancel 函数来取消前面已经发出的请求,在取消请求之后,我们还需要把取消的请求从 pendingRequest 中移除。

2、如何取消重复请求?

因为我们需要为全局请求进行此优化,此时可以在拦截器上添加相关配置。

在配置请求拦截器和响应拦截器前,我们先定义3个功能辅助函数。

getRequestKey:用于根据当前请求的信息,生成唯一的请求 key:

// 函数返回唯一的请求key
function getRequestKey(config) {
  let { method, url, params, data } = config;
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join("&");
}

addPendingRequest:用于把当前请求信息添加到pendingRequest对象中:

let pendingRequest = new Map();
function addPendingRequest(config) {
  let requestKey = getRequestKey(config);
  config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
    if (!pendingRequest.has(requestKey)) {
       pendingRequest.set(requestKey, cancel);
    }
  });
}

removePendingRequest:检查是否存在重复请求,若存在则取消已发的请求:

function removePendingRequest(config) {
  let requestKey = getRequestKey(config);
  if (pendingRequest.has(requestKey)) {
     // 如果是重复的请求,则执行对应的cancel函数
     let cancel = pendingRequest.get(requestKey);
     cancel(requestKey);
     // 将前一次重复的请求移除
     pendingRequest.delete(requestKey);
  }
}

3、配置拦截器

请求拦截器

axios.interceptors.request.use(
  function (config) {
    // 检查是否存在重复请求,若存在则取消已发的请求
    removePendingRequest(config);
    // 把当前请求信息添加到pendingRequest对象中
    addPendingRequest(config);
    return config;
  },
  function (error) => {
    return Promise.reject(error);
  }
);

响应拦截器

axios.interceptors.response.use(
  function (response) => {
    // 从pendingRequest对象中移除请求
    removePendingRequest(response.config);
    return response;
  },
  function (error) => {
    // 从pendingRequest对象中移除请求
    removePendingRequest(error.config || {});
    if (axios.isCancel(error)) {
      console.log("已取消的重复请求:" + error.message);
    } else {
      // 添加异常处理
    }
    return Promise.reject(error);
  }
);

完整实例

import axios from "axios";

// 函数返回唯一的请求key
function getRequestKey(config) {
  let { method, url, params, data } = config;
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join("&");
}

// 添加请求信息
let pendingRequest = new Map();
function addPendingRequest(config) {
  let requestKey = getRequestKey(config);
  config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
    if (!pendingRequest.has(requestKey)) {
       pendingRequest.set(requestKey, cancel);
    }
  });
}

// 取消重复请求,移除重复请求信息
function removePendingRequest(config) {
  let requestKey = getRequestKey(config);
  if (pendingRequest.has(requestKey)) {
     // 如果是重复的请求,则执行对应的cancel函数
     let cancel = pendingRequest.get(requestKey);
     cancel(requestKey);
     // 将前一次重复的请求移除
     pendingRequest.delete(requestKey);
  }
}

// 请求拦截器
axios.interceptors.request.use(
  function (config) {
    // 检查是否存在重复请求,若存在则取消已发的请求
    removePendingRequest(config);
    // 把当前请求信息添加到pendingRequest对象中
    addPendingRequest(config);
    return config;
  },
  function (error) {
    return Promise.reject(error);
  }
);

// 响应拦截器
axios.interceptors.response.use(
  function (response) {
    // 从pendingRequest对象中移除请求
    removePendingRequest(response.config);
    return response;
  },
  function (error) {
    // 从pendingRequest对象中移除请求
    removePendingRequest(error.config || {});
    if (axios.isCancel(error)) {
      console.log("已取消的重复请求:" + error.message);
    } else {
      // 添加异常处理
    }
    return Promise.reject(error);
  }
);
export default axios

此时连续多次点击按钮,重复请求(处于pending)将被取消,结果如下

完整实例可直接使用,在你项目里面试试吧。

Logo

前往低代码交流专区

更多推荐