跳到主要内容

我的架构设计方案

提示

封装 axios 的功能:

  • 取消重复请求: 完全相同的接口在上一个pending状态时,自动取消下一个请求
  • 分页不同页请求: 当新页请求数据发出,自动中断上一次的请求
  • 请求失败自动重试: 接口请求后台异常时候,自动重新发起多次请求,直到达到所设次数
  • 请求接口数据缓存:接口在设定时间内不会向后台获取数据,而是直接拿本地缓存
  • 支持JSONP 接口:

TypeScript 类型订制


import { AxiosRequestConfig, AxiosResponse, CancelToken, AxiosError } from 'axios';

export type AxiosConfigType = {
baseURL: string;
timeout: number;
};
export interface AxiosRequestConfigType extends AxiosRequestConfig {
cancelToken?: CancelToken;
cache?: boolean;
retry?: number;
retryDelay?: number;
setExpireTime?: number;
cancelRequest?: boolean;
retryCount?: number;
}
export type AxiosResponseDataType<T = any> = {
code: 0 | 200 | 401 | 'default';
msg: string;
data: T;
};
export interface AxiosResponseType extends Omit<AxiosResponse, 'config'> {
config: AxiosRequestConfigType;
data: AxiosResponseDataType;
}
export interface AxiosErrorType extends AxiosError {
config: AxiosRequestConfigType;
}
export type APIConfigType = {
url: string;
method: string;
config: {
cancelToken?: CancelToken;
cache?: boolean;
retry?: number;
retryDelay?: number;
setExpireTime?: number;
cancelRequest?: boolean;
retryCount?: number;
};
};
export type APIConfigListType = APIConfigType[];

取消重复请求逻辑实现


/*  假如用户重复点击按钮,先后提交了 A 和 B 这两个完全相同(考虑请求路径、方法、参数)的请求,我们可以从以下几种拦截方案中选择其一:
1. 取消 A 请求,只发出 B 请求(会导致A请求已经发出去,被后端处理了)
2. 取消 B 请求,只发出 A 请求
3. 取消 B 请求,只发出 A 请求,把收到的 A 请求的返回结果也作为 B 请求的返回结果
第3种方案需要做监听处理增加了复杂性,结合我们实际的业务需求,最后采用了第2种方案来实现,即:
只发第一个请求。在 A 请求还处于 pending 状态时,后发的所有与 A 重复的请求都取消,实际只发出 A 请求,直到 A 请求结束(成功/失败)才停止对这个请求的拦截。
*/
import Axios from 'axios';
import { AxiosRequestConfigType, AxiosResponseType } from './type';
import { generateReqKey } from './common';

const pendingRequest = new Map();

export function addPendingRequest(config: AxiosRequestConfigType): void {
const configCopy = config;
if (configCopy.cancelRequest) {
const requestKey = generateReqKey(configCopy);
if (pendingRequest.has(requestKey)) {
configCopy.cancelToken = new Axios.CancelToken((cancel) => {
cancel(`${configCopy.url} 请求已取消`);
});
} else {
configCopy.cancelToken =
configCopy.cancelToken ||
new Axios.CancelToken((cancel) => {
pendingRequest.set(requestKey, cancel);
});
}
}
}

export function removePendingRequest(response: AxiosResponseType): void {
if (response && response.config && response.config.cancelRequest) {
const requestKey = generateReqKey(response.config);
if (pendingRequest.has(requestKey)) {
const cancelToken = pendingRequest.get(requestKey);
cancelToken(requestKey);
pendingRequest.delete(requestKey);
}
}
}

请求失败自动重试逻辑实现


import { AxiosInstance, AxiosPromise } from 'axios';
import { AxiosErrorType } from './type';
import { isJsonStr } from './common';

export default function againRequest(err: AxiosErrorType, axios: AxiosInstance): AxiosPromise {
const { config } = err;
if (!config || !config.retry) return Promise.reject(err);
config.retryCount = config.retryCount || 0;
if (config.retryCount >= config.retry) {
return Promise.reject(err);
}
config.retryCount += 1;
const backoff = new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, config.retryDelay || 1000);
});
return backoff.then(() => {
if (config.data && isJsonStr(config.data)) {
config.data = JSON.parse(config.data);
}
return axios(config);
});
}

请求接口数据缓存逻辑实现


import Axios, { AxiosInstance } from 'axios';
import { AxiosResponseType, AxiosRequestConfigType } from './type';
import { generateReqKey } from './common';

const options = {
storage: true, // 是否开启loclastorage缓存
storageKey: 'apiCache',
storage_expire: 600000, // localStorage 数据存储时间10min(刷新页面判断是否清除)
expire: 20000, // 每个接口数据缓存ms 数
};

function getNowTime() {
return new Date().getTime();
}

(() => {
const cache = window.localStorage.getItem(options.storageKey);
if (cache) {
const { storageExpire } = JSON.parse(cache);
if (storageExpire && getNowTime() - storageExpire < options.storage_expire) {
return;
}
}
window.localStorage.setItem(options.storageKey, JSON.stringify({ data: {}, storageExpire: getNowTime() }));
})();

function getCacheItem(key) {
const cache = window.localStorage.getItem(options.storageKey);
const { data, storageExpire } = JSON.parse(cache || '{}');
return (data && data[key]) || null;
}
function setCacheItem(key, value) {
const cache = window.localStorage.getItem(options.storageKey);
const { data, storageExpire } = JSON.parse(cache || '{}');
data[key] = value;
window.localStorage.setItem(options.storageKey, JSON.stringify({ data, storageExpire }));
}

const CACHE = {};
const cacheHandler = {
get(target, key) {
let value = target[key];
console.log(`${key} 被读取`, value);
if (options.storage && !value) {
value = getCacheItem(key);
}
return value;
},
set(target, key, value) {
console.log(`${key} 被设置为 ${value}`);
target[key] = value;
if (options.storage) {
setCacheItem(key, value);
}

return true;
},
};

const CACHEProxy = new Proxy(CACHE, cacheHandler);

export function requestInterceptor(config: AxiosRequestConfigType, axios: AxiosInstance): void {
const configCopy = config;
if (configCopy.cache) {
const data = CACHEProxy[`${generateReqKey(configCopy)}`];
let setExpireTime;
if (configCopy.setExpireTime) {
setExpireTime = config.setExpireTime;
} else {
setExpireTime = options.expire;
}
if (data && getNowTime() - data.expire < setExpireTime) {
configCopy.cancelToken = new Axios.CancelToken((cancel) => {
cancel(data);
});
}
}
}

export function responseInterceptor(response: AxiosResponseType): void {
if (response && response.config.cache && response.data.code === 200) {
const data = {
expire: getNowTime(),
data: response,
};
CACHEProxy[`${generateReqKey(response.config)}`] = data;
}
}

Axios 实例创建


import Axios, { AxiosInstance } from 'axios';
import { AxiosConfigType, AxiosResponseType, AxiosRequestConfigType } from './type';
import statusCode from './statusCode';
import { addPendingRequest, removePendingRequest } from './cancelRepeatRquest';
import againRequest from './requestAgainSend';
import { requestInterceptor, responseInterceptor } from './requestCache';

function createAxios(axiosConfig: AxiosConfigType): AxiosInstance {
const axios = Axios.create({
baseURL: axiosConfig.baseURL,
timeout: axiosConfig.timeout,
});
axios.interceptors.request.use(
(config: AxiosRequestConfigType) => {
const configCopy = config;
if (configCopy.headers) {
configCopy.headers.Authorization = 'token';
}
addPendingRequest(configCopy);
requestInterceptor(configCopy, axios);
return configCopy;
},
(error) => {
return Promise.reject(error);
}
);
axios.interceptors.response.use(
(response: AxiosResponseType) => {
removePendingRequest(response);
responseInterceptor(response);
return statusCode[response.data.code || 'default'](response);
},
(error) => {
removePendingRequest(error.config || {});
if (!Axios.isCancel(error)) {
againRequest(error, axios);
}
if (Axios.isCancel(error) && error.message.data && error.message.data.config.cache) {
return Promise.resolve(error.message.data.data.data);
}
return Promise.reject(error);
}
);
return axios;
}

export default createAxios;

request 请求封装


import Axios from './utils/axios';
import { mapValues } from './utils/common';
import { AxiosConfigType, APIConfigListType, APIConfigType } from './utils/type';

export default function request(basicConfig: AxiosConfigType): any {
const instance = Axios(basicConfig);
return (apiConfigList: APIConfigListType) => {
return mapValues(apiConfigList, (apiConfig: APIConfigType) => {
const { method, url, config = {} } = apiConfig;
if (method === 'get') {
return (params: any) => instance.get(url, { params, ...config });
}
if (method === 'post') {
return (params: any) => instance.post(url, params, config);
}
return '请求方法配置错误';
});
};
}

api 请求管理


import Request from './request';

const request = Request({
baseURL: '/',
timeout: 0,
})({
middleViewData: {
url: '/jscApi/middleViewData',
method: 'get',
},
cancelReq: {
url: 'http://localhost:3003/jscApi/middleViewData',
method: 'get',
config: {
cancelRequest: true,
},
},
reqAgainSend: {
url: '/equ/equTypeList11',
method: 'get',
config: {
retry: 3,
retryDelay: 1000,
},
},
cacheEquList: {
url: '/equ/equList',
method: 'get',
config: {
cache: true,
setExpireTime: 30000,
},
},
});

export default request;

参考资料


在项目中用ts封装axios,一次封装整个团队受益