基于 axios 的 http 竟态问题解决方案--- theme: channing-cyan highlight:
前言
本文仅针对 axios
库, 如果你使用的是 fetch
或者 xhr
, 请自行适配处理。
彦祖们也可以先看下完整代码(有助于下文的理解)
效果预览
自动把前置的 http 请求全部取消,只保留最后一个有效请求。
什么是 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:10,pageSize: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:10,pageSize: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:10,pageSize: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()
})
}
现有问题
目前遇到两个问题:
-
如何命中 api 请求的时机?
-
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
方法的实现也就此完成了。
完整代码
写在最后
彦祖们,如果有兴趣,强烈推荐大家阅读 <<vue.js 设计与实现>>
一书
其中的 effect
函数实现,十分精彩!力荐!
感谢彦祖们的阅读
个人能力有限
如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟
彩蛋
宁波团队还有一个资深前端
hc, 带你实现海鲜自由
。 欢迎彦祖们私信😚
转载自:https://juejin.cn/post/7414316246613278720