import type { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from "axios"; import { getAdapter } from "axios"; import { storageService } from "./apiStorage"; type IRequestStatus = 'timeout' | 'error' | 'success'; // 请求状态表 interface IRequestStatusRecord { id: string; // 请求匹配到的缓存策略 url status: IRequestStatus; // 请求状态 updatedAt: number; // 请求状态更新时间 } const DEBUG_FLAG = 'common-degrade-interceptor-debug'; const REQUEST_STATUS_TABLE_KEY = 'common-request-status-table'; const PARAM_REG = '[\%0-9a-zA-Z-_.]+'; function debug(...args: any) { const debugMode = localStorage.getItem(DEBUG_FLAG) === 'true'; if (debugMode) { console.log('[gitcode]', ...args); } } export class DegradeInterceptor { private storageService; constructor() { this.storageService = storageService; } onRequestFulfilled(config: AxiosRequestConfig) { // 是否走缓存策略? if (this.disableDegrade() || !this.isReqMatched(config)) { return config; } const strategy = this.getReqCacheStrategy(config); if (strategy?.timeout) { config.timeout = strategy.timeout; } debug('当前请求', config.method, config.url); debug('缓存策略', strategy); if (this.isNeedReadCache(config)) { debug('降级状态下读缓存'); config.adapter = this.readCacheAdapter.bind(this); return config; } config.adapter = this.retryAdapterEnhancer(config).bind(this); return config; } disableDegrade() { const disableDegradeFeat = localStorage.getItem('disableDegradeFeat') && localStorage.getItem('disableDegradeFeat')?.toLowerCase() === 'true'; debug('降级开关:', disableDegradeFeat); return disableDegradeFeat; } onResponseFulfilled(response: AxiosResponse) { if (!this.disableDegrade() && !response?.request?.isCache && this.isReqMatched(response.config)) { this.handleResp(response); } return response; } onResponseRejected(error: any) { if (!this.disableDegrade() && error.config && this.isReqMatched(error.config)) { // 状态码屏蔽 const strategy = this.getReqCacheStrategy(error.config); if (strategy?.excludeStatusCode?.includes(error?.request?.status)) { debug(`当前错误码为${error?.request?.status}不读缓存`); return Promise.reject(error); } const isTimeout = error?.request?.status === 504 || error.code === 'ECONNABORTED'; const errorStatus = isTimeout ? 'timeout' : 'error'; this.setRequestCacheStatus(error.config, errorStatus); debug('错误状态下读缓存'); return this.getRequestCache(error.config); } return Promise.reject(error); } handleResp(response: AxiosResponse) { // 请求没有报错,更新缓存并记录缓存的时间戳 this.setRequestCache(response.config, response); this.setRequestCacheStatus(response.config, 'success'); // 判断缓存的条数 this.checkRequestCacheRows(response.config); } checkRequestCacheRows(request: any) { const statusList = this.getAllRequestStatus(); const strategy = this.getReqCacheStrategy(request); const maxRows = strategy.maxRows || 10; // 先清理一波过期缓存? const reqCacheRecord = Object.keys(statusList) .reduce((p, c) => { p.push({ storageKey: c, ...statusList[c] } as never); return p; }, []) .filter((item: IRequestStatusRecord) => item.id === strategy.url && item.status === 'success' && item.updatedAt) .sort((a: any, b: any) => b.updatedAt - a.updatedAt); debug('reqCacheRecord', reqCacheRecord); if (reqCacheRecord.length < maxRows) { return; } reqCacheRecord.filter((_, index) => index + 1 > maxRows) .forEach((item: any) => { debug('删除超限缓存', item.storageKey); this.storageService.removeItem(item.storageKey); delete statusList[item.storageKey]; }) this.saveAllRequestStatus(statusList); } isReqMatched(config: AxiosRequestConfig): boolean { const strategy = this.getReqCacheStrategy(config); const method = strategy?.method || 'get'; const isMatchMethod = config.method?.toLocaleLowerCase() === method.toLocaleLowerCase(); return strategy && isMatchMethod; } wrapHttpResponse(config: AxiosRequestConfig, responseCache: any) { return { ...responseCache, config, request: { ...responseCache?.request, isCache: true, }, }; } getStorageKey(config: AxiosRequestConfig) { const method = config.method?.toLocaleUpperCase(); const strategy = this.getReqCacheStrategy(config); // 处理 url 尾部参数 const requestParams = this.getRequsetParams(config); const paramsStringArr: any = []; Object.keys(requestParams).forEach((key) => { if (!strategy?.ignoreUrlParams?.includes(key) && key !== '_') { const value = requestParams[key]; paramsStringArr.push(`${key}=${value}`); } }); // 处理 url 内参数 let shortUrl = config.url?.split('?')[0] || ''; const matches: any = this.getReqUrlMatchResult(strategy.url, shortUrl); Object.keys(matches?.groups ?? {}).forEach((key) => { if (strategy?.ignoreUrlParams?.includes(key)) { const value = matches.groups[key]; // 将 /abc123/xxx 替换成 /{serviceId}/xxx shortUrl = shortUrl.replace(value, `{${key}}`); } }); // 处理 headers 参数 const headerString = strategy?.withHeaders?.map((header: any) => config.headers?.[header]).join('&'); const headerStr = headerString ? `${headerString}~` : ''; const paramsString = paramsStringArr.length ? '?' + paramsStringArr.join('&') : ''; return method + '~' + headerStr + shortUrl + paramsString; } onRequestRejected = (error: AxiosError) => { return Promise.reject(error) } getRequsetParams(config: AxiosRequestConfig) { if (config.method === 'get') { try { const idx = config.url?.indexOf('?') || -1; if (idx === -1) { return config.params || {}; } const params = config.url?.slice(idx + 1); const requestParams = params?.split('&').reduce((obj: any, curr) => { const [key = '', value = ''] = curr?.split('=') || []; obj[key] = value; return obj; }, {}); Object.assign(requestParams, config.params); return requestParams ?? {}; } catch (error) { return {}; } } else { return typeof config.data === 'string' ? JSON.parse(config.data) : config.data; } } isNeedReadCache(config: AxiosRequestConfig) { // 降级状态或已经还未过熔断时间,默认为 30s const reqStatus = this.getRequestStatus(config); const strategy: any = this.getReqCacheStrategy(config); const degradedTime = strategy?.degradedTime || 30 * 1000; const isError = ['timeout', 'error'].includes(reqStatus?.status); const isInDegradedTime = Date.now() - reqStatus?.updatedAt < degradedTime; debug('isError', isError, 'isInDegradedTime', isInDegradedTime); return isError && isInDegradedTime; } isCacheTimeout(config: AxiosRequestConfig) { const strategy: any = this.getReqCacheStrategy(config); const reqStatus = this.getRequestStatus(config); if (!strategy.maxAge) { return false; } if (Date.now() - reqStatus.updatedAt > strategy.maxAge) { this.removeRequestCache(config); this.deleteRequestStatus(config); return true; } return false; } readCacheAdapter(config: AxiosRequestConfig) { return this.getRequestCache(config).then((responseCache: any) => { debug('命中缓存'); return this.wrapHttpResponse(config, responseCache); }); } retryAdapterEnhancer(config: AxiosRequestConfig) { const defaultAdapter: any = getAdapter(config.adapter); return (config: AxiosRequestConfig) => { const strategy: any = this.getReqCacheStrategy(config); const retryCount = strategy.retry ?? 0; let __retryCount = 0; const request: any = async () => { try { return await defaultAdapter(config); } catch (error: any) { if (strategy?.excludeStatusCode?.includes(error?.status)) { debug(`当前错误码为${error?.status}不需要重试`); return Promise.reject(error); } if (!retryCount || __retryCount >= retryCount) { return Promise.reject(error); } __retryCount++; return request(); } }; return request(); }; } // 请求状态表 start getAllRequestStatus(): { [key: string]: IRequestStatusRecord } { return JSON.parse(sessionStorage.getItem(REQUEST_STATUS_TABLE_KEY) || '{}'); } saveAllRequestStatus(statusList: any) { sessionStorage.setItem(REQUEST_STATUS_TABLE_KEY, JSON.stringify(statusList)); } getRequestStatus(config: AxiosRequestConfig): IRequestStatusRecord { const statusList = this.getAllRequestStatus(); const key = this.getStorageKey(config); return statusList[key]; } setRequestCacheStatus(config: AxiosRequestConfig, status: IRequestStatus) { const key = this.getStorageKey(config); const strategy: any = this.getReqCacheStrategy(config); const statusList = this.getAllRequestStatus(); statusList[key] = { id: strategy.url, status, updatedAt: Date.now(), }; debug(key, '状态', status, '时间:', new Date().toLocaleTimeString('zh-cn')); this.saveAllRequestStatus(statusList); } deleteRequestStatus(config: AxiosRequestConfig) { const key = this.getStorageKey(config); const statusList = this.getAllRequestStatus(); delete statusList[key]; this.saveAllRequestStatus(statusList); } // 请求状态表 end // 请求缓存表 start setRequestCache(config: AxiosRequestConfig, response: AxiosResponse) { const key = this.getStorageKey(config); // function cannot be cloned in localForage this.storageService.setItem(key, JSON.parse(JSON.stringify(response))); } getRequestCache(config: AxiosRequestConfig) { const key = this.getStorageKey(config); return this.storageService.getItem(key); } removeRequestCache(config: AxiosRequestConfig) { const key = this.getStorageKey(config); this.storageService.removeItem(key); } // 请求缓存表 end // 请求缓存策略表 start getReqUrlMatchResult(regexpUrl: string, requestUrl: string) { // 将 /{serviceId}/xxx 替换成具名正则匹配 /(?[0-9a-z-]+)/xxx let regExp = regexpUrl; if (typeof regexpUrl === 'string') { regExp = regexpUrl.replace(/{/g, '(?<').replace(/}/g, `>${PARAM_REG})`) + '$'; } const noParamUrl = requestUrl.split('?')[0]; const matches = new RegExp(regExp).exec(noParamUrl); return matches; } getReqCacheStrategy(config: AxiosRequestConfig): any { const requestCacheList = this.storageService.getRequestCacheList(); return requestCacheList .filter((item: any) => item.url && (typeof item.url === 'string' ? item.url?.length > 2 : true)) .find((item: any) => { const matches = this.getReqUrlMatchResult(item.url, config.url || ''); return !!matches; }); } // 请求缓存策略表 end } const degradeInterceptor = new DegradeInterceptor() degradeInterceptor.onRequestFulfilled = degradeInterceptor.onRequestFulfilled.bind(degradeInterceptor) degradeInterceptor.onRequestRejected = degradeInterceptor.onRequestRejected.bind(degradeInterceptor) degradeInterceptor.onResponseFulfilled = degradeInterceptor.onResponseFulfilled.bind(degradeInterceptor) degradeInterceptor.onResponseRejected = degradeInterceptor.onResponseRejected.bind(degradeInterceptor) export default degradeInterceptor