背景
保存按钮点击多次,造成新增多个单据
列表页疯狂刷新,导致服务器压力大
如何彻底解决
方案:我的思路从请求层面判断相同请求只发送一次,将结果派发给各个订阅者
实现思路
对请求进行数据进行hash
添加store 存储 hash => Array promise
相同请求,直接订阅对应的promise
请求取消,则将store中对应的promise置为null
请求返回后,调用所有未取消的订阅
核心代码
private handleFinish(key: string, index: number) { const promises = this.store.get(key); // 只有一个promise时则删除store if (promises?.filter((item) => item).length === 1) { this.store.delete(key); } else if (promises && promises[index]) { // 还有其他请求,则将当前取消的、或者完成的置为null promises[index] = null; } } private async handleRequest(config: any) { const hash = sha256.create(); hash.update( JSON.stringify({ params: config.params, data: config.data, url: config.url, method: config.method, }), ); const fetchKey = hash.hex().slice(0, 40); const promises = this.store.get(fetchKey); const index = promises?.length || 0; let promise = promises?.find((item) => item); const controller = new AbortController(); if (config.signal) { config.signal.onabort = (reason: any) => { const _promises = this.store.get(fetchKey)?.filter((item) => item); if (_promises?.length === 1) { controller.abort(reason); this.handleFinish(fetchKey, index); } }; } if (!promise) { promise = this.instance({ ...config, signal: controller.signal, headers: { ...config.headers, fetchKey, }, }).catch((error) => { console.log(error, "请求错误"); // 失败的话,立即删除,可以重试 this.handleFinish(fetchKey, index); return { error }; }); } const newPromise = Promise.resolve(promise) .then((result: any) => { if (config.signal?.aborted) { this.handleFinish(fetchKey, index); return result; } return result; }) .finally(() => { setTimeout(() => { this.handleFinish(fetchKey, index); }, 500); }); this.store.set(fetchKey, [...(promises || []), newPromise]); return newPromise; }
以下为完整代码(仅供参考)
index.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; import { sha256 } from "js-sha256"; import transformResponseValue, { updateObjTimeToUtc } from "./utils"; type ErrorInfo = { message: string; status?: number; traceId?: string; version?: number; }; type MyAxiosOptions = AxiosRequestConfig & { goLogin: (type?: string) => void; onerror: (info: ErrorInfo) => void; getHeader: () => any; }; export type MyRequestConfigs = AxiosRequestConfig & { // 是否直接返回服务端返回的数据,默认false, 只返回 data useOriginData?: boolean; // 触发立即更新 flushApiHook?: boolean; ifHandleError?: boolean; }; type RequestResult<T, U> = U extends { useOriginData: true } ? T : T extends { data?: infer D } ? D : never; class LmAxios { private instance: AxiosInstance; private store: Map<string, Array<Promise<any> | null>>; private options: MyAxiosOptions; constructor(options: MyAxiosOptions) { this.instance = axios.create(options); this.options = options; this.store = new Map(); this.interceptorRequest(); this.interceptorResponse(); } // 统一处理为utcTime private interceptorRequest() { this.instance.interceptors.request.use( (config) => { if (config.params) { config.params = updateObjTimeToUtc(config.params); } if (config.data) { config.data = updateObjTimeToUtc(config.data); } return config; }, (error) => { console.log("intercept request error", error); Promise.reject(error); }, ); } // 统一处理为utcTime private interceptorResponse() { this.instance.interceptors.response.use( (response): any => { // 对响应数据做处理,以下根据实际数据结构改动!!... const [checked, errorInfo] = this.checkStatus(response); if (!checked) { return Promise.reject(errorInfo); } const disposition = response.headers["content-disposition"] || response.headers["Content-Disposition"]; // 文件处理 if (disposition && disposition.indexOf("attachment") !== -1) { const filenameReg = /filename\*?=['"]?(?:UTF-\d['"]*)?([^;\r\n"']*)['"]?;?/g; const filenames: string[] = []; disposition.replace(filenameReg, (r: any, r1: string) => { filenames.push(decodeURIComponent(r1)); }); return Promise.resolve({ filename: filenames[filenames.length - 1], data: response.data, }); } if (response) { return Promise.resolve(response.data); } }, (error) => { console.log("request error", error); if (error.message.indexOf("timeout") !== -1) { return Promise.reject({ message: "请求超时", }); } const [checked, errorInfo] = this.checkStatus(error.response); return Promise.reject(errorInfo); }, ); } private checkStatus( response: AxiosResponse<any>, ): [boolean] | [boolean, ErrorInfo] { const { code, message = "" } = response?.data || {}; const { headers, status } = response || {}; if (!status) { return [false]; } // 单地登录判断,弹出不同提示 if (status === 401) { this.options?.goLogin(); return [false]; } if (code === "ECONNABORTED" && message?.indexOf("timeout") !== -1) { return [ false, { message: "请求超时", }, ]; } if ([108, 109, 401].includes(code)) { this.options.goLogin(); return [false]; } if ((code >= 200 && code < 300) || code === 304) { // 如果http状态码正常,则直接返回数据 return [true]; } if (!code && ((status >= 200 && status < 300) || status === 304)) { return [true]; } let errorInfo = ""; const _code = code || status; switch (_code) { case -1: errorInfo = "远程服务响应失败,请稍后重试"; break; case 400: errorInfo = "400: 错误请求"; break; case 401: errorInfo = "401: 访问令牌无效或已过期"; break; case 403: errorInfo = message || "403: 拒绝访问"; break; case 404: errorInfo = "404: 资源不存在"; break; case 405: errorInfo = "405: 请求方法未允许"; break; case 408: errorInfo = "408: 请求超时"; break; case 500: errorInfo = message || "500: 访问服务失败"; break; case 501: errorInfo = "501: 未实现"; break; case 502: errorInfo = "502: 无效网关"; break; case 503: errorInfo = "503: 服务不可用"; break; default: errorInfo = "连接错误"; } return [ false, { message: errorInfo, status: _code, traceId: response?.data?.requestId, version: response.data.ver, }, ]; } private handleFinish(key: string, index: number) { const promises = this.store.get(key); if (promises?.filter((item) => item).length === 1) { this.store.delete(key); } else if (promises && promises[index]) { promises[index] = null; } } private async handleRequest(config: any) { const hash = sha256.create(); hash.update( JSON.stringify({ params: config.params, data: config.data, url: config.url, method: config.method, }), ); const fetchKey = hash.hex().slice(0, 40); const promises = this.store.get(fetchKey); const index = promises?.length || 0; let promise = promises?.find((item) => item); const controller = new AbortController(); if (config.signal) { config.signal.onabort = (reason: any) => { const _promises = this.store.get(fetchKey)?.filter((item) => item); if (_promises?.length === 1) { controller.abort(reason); this.handleFinish(fetchKey, index); } }; } if (!promise) { promise = this.instance({ ...config, signal: controller.signal, headers: { ...config.headers, fetchKey, }, }).catch((error) => { console.log(error, "请求错误"); // 失败的话,立即删除,可以重试 this.handleFinish(fetchKey, index); return { error }; }); } const newPromise = Promise.resolve(promise) .then((result: any) => { if (config.signal?.aborted) { this.handleFinish(fetchKey, index); return result; } return result; }) .finally(() => { setTimeout(() => { this.handleFinish(fetchKey, index); }, 500); }); this.store.set(fetchKey, [...(promises || []), newPromise]); return newPromise; } // add override type public async request<T = unknown, U extends MyRequestConfigs = {}>( url: string, config: U, ): Promise<RequestResult<T, U> | null> { // todo const options = { url, // 是否统一处理接口失败(提示) ifHandleError: true, ...config, headers: { ...this.options.getHeader(), ...config?.headers, }, }; const res = await this.handleRequest(options); if (!res) { return null; } if (res.error) { if (res.error.message && options.ifHandleError) { this.options.onerror(res.error); } throw new Error(res.error); } if (config.useOriginData) { return res; } if (config.headers?.feTraceId) { window.dispatchEvent( new CustomEvent<{ flush?: boolean }>(config.headers.feTraceId, { detail: { flush: config?.flushApiHook, }, }), ); } // 默认返回res.data return transformResponseValue(res.data) } } export type MyRequest = <T = unknown, U extends MyRequestConfigs = {}>( url: string, config: U, ) => Promise<RequestResult<T, U> | null>; export default LmAxios;
utils.ts(这里主要用来处理utc时间,你可能用不到删除相关代码就好)
import moment from 'moment'; const timeReg = /^\d{4}([/:-])(1[0-2]|0?[1-9])\1(0?[1-9]|[1-2]\d|30|31)($|( |T)(?:[01]\d|2[0-3])(:[0-5]\d)?(:[0-5]\d)?(\..*\d)?Z?$)/; export function formatTimeValue(time: string, format = 'YYYY-MM-DD HH:mm:ss') { if (typeof time === 'string' || typeof time === 'number') { if (timeReg.test(time)) { return moment(time).format(format); } } return time; } // 统一转化如参 export const updateObjTimeToUtc = (obj: any) => { if (typeof obj === 'string') { if (timeReg.test(obj)) { return moment(obj).utc().format(); } return obj; } if (toString.call(obj) === '[object Object]') { const newObj: Record<string, any> = {}; Object.keys(obj).forEach((key) => { newObj[key] = updateObjTimeToUtc(obj[key]); }); return newObj; } if (toString.call(obj) === '[object Array]') { obj = obj.map((item: any) => updateObjTimeToUtc(item)); } return obj; }; const utcReg = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*Z$/; const transformResponseValue = (res: any) => { if (!res) { return res; } if (typeof res === 'string') { if (utcReg.test(res)) { return moment(res).format('YYYY-MM-DD HH:mm:ss'); } return res; } if (toString.call(res) === '[object Object]') { const result: any = {}; Object.keys(res).forEach((key) => { result[key] = transformResponseValue(res[key]); }); return result; } if (toString.call(res) === '[object Array]') { return res.map((item: any) => transformResponseValue(item)); } return res; }; export default transformResponseValue;