使用axios阻止多余的请求(007,久违的更新)

1,432 阅读6分钟

使用axios阻止多余的请求

停更了很长一段时间(公司业务繁忙,目前007,实在抽不出时间写博客。加 油 打 工 人)

前言

在日常的复杂业务中,我们总是调用大量的后端api。为了提升服务器的效率,我们需要尽可能的减少每个用户的平均请求数量

本文的内容,着重对重复多余的请求做处理,以节约网络资源。比如:

  • 用户高频点击某个发送请求的按钮
  • 本地的定时请求任务,前一个请求还未完成就发送了新的请求

第二个场景比较罕见,不过在一次开发任务中,糟糕的测试服务器使这个问题严重影响了我的开发体验,印象深刻。

在服务器资源充足的时候,上述问题一般不会暴露,不过在服务器资源紧张的时候,减少非必要请求可以为服务器争取更多的可用空间。

本文会展示“阻止多余请求”的工具的实现,最后,会稍微探讨我们通过这个工具还可以做些什么。

做什么

如何定义“重复”和“多余”?当我们通过请求获取数据时,我们希望在该请求完成前,不去发送新的相同请求;当我们提交数据时,我们希望提交的数据是最新的,所以需要取消之前的请求。我们把问题展开,发现需求是这样的:

  • 当请求方法为 get 时,当 x 请求未完成时,阻止新触发的 x 请求
  • 当请求方法不为 get 时,当 x 请求未完成时,如果有新的 x 请求被触发,则取消前一个 x 请求,重新发送新的 x 请求,以保证发送的数据是最新的
  • 可以手动清除特定的请求

怎么做

使用工具

我们将会用到 axios 库中的 CancelToken API,这个API专门用于取消某个正在执行的请求,具体参加文档,不再赘述。文档传送门

整体思路

先规定这个工具应该放在哪里。作为一个全局的中间件,放在axios的拦截器里是再合适不过了。我们定义两个方法,cancelReqresetReq

cancelReq 负责标记正在进行的请求、判断是否有相同的请求正在执行;resetReq 负责清除已完成请求的标记

import { cancelReq, resetReq } from 'cancel-token.js'

// axios.js
// 请求拦截器
_axios.interceptors.request.use(
  function(config) {
    // * cancelToken 阻止重复请求
    config = cancelReq(config)
    return config
  },
  function(error) {
    return Promise.reject(error)
  }
)

// 响应拦截器
_axios.interceptors.response.use(
  function(response) {
    // * cancelToken 完成请求,清除标记
    resetReq(response.config)
  },
  function(error) {
    if (error.config) {
      resetReq(error.config)
    }
    return Promise.reject(error)
  }
)

我们将这个工具放在 cancel-token.js 文件中。首先我们需要一个存放“正在执行的请求”的一个队列 reqList ;同时,我们规定一个构造函数 Req,存放一个请求的关键信息,Req 主要存储一个请求的 urlmethod 以及 用于结束该请求的cancel 方法。

考虑到某些复杂的业务场景,如果上述参数不足以区分每个请求(比如某个页面需要同时发送两次 /get/methodget 请求,分别带上 ?query=a?param=b),这时,我们需要在 Req 构造函数中添加 querydata

// cancel-token.js
// * 存放当前正在执行的请求
const reqList = []

/**
 * * 构造函数 - axios请求的关键对象(url/method/params/data)
 * @param {*} config 当前axios请求的config
 */
function Req(config) {
  this.url = config.url
  this.method = config.method
  this.cancel = config.cancel
  // * 可能需要添加config.query或config.data
}

cancelReq 方法的实现

cancelReq 需要做的是:

  • 将新的请求添加到“正在执行”的队列中
  • 阻止重复的 get 请求
  • 如果有新的提交数据的请求,如 postput,取消之前的那个请求并清除出队列,执行新的请求并添加到队列

cancelReq 使用到了 CancelToken。我们使用 _cancel 变量去接受 cancel 方法,并将这个方法添加到该请求的 config,这样我们可以随时随地调用 cancel('这个请求结束了') 终止这个请求。

准备完成后,创建该请求的 Req 实例 _req ,这个 _req 将存放于执行队列 reqList中,如果队列里存在该请求,则按照下方 else {} 函数体中的逻辑执行。

// cancel-token.js
import { CancelToken } from 'axios'

// * 阻止请求重复发送
export function cancelReq(config) {
  const _config = config
  let _cancel
  // * 拿到cancel方法
  _config.cancelToken = new CancelToken(function(cancel) {
	// 赋值
    _cancel = cancel
    _config.cancel = cancel
  })
  // 创建Req的一个实例
  const _req = new Req(_config)
  // 🎈注意,findReq 这个方法用于查找这个req实例是否已经存在于执行队列reqList中
  const _index = findReq(_req)
  if (_index === -1) {
  	// 该请求未处于执行状态,将它添加到执行队列,over
    reqList.push(_req)
  } else {
  	// 看来这个请求正在执行
    if (_req.method.toLowerCase() === 'get') {
      // * 如果method 为 get 则阻止后一个请求
      _cancel(`已取消重复发送的请求: ${_config.url}`)
    } else {
      // * 如果不为get 则取消前一个请求,发送新的请求
      reqList[_index].cancel(`已取消重复发送的请求: ${_config.url}`)
      reqList.splice(_index, 1)
    }
  }
  // 最后返回处理完成的config,交给axios请求拦截器
  return _config
}

我们判断 reqList 中是否有某个 req ,使用的是一个 findReq 方法。下面看一下 findReq 方法的实现。

/**
 * * 将要发送的请求是否已经存在
 * @param {*} target 将要进行的请求
 * @returns {*} 这个请求的index,-1即没有
 */
function findReq(target) {
  let _index = -1
  for (const i in reqList) {
    if (reqList[i].url === target.url &&
      reqList[i].method === target.method) {
      // * 还可以添加其他的规则
        _index = i
        break
    }
  }
  return _index
}

resetReq 方法的实现

resetReq 需要做的是:

  • 清除完成的请求

做法与 cancelReq 类似,创建一个 Req 实例,在 reqList 中查找该请求并清除。

// * 清除完成发送的请求
export function resetReq(config) {
  const _req = new Req(config)
  const _index = findReq(_req)
  if (_index !== -1) {
    reqList.splice(_index, 1)
  }
}

手动清除请求的方法

某些具体的业务的实现可能会需要我们手动清理某些需求,此时我们定义一个 manualCancelReq 方法。这个方法清理掉特定 urlmethod 的请求,并且可以做到清理不至一个匹配到的请求(如果有的话)。这里使用了递归来做到清理多个请求。

import { findIndex as _findIndex } from 'lodash'

/**
 * * 手动清除某个或某些可能存在的请求(根据url匹配)
 * @param {string} url 请求的url
 * @param {string | null} method 请求的method,可传 null
 * @param {boolean} multi 清除多个请求的就传 true
 */
export function manualCancelReq(url, method, multi) {
  const _reqIndex = _findIndex(
    reqList,
    Object.assign({ url }, method ? { method } : {})
  )
  if (_reqIndex !== -1) {
    reqList[_reqIndex].cancel()
    reqList.splice(_reqIndex, 1)
    // * 如果是清除多个请求
    if (multi) {
      manualCancelReq(url, method, multi)
    }
  }
}

好的,那么现在你的axios拥有了阻止多余请求的能力。

扩展

基本功能到此为止就实现了。我们可以通过这个工具实现其他的一些功能,比如下面的“当有正在进行的请求时,页面展示loading动画”

我们需要在每一个请求发起时,也就是 cancelReq 方法中执行 startLoading 方法激活 loading 动画;在请求完成时,也就是 resetReq 方法中执行 stopLoading 方法关闭 loading 动画。

当然,我们需要做一些判断,不能重复激活动画,也必须在所有请求完成后再关闭动画。同时我们可以为动画激活做一点延时,比如小于300ms的快速请求情况下,不激活动画。

假设激活和关闭动画的方法分别为 Loading.start()Loading.stop()

// * 页面是否处于loading状态
let loadingStatus = false
// * 定时器
let loadingTimeout = null
/**
 * * 触发loading - 延时
 * * 条件:loadingStatus 为 false
 * @param {Number} timeout 激活动画的延时 ms
 */
export function startLoading(timeout) {
  if (!loadingStatus) {
    loadingTimeout = setTimeout(() => {
      Loading.start()
    }, timeout || 300)
    loadingStatus = true
  }
}

/**
 * * 结束loading
 */
export function stopLoading() {
  if (loadingStatus && !reqList.length) {
    clearTimeout(loadingTimeout)
    Loading.stop()
    loadingStatus = false
  }
}