likes
comments
collection
share

基于 axios 的 http 竟态问题解决方案--- theme: channing-cyan highlight:

作者站长头像
站长
· 阅读数 16

前言

本文仅针对 axios 库, 如果你使用的是 fetch 或者 xhr, 请自行适配处理。

彦祖们也可以先看下完整代码(有助于下文的理解)

效果预览

自动把前置的 http 请求全部取消,只保留最后一个有效请求。

基于 axios 的 http 竟态问题解决方案--- theme: channing-cyan highlight:

什么是 http 请求竟态问题?

首先让我们了解下什么是 http 请求竟态问题, 我们看下 gpt 的回复。

前端 HTTP 请求竞态问题指的是在前端应用中,多个并发的 HTTP 请求之间相互竞争资源,导致结果不确定的情况。例如,当用户快速点击一个按钮,触发了多个相同的请求,而这些请求可能在不同的时间返回结果,最终导致页面显示不一致或者出现错误。

场景复现

在我们日常工作中经常会遇到这样的场景。

比如现在我们有个需求,点击苹果 橘子 香蕉三个按钮,分别渲染对应的一组水果列表。

对应请求我们假设为 /apple /orange /banana。

也就是说当我们点击苹果的时候 会去请求苹果列表(橘子,香蕉逻辑相同)。

乍一听,这太简单了,用手都能写是吧?

但是此时测试直接来了个 bug , 我当前明明是香蕉按钮,出来的怎么是苹果列表?

问题分析

那么上面的问题是如何导致的呢?

其实非常简单,测试先点击了 苹果按钮,瞬间又点了 香蕉按钮

导致按钮停在了 香蕉按钮, 由于 http 请求是异步的,/banana 请求 500ms 就完成了。

但是由于 /apple 1000ms 后才返回,

这就导致了这个 bug 的产生。

其实这就是典型的 HTTP 请求竞态问题

利用 axios 库解决问题

要解决竟态问题,那么我们目的其实很简单,只让最后一个请求是活跃的,也就是说我们要取消其他请求。

axios 如何取消请求

在 axios 库中, 我们可以使用 CancelToken 来取消请求。

import axios from 'axios'
const source = axios.CancelToken.source() // 创建一个 CancelToken 源

axios.get('/your-api-url', {
  cancelToken: source.token // 将 CancelToken 传递给 axios 请求配置对象
})

// 在某个时候取消请求
source.cancel('Operation canceled by the user.') // 取消请求

这个非本文重点,不做详细描述。 如果有看不懂的彦祖, 具体看下 axios 官网文档

封装一个可以 cancel 的 axios 函数

既然我们知道了 axios 能取消请求, 那么我们如何在项目中使用它呢?

简单的 axios 封装

在实际项目中, 我们通常会封装一个 axios 请求函数。

下面就以一个简单封装的 axios 请求函数为例(非常简陋,主要为了理解本文)

  • request.js
const service = axios.create({
  baseURL: ""// url = base url + request url
  timeout: 10000// request timeout
});
// request interceptor
service.interceptors.request.use(
  async (config) => {
    // 此处先省略了取消请求相关的代码
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

service.interceptors.response.use(
  async (response) => {
    const res = response.data;
    return res;
  },
  (error) => {
    return Promise.reject(error);
  }
);

export default service;

这样我们就封装了一个简单的 axios 请求函数。

接下来我们就可以在 api 使用它了。

  • api.js


import request from "@/request.js";

export const getApple = (data = {}) => {
  return request({
    url: "/apple"
    method: "get"
    data,
  });
};

export const getOrange = (data = {}) => {
  return request({
    url: "/orange"
    method: "get"
    data,
  });
};

export const getBanana = (data = {}) => {
  return request({
    url: "/banana"
    method: "get"
    data,
  });
};

接着我们把它引入到组件:

  • App.vue
<template>
  <div id="app">
    <button @click="getApple">apple</button>
    <button @click="getOrange">orange</button>
    <button @click="getBanana">banana</button>
  </div>
</template>

<script>
import { getOrange, getApple, getBanana } from "@/apis/fruits.js";
export default {
  name: "App"
  methods: {
    getApple() {
      getApple({page:10pageSize:100}).then((res) => {
        console.log("__SY__🎄 ~ getApple ~ res:", res);
      });
    },
    getOrange() {
      getOrange().then((res) => {
        console.log("__SY__🎄 ~ getOrange ~ res:", res);
      });
    },
    getBanana() {
      getBanana().then((res) => {
        console.log("__SY__🎄 ~ getBanana ~ res:", res);
      });
    },
  },
};
</script>

取消请求功能

目前我们的代码是没有任何取消请求功能的, 接下来我们加上取消请求的功能。

上文中我们已经知道可以使用 source.cancel 方法取消请求了。

那么我们考虑该如何把 source.cancel 暴露给开发者呢?

也就是说我们如何为每个 api 新增一个 cancel 方法呢?

还是受益于 <<vue.js 设计与实现>> 一书中的 effect 函数(其中把副作用函数当做参数)。

同样我们也可以把 api 函数当做参数传递给某个函数,我们暂且命名这个函数为 useCanCancelRequest(内部提供一个 cancel)。

先来看下 useCanCancelRequest 函数的函数签名:

function useCanCancelRequest(api){
  // params 即外部的业务参数
  const fn = function(params){
    // params 是非必传的, 如果没有传 params 参数, 那么我们就把 params 设置为一个空对象
    if(!params) params = {}
    // 接下来我们为 params 挂载一个 __cancelToken__ 属性(后续会用到),用于接收前文中的 source 对象
    // 如果为了严谨,可以设计成一个 symbol 类型
    params.__cancelToken__ = function(source){
      fn.source = source
    }
    return api(params)
  }
  const cancel = function(){
    fn.source && fn.source.cancel() // 直接调用 source.cancel 方法即可
  }
  return {fn,cancel}
}

看了这个方法,是不是有点一脸懵逼?

别慌,我们来看下使用方法,就大致明白了。

我们以 request.js 中的 getApple 函数为例:

// _getApple 就是我们上文中返回的 fn
const {fn:_getApple,cancel} = useCanCancelRequest(getApple)
// 这里的参数对应 useCanCancelRequest 函数中的 params
_getApple({page:10pageSize:100}).then((res) => {
  console.log("__SY__🎄 ~ getApple ~ res:", res);
});
cancel 方法的实现

到这里彦祖们可能会说也没看到 cancel 的使用啊!

好吧,接下来,我们来看下 cancel 的使用。

使用 cancel 前, 我们还需要修改 request.js 文件(只展示关键代码)

service.interceptors.request.use(
  async (config) => {
    // 前文中的 params.__cancelToken__ 参数, 就是 config?.data?.__cancelToken__ 参数
    if (typeof config?.data?.__cancelToken__ === 'function') {
      const cancelTokenSource = axios.CancelToken.source() // 创建一个 source 对象
      config.cancelToken = cancelTokenSource.token // 把 token 挂载到 config 上
      config.data.__cancelToken__(cancelTokenSource) // 把 source 对象回调给 useCanCancelRequest 函数内部
      Reflect.deleteProperty(config?.data'__cancelToken__') // 当然我们还需要把 __cancelToken__ 属性删除
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

这样我们就完成了一个比较完整的取消请求的功能封装。

接下来看下如何使用:

const {fn:_getApple,cancel} = useCanCancelRequest(getApple)
_getApple({page:10pageSize:100}).then((res) => {
  console.log("__SY__🎄 ~ getApple ~ res:", res);
});
setTimeout(() => {
  cancel() // 取消请求
},20)

当然我们在测试的时候,最好把 chrome devtools 网络面板的节流模式改成 3G 模式 否则请求太快可能直接变成 404 看不出效果

利用 useCanCancelRequest 解决竟态问题

我们看下如何利用这个方法解决竟态问题:

mounted(){
    const {fn:_getApple,cancel:cancelApple} = useCanCancelRequest(getApple)
    const {fn:_geOrange,cancel:cancelOrange} = useCanCancelRequest(geOrange)
    const {fn:_getBanana,cancel:cancelBanana} = useCanCancelRequest(getBanana)
    this.cancelApple = cancelApple
    this.cancelOrange = cancelOrange
    this.cancelBanana = cancelBanana
},
methods:{
    getApple(){
        // 也就是说在每次请求前,我们需要调用三次 cancel 方法来取消前置请求
        this.cancelApple()
        this.cancelOrange()
        this.cancelBanana()
        // 发送真实请求
        ...
    }
}

完美的 http 竟态解决方案

其实我们利用 useCanCancelRequest 就可以解决竟态问题,但是还是过于复杂。

假设用户有 10 个 api 需要取消,那么我们就需要调用 10 次 cancel 方法。

这极大的增加了开发者心智负担,那么我们该如何优化呢?

设计思路

既然我们只要保证最后一个请求成功,逆向思维思考一下。

我们只要把前置请求全部取消即可。

那么第一步我们需要把所有的 api 函数收集起来。

开始实现

接下来我们来看下如何实现:

// 收集所有的 apis
function useUniqueRequest(apis){
  // 大致思路就是 apis 中的某个函数被调用,我们就取消其他 api 的请求
  // 我们要达到的目的就是
  // 用伪代码表述一下
  apis.forEach(api=>{
    cancelAll() // api 执行前取消其他 api 的请求
    api()
  })
}

现有问题

目前遇到两个问题:

  1. 如何命中 api 请求的时机?

  2. cancelAll 方法如何实现?

如何命中 api 请求的时机?

api 的请求时机完全是有业务端控制的,我们无法得知,这该如何解决呢?

其实不难,我们可以返回一个自定义函数让开发者调用

当开发者调用自定义函数 = 调用 api 函数效果即可

这样不就能掌控 api 的请求时机了吗?

具体来看代码

function useUniqueRequest(apis){
  return apis.map(api=>{
    // 返回自定义函数
    const executor = function(params){
      // 这里可以掌控 api 的请求时机,执行相关逻辑
      return api(params)
    }
    return executor
  })
}
// 看下使用方式,此处如果有更好的数据结构设计,欢迎评论区留言
const [_getApple, _geOrange, _getBanana] = useUniqueRequest([getApple, geOrange, getBanana]) 

_getApple, _geOrange, _getBanana 三个函数的执行也会执行我们的自定义函数 executor

这样不就能控制 api 的请求时机了吗?

cancelAll 方法如何实现?

上文中,我们完成了 api 请求时机的控制,接下来让我们实现 cancelAll 方法

其实 useCanCancelRequest 内部不就有一个cancel方法吗?

让我们来回顾下它的使用方式:

// 这里的 api 不就是我们的 apis.map 中的 api 吗?
const { fn, cancel } = useCanCancelRequest(api) 

得益于 useCanCancelRequest 函数的设计。

那么我们只需要在 apis.map 的时候收集对应的 cancel 方法即可。

具体实现:

function useUniqueRequest (apis) {
  const cancelList = [] // 维护一个 cancel 方法的列表
  const cancelAll = () => cancelList.forEach(c => c()) // 取消所有请求
  return apis.map(api => {
    const { fn, cancel } = useCanCancelRequest(api) // 收集 cancel 方法
    cancelList.push(cancel) // 把 cancel 维护到 cancelList 列表中

    // 返回自定义函数
    const executor = function (params) {
      cancelAll() // 取消所有 api 的请求
      return fn(params)
    }
    return executor
  })
}

cancelAll 方法的实现也就此完成了。

完整代码

codesandbox.io/p/sandbox/r…

写在最后

彦祖们,如果有兴趣,强烈推荐大家阅读 <<vue.js 设计与实现>> 一书

其中的 effect 函数实现,十分精彩!力荐!

感谢彦祖们的阅读

个人能力有限

如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟

彩蛋

宁波团队还有一个资深前端hc, 带你实现海鲜自由。 欢迎彦祖们私信😚

转载自:https://juejin.cn/post/7414316246613278720
评论
请登录