场景
最近在开发询价功能的时候遇到一个问题,功能是传参调用接口获取价格,并在前端展示出来,每个参数的改变都会调用询价接口,重新获取价格。本来是没什么的,问题在于该接口的入参涉及多个参数,这部分代码的函数又彼此嵌套(吐槽...这部分代码阅读起来极其上头!),一个入参改变就会引起其他参数的改变,导致询价接口发生多次调用。
理想情况下,前端只会展示最后一个接口返回的价格。然而实际情况是,接口的处理时间不同,导致最后一个响应的请求≠最后一个发送的请求。由于每次请求入参不同,导致返回结果,极端情况下,在调用了两三次请求后,搞不好最先发送的请求在最后返回,导致前端价格展示错误。
价格展示极大关联用户感知,修改BUG迫在眉睫,第一次遇到这种重复请求问题的我开始寻找解决办法,最终经过亲测,有以下两种办法对我当前项目适用。
方法1. 使用全局Id标记该请求
存储一个全局id,每次发送请求时+1,在接收请求回调时使用存储的id判断该回调是否为最后一次请求的回调。
// 页面初始化时,存储全局Id
onMounted(() => {
sessionStorage.setItem('priceId', '0')
}
// 发送请求,只取最后一次请求的返回值
function queryPrice(data) {
transmitData.priceLoading = true
// 全局priceId加1
const priceId = Number(sessionStorage.getItem('priceId')) + 1
sessionStorage.setItem('priceId', String(priceId))
// 调用接口发送请求
queryCheckoutPrice(data)
.then(res => {
// 判断当前请求是否是最后一次请求,若是则取返回值
if (String(priceId) === sessionStorage.getItem('priceId')) {
// 处理数据并展示
// .............
}
})
.catch(err => {
console.log('err in queryCheckoutPrice', err)
})
.finally(() => {
transmitData.priceLoading = false
})
}
方法2. 使用Axios取消请求 - CancelToken
Axios提供了取消请求的多种方法:取消请求 | Axios中文文档 | Axios中文网 (axios-http.cn)
1. AbortController:Axios 支持以 fetch API 方式—— AbortController 取消请求。
2. 使用CancelToken取消请求:
(1)调用 CancelToken
的静态方法 source
创建一个 cancel token。
(2)CancelToken
的构造函数实例化一个来创建 cancel token。
这里我使用CancelToken的第2种方法,下面贴出主要代码:
import axios from 'axios'
import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import { defaultConfig } from './config'
import { isPlainObject, merge } from 'lodash-es'
import { ResponseStatus } from './httpCode'
const CancelToken = axios.CancelToken // axios取消token
const cancelTokenObj = {} // 用于储存每个请求的取消函数和请求标识
const whiteList = ['/queryCheckoutPriceForOp'] // 不允许多次重复调用的API名单
// 取消被多次发起的请求,只保留最后一个请求
const cancelAjax = (type, config) => {
// 取出请求路径作为key
const key = config.url
if (type === 'check') {
// 1. 若该url的请求未被记录,则记录改请求,并为其赋值取消函数
if (!cancelTokenObj[key]) {
config.cancelToken = new CancelToken(cancelFunc => {
cancelTokenObj[key] = cancelFunc
})
// 2. 若该url的请求已存在,则取消之前对该url发起的请求. (取消上一个,保留本次请求)
} else {
if (cancelTokenObj[key] !== undefined) {
cancelTokenObj[key]()
}
}
} else if (type === 'remove') {
// 最后一个请求结束,移除对该url请求的记录
if (cancelTokenObj[key] !== undefined) {
delete cancelTokenObj[key]
}
}
}
export function Http(options = {}) {
// 创建axios实例
const instance = axios.create(defaultConfig)
// 请求拦截器,
instance.interceptors.request.use(
function (config: InternalAxiosRequestConfig) {
// 添加请求取消功能
if (whiteList.includes(config.url)) {
cancelAjax('check', config)
}
return config
},
function (error) {
return Promise.reject(error)
}
)
// 响应拦截器
instance.interceptors.response.use(
// 默认的拦截器
function (response: AxiosResponse<any, any>): AxiosResponse<any, any> | Promise<AxiosResponse<any, any>> {
// 包含了code>=200和code<400的请求,同时满足业务code要求
if (response?.data && Object.prototype.hasOwnProperty.call(response.data, 'returnCode')) {
// 但是业务系统自己定义的业务code:returnCode=000000表示成功
if (response.data.returnCode === ResponseStatus.SUCCESS) {
// 最后一次请求获取完成,移除该请求取消功能
cancelAjax('remove', response.config)
return response.data
} else {
// 业务失败请求逻辑处理
// ......
}
} else {
// 其他失败请求逻辑处理
// ......
// throw new Error(`${response.config.url}接口返回数据格式错误`)
}
}
)
return instance
}
需要注意的是,如果在请求被取消之前,请求已经被发送并成功响应,那么该已经成功响应的请求结果无法被取消。因此也需要对已经成功响应的请求结果做好处理。另外,也需要处理请求被取消的异常情况,以避免出现错误。
目前暂时使用方法1.
TODO:
CancelToken原理:axios的cancelToken取消机制原理 - 前端小记 - SegmentFault 思否
参考: