在实际开发中,我们需要对用户发起的重复请求进行拦截处理,比如用户快速点击提交按钮。

对于重复的 get 请求,会导致页面更新多次,发生页面抖动的现象,影响用户体验;对于重复的 post 请求,会导致在服务端生成两次记录(例如生成两条订单记录)。

无论从用户体验或者从业务严谨方面来说,取消无用的请求是需要避免的。

一、一般处理方式

我们可以在用户即将发送请求,但还未发送请求时给页面添加一个 loading 效果,提示数据正在加载,loading 会阻止用户继续操作。

这种方式在大部分情况下是可行的,但是在某些情况下却不奏效,比如在 loading 显示之前,用户就已经触发了两次请求的情况。

二、Axios 拦截器统一处理

重复发送的请求的场景很多,我们需要在一个公共的地方对请求响应进行处理,Axios 拦截器就闪亮登场了。
Axios 拦截器包括请求拦截器和响应拦截器,可以在请求发送前或响应后进行拦截处理,用法如下:

// 添加请求拦截器
axios.interceptors.request.use(
  function (config) {
    // 在发送请求之前做些什么
    return config;
  },
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  }
);

// 添加响应拦截器
axios.interceptors.response.use(
  function (response) {
    // 对响应数据做点什么
    return response;
  },
  function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  }
);

那么,如何进行拦截呢?也就是如何取消用户的请求,将它扼杀在摇篮里…

2.1、如何取消请求

众所周知,浏览器是通过 XMLHttpRequest 对象进行 http 通信的,如果要取消请求的话,我们可以通过调用 XMLHttpRequest 对象上的 abort 方法来取消请求。

let xhr = new XMLHttpRequest();
xhr.open("GET", "http://www.shanzhonglei.com/", true);
xhr.send();
setTimeout(() => xhr.abort(), 300);

Axios是一个主流的http请求库,它提供了两种取消请求的方式。

第一种,通过axios.CancelToken.source生成取消令牌token和取消方法cancel。

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

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

第二种,通过axios.CancelToken构造函数生成取消函数。

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel function as a parameter
    cancel = c;
  })
});

// cancel the request
cancel();

需要注意的是在catch中捕获异常时,应该使用axios.isCancel()判断当前请求是否是主动取消的,以此来区分普通的异常逻辑。

知道了如何取消请求就好办了,如果两个请求是相同的,那么我们就可以对后一个请求进行拦截操作。

2.2、判断重复请求

我们可以把每个请求的方法、url 和参数组合成一个字符串,作为该请求的唯一标识 key,与此同时,为对应的 key 生成一个 CancelToken 以备取消当前的请求。把 key 和对应的 cancel 函数以键值对的形式保存在 Map 对象中。

const pendingRequest = new Map();
const requestKey = [
  method,
  url,
  JSON.stringify(params),
  JSON.stringify(data),
].join("&");
const cancelToken = new CancelToken(function executor(cancel) {
  if (!pendingRequest.has(requestKey)) {
    pendingRequest.set(requestKey, cancel);
  }
});

定义pendingRequests 为 map 对象的目的是为了方便我们查询它是否包含某个 key,以及添加和删除 key。

在请求拦截器中,会检查pendingRequests 对象中是否包含当前请求的 requestKey,如果重复,就cancel拦截掉当前请求,如果不重复,则将requestKey 添加到 pendingRequests 对象中。

2.3 具体实现

我们先来生成几个辅助函数:

generateReqKey:用于根据当前请求的信息,生成请求 Key

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

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

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

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

function removePendingRequest(config) {
  const requestKey = generateReqKey(config);
  if (pendingRequest.has(requestKey)) {
    const cancelToken = pendingRequest.get(requestKey);
    cancelToken(requestKey);
    pendingRequest.delete(requestKey);
  }
}

clearPending 清空 pending 中的请求(在路由跳转时调用)

function clearPending() {
  for (const [requestKey, cancelToken] of pendingRequest) {
    cancelToken(requestKey)
  }
  pendingRequest.clear()
}

实操来了…

请求拦截器

axios.interceptors.request.use(
  function (config) {
    removePendingRequest(config); // 检查是否存在重复请求,若存在则取消已发的请求
    addPendingRequest(config); // 把当前请求信息添加到pendingRequest对象中
    return config;
  },
  (error) => {
    // 这里出现错误可能是网络波动造成的,清空 pendingRequests 对象
    pendingRequests.clear();
    return Promise.reject(error);
  }
);

响应拦截器

在这里,说明请求已经结束了,状态已经变成pending,这时需要把它从pendingRequests删除。

axios.interceptors.response.use(
  (response) => {
    removePendingRequest(response.config); // 从pendingRequest对象中移除请求
    return response;
  },
  (error) => {
    removePendingRequest(error.config || {}); // 从pendingRequest对象中移除请求
    if (axios.isCancel(error)) {
      console.warn(error);
      return Promise.reject(error);
    } else {
      // 添加其它异常处理
    }
    return Promise.reject(error);
  }
);

最后,我们要在页面切换之前取消上一个路由中未完成的请求,清空缓存的pendingRequest对象。

router.beforeEach((to, from, next) => {
  clearPending();
  // ...
  next();
});

最后

最后

欢迎关注我的公众号【前端技术驿站】,多多交流,共同进步!
回复react:
1、React.js大众点评案例完整版
2、React+TypeScript高仿AntDesign开发企业级UI组件库
3、React17+React Hook+TS4最佳实践 仿Jira企业级项目
回复vue
1、[全栈开发 ]Vue+Django REST framework 打造生鲜电商项目
2、核心源码内参
3、Vue3+ElementPlus+Koa2 全栈开发后台系统
4、ES6零基础教学解析彩票
5、Node.js+Koa2框架生态实战 - 从零模拟新浪微博(完整版)
6、vue无人点餐收银系统
回复node
1、Nodejs视频教程
2、全栈最后一公里 - Nodejs 项目的线上服务器部署与发布
3、深入浅出Node.js

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐