360 lines
12 KiB
TypeScript
360 lines
12 KiB
TypeScript
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 替换成具名正则匹配 /(?<serviceId>[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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|