哈喽,我是JL😄
共同成长是我一直以来的追求!✨
希望下面分享能让你有所收获💦
背景
日常开发中我们经常会用到搜索框进行搜索:
当我们输入数字不断回车,或者不断点击旁边的搜索按钮,正常情况下会发送请求,或者其他场景如按钮点击等,在弱网情况情况下,或者接口卡的情况下,就会不断触发相同http请求,即可能导致的问题:
1. 重复http请求过多,造成性能浪费。
2. 可能出现当前搜搜结果返回之前请求的接口,导致数据错乱。
那么我们就需要配置取消重复请求,同时,往往我们需要在已有项目上配置,有可能出现改动成本大的情况,下面介绍一种比较少改动的方法,结合常规的取消重复请求配置,用最小成本去嵌入已有项目中。
常规解决方案:取消重复请求
定义: 完全相同的接口在上一个pending状态时,自动取消下一个请求。
Axios是一个强大的基于Promise的客户端,被广泛应用于各公司项目,这里主要讲Axios如何配置取消重复请求。
1️⃣ abort
Axios 底层通过 XMLHttpRequest 对象来发起 HTTP 请求,该对象提供 abort方法来取消请求:
当然,这种方法比较原始,我们来看另一种目前普遍流行的方法。
2️⃣ CancelToken
Axios内部是已经封装好另一个取消请求的方法,通过CancelToken达到取消请求的操作:
也可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建一个 cancel token:
知道了请求,接下来我们看一下怎么去配置化以及无缝接入已有项目通用Axios当中。
思路
1️⃣ 辅助函数 generateReqKey
需要一个存储方法generateReqKey,存储Map对象pendingRequest,根据当前请求信息,存储请求的唯一key值、以及取消函数。
唯一key值可指:请求方法、参数、地址,可通过qs进行序列化,增加点安全性~
function generateReqKey (config) {
const { method, url, params, data } = config
return [method, url, Qs.stringify(params), Qs.stringify(data)].join("&")
}
2️⃣ 辅助函数 pendingRequest
需要一个pendingRequest方法,用于把当前请求信息添加到pendingRequest对象中,这里用到Axios取消方法CancelToken
const pendingRequest = new Map()
function addPendingRequest (config) {
const requestKey = generateReqKey(config) // 配合上面generateReqKey方法
config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
if (!pendingRequest.has(requestKey)) {
pendingRequest.set(requestKey, cancel)
}
})
}
3️⃣ 辅助函数 removePendingRequest
需要一个removePendingRequest方法,用来检查是否存在重复请求,若存在则取消已发请求并清除对应的存储。
function removePendingRequest (config) {
const requestKey = generateReqKey(config) // 配合上面generateReqKey方法
if (pendingRequest.has(requestKey)) {
const cancelToken = pendingRequest.get(requestKey)
cancelToken(requestKey)
pendingRequest.delete(requestKey)
}
}
4️⃣ 配置化
/**
* @description axios实例配置化
* @param {Boolean} reqCancel
* @returns {any}
*/
const http = (userCancel = false) => {
const service = axios.create({
withCredentials: true, // 允许跨域携带cookie
timeout: 30000
})
// request interceptor
service.interceptors.request.use(config => {
... do some thing
if (reqCancel) { // 重复请求处理
removePendingRequest(config)
addPendingRequest(config)
}
return config
}, error => {
return Promise.reject(error)
})
service.interceptors.response.use(response => {
reqCancel && removePendiingRequest(response.config) // 移除重复请求
... do some thing
}, error => {
... do some thing
})
}
大部分逻辑沿用原先项目,只需要抽离出配置函数http,嵌入辅助函数1,2,3等。
5️⃣ 适配通用Axios
正常项目会配置axios,然后导出:
// axios.js
export default axios // 导出
// main.js
import axios from '@/util/axios' // 引入
...
Vue.prototype.$http = axios
// 页面上使用
...
this.$http.post()...
适配后,我们可以导出两种请求方式,一种通用,一种定制化
// axios.js
export const reqFree = http() // 通用
export const reqWithCancel = http(true) // 取消重复请求
// main.js
import { reqFree, reqWithCancel } from '@/util/axios'
...
Vue.prototype.$http = reqFree
Vue.prototype.$cancelHttp = reqWithCancel
页面上使用:
this.$http.post()... // 通用不需要变动
this.$cancelHttp.post()... // 取消重复请求用法
通过这种配置,可以无缝嵌入到已有项目当中,不需要调整通用请求。
扩展
在项目开发中遇到另一种比较实用的方案,记录一下:
思路: 活用axios cancel token 取消重复请求,vuex或者pinia抛出方法存储对应请求的url以及cancel token以及判断是否存在重复请求,存在即调用对应url的cancel方法取消,这里通过数组存储映射关系。
// 以vuex为例
// store
const state = {
cancelMap = []
}
...
const mutations = {
SET_CANCEL_MAP: (state, { key, cancel }) => {
const source = state.cancelMap.find(item => item.key === key)
if (source) { // 存在重复请求
source.source.cancel() // 取消对应请求
state.cancelMap = state.cancelMap.filter(item => item.key !== key)
}
state.cancelMap.push({
key,
source: cancel
})
},
DELETE_CANCEL_MAP: (state, key) => {
state.cancelMap = state.cancelMap.filter(i => i.key !== key)
}
}
...
const actions = {
setCancelMap ({ commit }, { key, cancel }) {
commit('SET_CANCEL_MAP', { key, cancel })
},
deleteCancelMap ({ commit }, { key }) {
commit('DELETE_CANCEL_MAP', { key })
},
}
...
// xxx.vue
import axios from 'axios'
import { mapState, mapActions } from 'vuex'
methods: {
...mapActions('xxx', ['setCancelMap']),
getList () {
const cancelToken = axios.CancelToken
const source = cancelToken.source()
const key = 'getList/' // 这里用url作为键值
this.setCancelMap({ key, cancel: source }) // 存储映射关系
this.$http.post(key, {
xxx: xxx
}, { // get是第二个参数,post是第三个参数
cancelToken: source.token
}).catch(err => {
// 加上判断,不然取消会执行这块逻辑
if (axios.isCancel(err)) return
// do some thing
}).finally(() => {
this.deleteCancelMap(key)
})
}
}
这种方式还可以延伸其他使用方法,例如切换菜单栏清除之前菜单栏未加载成功的请求等。
结语
希望能给你带来帮助✨~
分享不易,点赞鼓励咯🤞