使用到的技术/库:axios、TS、发布订阅者模式。
本文将使用发布订阅者模式来处理重复的axios请求。将实现“一定时间内发送多个相同的请求时,只向服务器发出第一个请求,然后将第一个请求的响应结果分发给各个请求方法”的效果。
前言
首先,我们要先定义何为重复的请求?
- 接口地址、类型、参数相同,则视为相同的请求;
- 上一个请求还未返回响应结果时,又发送一个相同的请求,则视为重复请求。
其次,我们将每次请求生成一个唯一的key并缓存起来,用作判断是否已存在相同的请求。
最后,我们需要实现一个发布订阅者模式,在存在重复请求时,中断请求,并添加订阅,当之前的请求结果返回时,发布给订阅者。
还有一个问题,如何中断请求?只需在axios的请求拦截器中返回一个Promise.reject()即可,这样将会直接执行axios的响应拦截器中的错误处理方法,而不会向服务器发送请求。
技术实现
1. 生成key
根据接口的地址、类型、参数,我们可以判断是否为相同的请求,那我们可以在key中包含这些关键信息。
import { type AxiosRequestConfig } from 'axios' const getDataType = (obj: unknown) => { let res = Object.prototype.toString.call(obj).split(' ')[1] res = res.substring(0, res.length - 1).toLowerCase() return res } const getKey = (config: AxiosRequestConfig) => { const { method, url, data, params } = config; let key = `${method}-${url}`; try { if (data && getDataType(data) === 'object') { key += `-${JSON.stringify(data)}`; } else if (getDataType(data) === 'formdata') { for (const [k, v] of data.entries()) { if (v instanceof Blob) { continue; } key += `-${k}-${v}`; } } if (params && getDataType(params) === 'object') { key += `-${JSON.stringify(params)}`; } } catch (e) {console.error(e);} return key; };
判断参数类型是为了处理FormData
、二进制等格式情况。
2. 缓存请求
我们可以创建一个全局变量来保存请求信息,在请求拦截器(发送请求之前)中将请求信息添加至全局变量,然后在响应拦截器(请求完成之后)中移除相关信息。
import axios from 'axios'; const historyRequests = new Map<string, number>() axios.interceptors.request.use( (config) => { // 生成key const key = createKey(config); // 响应拦截器中需要用到 config.headers.key = key; // 缓存请求信息 historyRequests.set(key, 1); return config; }, (error) => { return Promise.reject(error); } ); instance.interceptors.response.use( (res) => { // 请求完成,删除缓存信息 const key = res.config.headers.key as string; if (historyRequests.has(key)) { historyRequests.delete(key); } return res; }, (error) => { return Promise.reject(error); } );
3. 发布订阅者模式实现
订阅者通过注册事件到调度中心,当发布者触发事件时,由调度中心执行相应的订阅事件。
export default class EventBus<T extends Record<string | symbol, any>> { private listeners: Record<keyof T, ((...args: any[]) => void)[]> = {} as any $on<K extends keyof T>(event: K, callback: T[K]) { if (!this.listeners[event]) { this.listeners[event] = [] } this.listeners[event].push(callback) } $emit<K extends keyof T>(event: K, ...args: Parameters<T[K]>) { const callbacks = this.listeners[event] if (!callbacks || callbacks?.length === 0) { return } callbacks.forEach((callback) => { callback(...args) }) } $off<K extends keyof T>(event: K, listener?: T[K]) { if (!listener) { delete this.listeners[event] return } const fns = this.listeners[event] if (!fns || !fns.length) { return } const idx = fns.indexOf(listener) if (idx !== -1) { fns.splice(idx, 1) } } clear() { this.listeners = {} as any } }
4. 完整代码实现
import axios, { type AxiosRequestConfig, AxiosError, type AxiosResponse } from 'axios'; import { getDataType } from './utils'; import { EventBus } from './event/eventBus'; interface IBaseResponse { code: number; msg: string; data?: unknown; } const createKey = (config: AxiosRequestConfig) => { const { method, url, data, params } = config; let key = `${method}-${url}`; try { if (data && getDataType(data) === 'object') { key += `-${JSON.stringify(data)}`; } else if (getDataType(data) === 'formdata') { for (const [k, v] of data.entries()) { if (v instanceof Blob) { continue } key += `-${k}-${v}`; } } if (params && getDataType(params) === 'object') { key += `-${JSON.stringify(params)}`; } } catch (e) {console.error(e);} return key; }; const instance = axios.create({ baseURL: '', timeout: 5000, withCredentials: true, headers: { 'Content-Type': 'application/json;' } }); const historyRequests = new Map<string, number>(); instance.interceptors.request.use( (config) => { const key = createKey(config); // 在响应拦截器中需要用到该key发布/订阅事件 config.headers.key = key; // 判断是否存在相同请求,存在则中断请求 if (historyRequests.has(key)) { // 为了后续方便处理中断请求超时 config.headers.requestTime = Date.now(); // 抛出错误并传递相应参数 return Promise.reject( new AxiosError('Redundant request', 'ERR_REPEATED', config) ); } historyRequests.set(key, 1); return config; }, (error: AxiosError) => { return Promise.reject(error); } ); const responseInterceptor = (res: AxiosResponse<IBaseResponse | Blob>) => { const result: [ AxiosResponse<IBaseResponse | Blob> | undefined, AxiosError | undefined ] = [undefined, undefined]; const data = res.data; // 可根据你的接口响应数据作处理,这里假设code不为200都为错误 if (data instanceof Blob || data.code === 200) { result[0] = res; } else { result[1] = new AxiosError(data.msg); } return result; }; const eventBus = new EventBus<{ [key: string]: ( data?: AxiosResponse<IBaseResponse | Blob>, error?: AxiosError ) => void; }>(); instance.interceptors.response.use( (res) => { const [data, error] = responseInterceptor(res); // 如果存在重复请求,则发布结果,执行订阅事件 const key = res.config.headers.key as string; if (historyRequests.has(key)) { historyRequests.delete(key); eventBus.$emit(key, data, error); } return data !== undefined ? data : Promise.reject(error); }, (error: AxiosError) => { // 处理中断的重复请求 if (error.code === 'ERR_REPEATED') { return new Promise((resolve, reject) => { const config = error.config!; const key = config.headers.key as string; const callback = ( res?: AxiosResponse<IBaseResponse | Blob>, err?: AxiosError ) => { res ? resolve(res) : reject(err); eventBus.$off(key, callback); }; // 订阅事件 eventBus.$on(key, callback); // 处理超时 const timeout = config.timeout || 5000; const requestTime = config.headers.requestTime as number; const now = Date.now(); if (now - requestTime > timeout) { historyRequests.delete(key); const error = new AxiosError( `timeout of ${timeout}ms exceeded`, 'ECONNABORTED', config ); error.name = 'AxiosError'; eventBus.$emit(key, undefined, error); } }); } return Promise.reject(error); } );
可以根据你的实际需求调整相关代码,例如:你需要对某些请求放行,则可以通过在headers
中添加一个属性来控制是否允许重复请求。
以上就是axios处理重复请求的方法小结的详细内容,更多关于axios处理重复请求的资料请关注站长课堂其它相关文章!