问题描述&背景
- 下拉选择框,支持搜索,搜索时携带参数调用接口并更新下拉选项
- 下拉选择连续进行多次搜索,先请求但响应时间长的返回值会覆盖后请求但响应时间短的
- 举例:
- 搜索后先清空选项,再输入内容进行搜索。清空后查询全量数据接口响应时间更长,覆盖搜索过滤后的数据
问题分析
- 连续多次请求导致问题
- 通过防抖
debounce
函数,限制短期内无法重复调用接口 - 使用lodash
的debounce
函数实现 - 若接口响应时间相差较大,仍会有覆盖问题,需要结合以下方法
- 通过防抖
- 接口响应慢导致问题
- 优化接口,如减少后端非必要属性的计算,提高响应速度 - 后端优化
- 接口调用重复问题
- 通过一些方法保证最后显示的数据为最晚发起的那个请求返回的值,方案见下文
方案选择及具体实施
- 在前一个请求响应前,阻止发起下一个请求(问题中通过禁用选择框
disabled=true
实现),避免返回值覆盖- 实现方法
- 接口请求时将组件的
disabled
和loading
均设置为true
- 返回后设置为
false
- 接口请求时将组件的
- 优点
- 可以减少接口调用,防止返回值相互覆盖
- 缺点
- 禁用选择框会让其失去焦点,用户需要再次点击输入
- 禁用状态的切换使得操作不连贯,用户有明显的感知,体验下降
- 需要操作页面元素,需要额外代码
- 实现方法
- 发送请求时,通过给每次请求添加一个序列号或时间戳,然后在处理响应时进行匹配,确保每次返回的结果与其对应
- 实现方法
- 发送请求时生成唯一的标识符(时间戳)
- 处理响应时保存该标识符
- 匹配标识符更新数据
- 优点
- 可以找出最新的请求赋值,保证数据为最后请求的
- 缺点
- 需要多次更新使用的数据
- 需要生成标识并用额外的变量储存标识,逻辑改动较大
- 没有实际减少或取消无效的请求,占用资源多
- 实现方法
- 发起新的请求时取消尚未完成的请求 ⭐️
- 运用技术
axios
的取消请求:axios取消请求- 项目使用的axios版本为
0.21.1
,使用CancelToken
- 更高版本使用
AbortControllerAbortController
:AbortController() 构造函数
- 运用技术
AbortController实现方法
const controller = new AbortController();
const {data: { data },
} = await this.$http.get('/api/v1/xxx'params,signal: controller.signal
})
// 取消请求
controller.abort()
CancelToken实现方法
- 在data中定义cancelToken用于保存当前请求token
data() {return {...cancelToken: null,}
},
- 在查询方法中进行如下配置
// 防抖
searchOptions: debounce(async function (searchString) {// 取消上一次的请求if (this.cancelToken) {this.cancelToken.cancel()}// 创建 cancelTokenthis.cancelToken = axios.CancelToken.source()this.loading = trueconst params = {...}const {data: { data },} = await this.$http.get('/api/v1/xxx'params,cancelToken: this.cancelToken.token, // 请求时传入token})// 数据处理...this.loading = false// 清除cancelTokenthis.cancelToken = null},300,{leading: false,trailing: true,}
),
- 使用第三方插件进行优化 ⭐️
- 插件名称:axios-extensions
- 功能:缓存请求结果,节流请求,请求重试
- 缓存请求:cacheAdapterEnhancer
cacheAdapterEnhancer(axios.defaults.adapter, option)
option
对象,可选enabledByDefault
:是否默认缓存,Boolean
类型, 默认是true
(缓存),false
(不缓存)cacheFlag
:是否通过flag
方式缓存,字符串类型, 只有flag
一样才会缓存,flag
不对或者没设置的都不会缓存。defaultCache
:可以配置maxAge
(缓存有效时间, 毫秒单位),默认是5分钟,max
(支持缓存的请求的最大个数),默认是100个
- 完整代码 🚀
import axios from 'axios'
import { Cache, cacheAdapterEnhancer } from 'axios-extensions'const request = axios.create({baseURL: process.env.BASE_URL,adapter: cacheAdapterEnhancer(axios.defaults.adapter, {defaultCache: new Cache({ maxAge: 2000, max: 100 }),}),
})
- 扩展:
- 节流请求:throttleAdapterEnhancer
throttleAdapterEnhancer(adapter, options)
option
对象,可选threshold
:限制请求调用的毫秒数,数字类型, 默认是1000cache
:可以配置max
(节流请求的最大个数),默认是100个
- 请求重试:retryAdapterEnhancer
retryAdapterEnhancer(adapter, options)
option
对象,可选times
:重试的次数,Number
类型, 默认是2,请求失败后会重试2次。
- 节流请求:throttleAdapterEnhancer
优化效果
- 杜绝先发起的请求结果覆盖旧发起的请求结果的情况
- 新的请求发起,取消当前进行中的请求,减少无用的请求调用
- 无需修改其他的业务逻辑,无需引入更多变量记录请求状态
- 用户没有感知,不会增加额外的用户操作
功能封装
- 封装一个基于
axios
的 HTTP 请求管理类:http.ts 🚀
import axios, { AxiosRequestConfig, CancelTokenSource } from 'axios'// 引入请求函数 包含一些请求拦截器或其他设置
import { request } from './axiosConfig' // 枚举 指定响应数据的格式(这里只举例1种返回体格式)
enum ResponseType {ResData, // 返回 res.data
}
// 完整的响应对象结构
interface ApiResponse<T> {data: {code: numbermessage: stringdata: T | null | undefined}status: numberheaders?: Record<string, string>config?: anyrequest?: any
}
// 异步请求的结果
type HttpResult<T> = Promise<T | ApiResponse<T> | any> // 扩展了 Axios 的请求配置,添加了两个自定义字段以支持请求取消功能
interface CustomAxiosRequestConfig extends AxiosRequestConfig {cancelPrevious?: boolean // 是否取消之前的请求cancelTokenId?: string // 保存取消请求tokenId
}class Http {// 存储 Http 类的实例,以便实现单例模式private static instancesMap: Map<string, Http> = new Map()// 存储与请求 URL 关联的取消令牌源,用于实现请求取消功能private static cancelTokenIdSourceMap: Map<string, CancelTokenSource> =new Map()private requestFunction: (config: AxiosRequestConfig) => Promise<any> // 请求方法private responseType: ResponseType = ResponseType.ResData // 相应数据格式类型// 构造函数-接收参数以配置请求函数、响应类型和公共URL前缀,同时初始化相关属性constructor({requestMethod = request,responseType = ResponseType.ResData,}: {requestMethod?: (config: AxiosRequestConfig) => Promise<any>}) {this.requestFunction = requestMethodthis.responseType = responseType}// 私有异步方法,用于执行 HTTP 请求,接受请求方法、URL 和配置private async createRequest<T>({method,url,config,}: {method: 'get' | 'post' | 'delete' | 'put'url: stringconfig: CustomAxiosRequestConfig}): HttpResult<T> {let source, cancelTokenIdif (config?.cancelPrevious) {// 取消之前的请求cancelTokenId = config?.cancelTokenId ?? this.getCancelTokenId(url)this.cancelPreviousRequest(cancelTokenId)// 创建新的取消令牌source = axios.CancelToken.source()}// 准备请求配置const requestConfig: AxiosRequestConfig = {...config,method,url,cancelToken: source?.token,}// 请求try {// 保存取消令牌if (cancelTokenId) Http.cancelTokenIdSourceMap.set(cancelTokenId, source)// 发起请求const res = await this.requestFunction(requestConfig)// 没有遇到重复请求-清空取消令牌if (cancelTokenId) Http.cancelTokenIdSourceMap.delete(cancelTokenId)// 返回响应值if (this.responseType === ResponseType.ResData) {return res.data as T} else {return res as ApiResponse<T>}} catch (error) { // 错误处理if (axios.isCancel(error)) {console.error('Request canceled', error.message)} else {if (cancelTokenId) Http.cancelTokenIdSourceMap.delete(cancelTokenId)console.error('Error:', error)}throw error}}private cancelPreviousRequest(cancelTokenId: string): void {const source = Http.cancelTokenIdSourceMap.get(cancelTokenId)source?.cancel(`Cancelled request ${cancelTokenId}`)}private getCancelTokenId(url: string): string {return url.split('?')[0] // 提取非 query 部分, 防止同一个get请求不同query时没取消}// 实现get方法public get<T>(url: string,config?: CustomAxiosRequestConfig): HttpResult<T> {return this.createRequest<T>({ method: 'get', url, config })}// 实现post方法public post<T>(url: string,data?: any,config?: CustomAxiosRequestConfig): HttpResult<T> {return this.createRequest<T>({method: 'post',url,config: { ...config, data },})}// 实现delete方法public delete<T>(url: string,config?: CustomAxiosRequestConfig): HttpResult<T> {return this.createRequest<T>({ method: 'delete', url, config })}// 实现put方法public put<T>(url: string,data?: any,config?: CustomAxiosRequestConfig): HttpResult<T> {return this.createRequest<T>({method: 'put',url,config: { ...config, data },})}// 单例// 该方法检查是否已经存在相同 ID 的实例,如果不存在,则创建一个新的实例并存储在 instancesMap 中。// 这样做的目的是减少同类实例的创建,确保在应用中使用的是同一个 Http 实例,从而管理配置和状态public static getInstance({requestMethod = request,responseType = ResponseType.ResData,instanceId = 'http',}: {requestMethod?: (config: AxiosRequestConfig) => Promise<any>responseType?: ResponseTypeinstanceId?: string}): Http {let instance = Http.instancesMap.get(instanceId)if (!instance) {instance = new Http({ requestMethod, responseType })Http.instancesMap.set(instanceId, instance)}return instance}
}// 导出实例
export const http = Http.getInstance({requestMethod: request,responseType: ResponseType.ResData,instanceId: 'http',
})
- 补充:
// Axios 请求实例
const request = axios.create({baseURL: process.env.BASE_URL,adapter: cacheAdapterEnhancer(axios.defaults.adapter, {defaultCache: new Cache({ maxAge: 2000, max: 100 }),}),
})
- 使用
await this.$http.post(`/xxx/xxx/${this.id}/xxx`,params
)
参考文档
来学习下axios的扩展插件1
来学习下axios的扩展插件2