chore: initial commit
This commit is contained in:
207
frontend/packages/lib-shared/api/http/Axios.ts
Normal file
207
frontend/packages/lib-shared/api/http/Axios.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import axios from 'axios';
|
||||
|
||||
import { ContentTypeEnum } from '../../enums/httpEnum';
|
||||
import { isFunction } from '../../method/is';
|
||||
import type { RequestOptions, Result, UploadFileParams } from '../../types/axios';
|
||||
import { AxiosCanceler } from './axiosCancel';
|
||||
import type { CreateAxiosOptions } from './axiosTransform';
|
||||
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
export * from './axiosTransform';
|
||||
|
||||
/**
|
||||
* @description: 封装axios请求,返回重新封装的数据格式
|
||||
*/
|
||||
export class CordysAxios {
|
||||
public axiosInstance: AxiosInstance;
|
||||
|
||||
private readonly options: CreateAxiosOptions;
|
||||
|
||||
constructor(options: CreateAxiosOptions) {
|
||||
this.options = options;
|
||||
this.axiosInstance = axios.create(options);
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
private getTransform() {
|
||||
const { transform } = this.options;
|
||||
return transform;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 拦截器配置
|
||||
*/
|
||||
private setupInterceptors() {
|
||||
const transform = this.getTransform();
|
||||
if (!transform) {
|
||||
return;
|
||||
}
|
||||
const { requestInterceptors, responseInterceptors, responseInterceptorsCatch } = transform;
|
||||
|
||||
const axiosCanceler = new AxiosCanceler();
|
||||
|
||||
// TODO: 拦截配置升级了 请求拦截器
|
||||
this.axiosInstance.interceptors.request.use((config: CreateAxiosOptions) => {
|
||||
// 如果ignoreCancelToken为true,则不添加到pending中
|
||||
const ignoreCancelToken = config.requestOptions?.ignoreCancelToken;
|
||||
const ignoreCancel =
|
||||
ignoreCancelToken !== undefined ? ignoreCancelToken : this.options.requestOptions?.ignoreCancelToken;
|
||||
|
||||
if (!ignoreCancel) {
|
||||
axiosCanceler.addPending(config);
|
||||
}
|
||||
if (requestInterceptors && isFunction(requestInterceptors)) {
|
||||
config = requestInterceptors(config, this.options);
|
||||
}
|
||||
// TODO: 拦截配置升级了,暂时 as 处理
|
||||
return config as InternalAxiosRequestConfig;
|
||||
}, undefined);
|
||||
|
||||
// 响应拦截器
|
||||
this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => {
|
||||
if (res) {
|
||||
axiosCanceler.removePending(res.config);
|
||||
}
|
||||
if (responseInterceptors && isFunction(responseInterceptors)) {
|
||||
res = responseInterceptors(res);
|
||||
}
|
||||
return res;
|
||||
}, undefined);
|
||||
|
||||
// 响应错误处理
|
||||
if (responseInterceptorsCatch && isFunction(responseInterceptorsCatch)) {
|
||||
this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 文件上传
|
||||
*/
|
||||
uploadFile<T = any>(
|
||||
config: AxiosRequestConfig & RequestOptions,
|
||||
params: UploadFileParams,
|
||||
customFileKey = '',
|
||||
isMultiple = false
|
||||
): Promise<T> {
|
||||
const formData = new window.FormData();
|
||||
const fileName = isMultiple ? 'files' : 'file';
|
||||
if (customFileKey !== '') {
|
||||
params.fileList.forEach((file: File) => {
|
||||
formData.append(customFileKey, file);
|
||||
});
|
||||
} else if (!isMultiple && !customFileKey) {
|
||||
params.fileList.forEach((file: File) => {
|
||||
formData.append(fileName, file);
|
||||
});
|
||||
} else {
|
||||
params.fileList.forEach((item: any) => {
|
||||
formData.append(fileName, item.file, item.file.name);
|
||||
});
|
||||
}
|
||||
if (params.request) {
|
||||
const requestData = JSON.stringify(params.request);
|
||||
formData.append('request', new Blob([requestData], { type: ContentTypeEnum.JSON }));
|
||||
}
|
||||
const transform = this.getTransform();
|
||||
|
||||
const { requestOptions } = this.options;
|
||||
|
||||
const opt = { ...requestOptions, isTransformResponse: false };
|
||||
const { transformRequestHook } = transform || {};
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axiosInstance
|
||||
.request<any, AxiosResponse<Result>>({
|
||||
...config,
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-type': ContentTypeEnum.FORM_DATA,
|
||||
},
|
||||
// @ts-ignore
|
||||
requestOptions: {
|
||||
ignoreCancelToken: true, // 文件上传请求不需要添加到pending中,以免路由切换导致文件上传请求被取消
|
||||
},
|
||||
})
|
||||
.then((res: AxiosResponse<Result>) => {
|
||||
// 请求成功后的处理
|
||||
if (transformRequestHook && isFunction(transformRequestHook)) {
|
||||
try {
|
||||
const ret = transformRequestHook(res, opt);
|
||||
resolve(ret);
|
||||
} catch (err) {
|
||||
reject(err || new Error('request error!'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
resolve(res as unknown as Promise<T>);
|
||||
})
|
||||
.catch((e: Error | AxiosError) => {
|
||||
if (axios.isAxiosError(e)) {
|
||||
// 在这可重写axios错误消息
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||
return this.request({ ...config, method: 'GET' }, options);
|
||||
}
|
||||
|
||||
post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||
return this.request({ ...config, method: 'POST' }, options);
|
||||
}
|
||||
|
||||
put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||
return this.request({ ...config, method: 'PUT' }, options);
|
||||
}
|
||||
|
||||
delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||
return this.request({ ...config, method: 'DELETE' }, options);
|
||||
}
|
||||
|
||||
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||
let conf: CreateAxiosOptions = cloneDeep(config);
|
||||
const transform = this.getTransform();
|
||||
|
||||
const { requestOptions } = this.options;
|
||||
|
||||
const opt = { ...requestOptions, ...options };
|
||||
|
||||
const { beforeRequestHook, transformRequestHook } = transform || {};
|
||||
// 请求之前处理config
|
||||
if (beforeRequestHook && isFunction(beforeRequestHook)) {
|
||||
conf = beforeRequestHook(conf, opt);
|
||||
}
|
||||
conf.requestOptions = opt;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.axiosInstance
|
||||
.request<any, AxiosResponse<Result>>(conf)
|
||||
.then((res: AxiosResponse<Result>) => {
|
||||
// 请求成功后的处理
|
||||
if (transformRequestHook && isFunction(transformRequestHook)) {
|
||||
try {
|
||||
const ret = transformRequestHook(res, opt);
|
||||
resolve(ret);
|
||||
} catch (err) {
|
||||
reject(err || new Error('request error!'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
resolve(res as unknown as Promise<T>);
|
||||
})
|
||||
.catch((e: Error | AxiosError) => {
|
||||
if (axios.isAxiosError(e)) {
|
||||
// 在这可重写axios错误消息
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
63
frontend/packages/lib-shared/api/http/axiosCancel.ts
Normal file
63
frontend/packages/lib-shared/api/http/axiosCancel.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { isFunction } from '../../method/is';
|
||||
import type { AxiosRequestConfig, Canceler } from 'axios';
|
||||
|
||||
let pendingMap = new Map<string, Canceler>();
|
||||
|
||||
export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&');
|
||||
|
||||
export class AxiosCanceler {
|
||||
/**
|
||||
* 添加请求
|
||||
* @param {Object} config
|
||||
*/
|
||||
addPending(config: AxiosRequestConfig) {
|
||||
this.removePending(config);
|
||||
const url = getPendingUrl(config);
|
||||
config.cancelToken =
|
||||
config.cancelToken ||
|
||||
new axios.CancelToken((cancel) => {
|
||||
if (!pendingMap.has(url)) {
|
||||
// 非重复请求,存入pending中
|
||||
pendingMap.set(url, cancel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 清理全部pending中的请求
|
||||
*/
|
||||
removeAllPending() {
|
||||
pendingMap.forEach((cancel) => {
|
||||
if (cancel && isFunction(cancel)) {
|
||||
cancel();
|
||||
}
|
||||
});
|
||||
pendingMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消并移除指定请求
|
||||
* @param {Object} config
|
||||
*/
|
||||
removePending(config: AxiosRequestConfig) {
|
||||
const url = getPendingUrl(config);
|
||||
|
||||
if (pendingMap.has(url)) {
|
||||
// 根据标识找到pending中对应的请求并取消
|
||||
const cancel = pendingMap.get(url);
|
||||
if (cancel && isFunction(cancel)) {
|
||||
cancel(url);
|
||||
}
|
||||
pendingMap.delete(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 重置pending列表
|
||||
*/
|
||||
static reset(): void {
|
||||
pendingMap = new Map<string, Canceler>();
|
||||
}
|
||||
}
|
||||
48
frontend/packages/lib-shared/api/http/axiosTransform.ts
Normal file
48
frontend/packages/lib-shared/api/http/axiosTransform.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Data processing class, can be configured according to the project
|
||||
*/
|
||||
import type { RequestOptions, Result } from '../../types/axios';
|
||||
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
export abstract class AxiosTransform {
|
||||
/**
|
||||
* @description: 请求之前处理配置
|
||||
*/
|
||||
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
|
||||
|
||||
/**
|
||||
* @description: 处理请求数据。如果数据不是预期格式,可直接抛出错
|
||||
*/
|
||||
transformRequestHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
|
||||
|
||||
/**
|
||||
* @description: 请求之前的拦截器
|
||||
*/
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
requestInterceptors?: (config: AxiosRequestConfig, options: CreateAxiosOptions) => AxiosRequestConfig;
|
||||
|
||||
/**
|
||||
* @description: 请求之后的拦截器
|
||||
*/
|
||||
responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;
|
||||
|
||||
/**
|
||||
* @description: 请求之后的拦截器错误处理
|
||||
*/
|
||||
responseInterceptorsCatch?: (error: Error) => void;
|
||||
}
|
||||
|
||||
export interface CreateAxiosOptions extends AxiosRequestConfig {
|
||||
authenticationScheme?: string;
|
||||
transform?: AxiosTransform;
|
||||
requestOptions?: RequestOptions;
|
||||
useAppStore?: any;
|
||||
showErrorMsg?: (options: any) => void;
|
||||
checkStatus?: (
|
||||
status: number,
|
||||
msg: string,
|
||||
msgDetail: string | Record<string, any>,
|
||||
code?: number,
|
||||
noErrorTip?: boolean
|
||||
) => void;
|
||||
}
|
||||
12
frontend/packages/lib-shared/api/http/helper.ts
Normal file
12
frontend/packages/lib-shared/api/http/helper.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export function joinTimestamp<T extends boolean>(join: boolean, restful: T): T extends true ? string : object;
|
||||
|
||||
export function joinTimestamp(join: boolean, restful = false): string | object {
|
||||
if (!join) {
|
||||
return restful ? '' : {};
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
if (restful) {
|
||||
return `?_t=${now}`;
|
||||
}
|
||||
return { _t: now };
|
||||
}
|
||||
191
frontend/packages/lib-shared/api/http/index.ts
Normal file
191
frontend/packages/lib-shared/api/http/index.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { getLocalStorage } from '../../method/local-storage';
|
||||
import { CordysAxios } from './Axios';
|
||||
import type { AxiosTransform, CreateAxiosOptions } from './axiosTransform';
|
||||
import { joinTimestamp } from './helper';
|
||||
import { ContentTypeEnum, RequestEnum } from '@lib/shared/enums/httpEnum';
|
||||
import { useI18n } from '@lib/shared/hooks/useI18n';
|
||||
import { deepMerge, setObjToUrlParams } from '@lib/shared/method';
|
||||
import { getToken } from '@lib/shared/method/auth';
|
||||
import { isString } from '@lib/shared/method/is';
|
||||
import type CommonResponse from '@lib/shared/models/common';
|
||||
import type { RequestOptions, Result } from '@lib/shared/types/axios';
|
||||
import type { Recordable } from '@lib/shared/types/global';
|
||||
import type { AxiosResponse } from 'axios';
|
||||
|
||||
export default function createAxios(opt: Partial<CreateAxiosOptions>) {
|
||||
/**
|
||||
* @description: 数据处理,方便区分多种处理方式
|
||||
*/
|
||||
const transform: AxiosTransform = {
|
||||
/**
|
||||
* @description 请求之前处理config
|
||||
*/
|
||||
beforeRequestHook: (config, options) => {
|
||||
const { joinParamsToUrl, joinTime = true } = options;
|
||||
|
||||
const params = config.params || {};
|
||||
const data = config.data || false;
|
||||
if (config.method?.toUpperCase() === RequestEnum.GET) {
|
||||
if (!isString(params)) {
|
||||
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
|
||||
config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
|
||||
} else {
|
||||
// 兼容restful风格
|
||||
config.url = `${config.url}/${params}${joinTimestamp(joinTime, true)}`;
|
||||
config.params = undefined;
|
||||
}
|
||||
} else if (isString(params)) {
|
||||
// 兼容restful风格
|
||||
config.url += params;
|
||||
config.params = undefined;
|
||||
} else {
|
||||
if (
|
||||
Reflect.has(config, 'data') &&
|
||||
config.data &&
|
||||
(Object.keys(config.data).length > 0 || Array.isArray(config.data))
|
||||
) {
|
||||
config.data = data;
|
||||
config.params = params;
|
||||
} else {
|
||||
// 非GET请求如果没有提供data,则将params视为data
|
||||
config.data = { ...params };
|
||||
config.params = undefined;
|
||||
}
|
||||
if (joinParamsToUrl) {
|
||||
config.url = setObjToUrlParams(config.url as string, { ...config.params, ...config.data });
|
||||
}
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
||||
/**
|
||||
* @description: 处理请求数据。如果数据不是预期格式,可直接抛出错误
|
||||
*/
|
||||
transformRequestHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
|
||||
const { t } = useI18n();
|
||||
const { isTransformResponse, isReturnNativeResponse } = options;
|
||||
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
||||
if (isReturnNativeResponse) {
|
||||
return res;
|
||||
}
|
||||
// 不进行任何处理,直接返回
|
||||
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
|
||||
if (!isTransformResponse) {
|
||||
return res.data;
|
||||
}
|
||||
// 错误的时候返回
|
||||
|
||||
const { data } = res;
|
||||
if (!data) {
|
||||
throw new Error(t('api.apiRequestFailed'));
|
||||
}
|
||||
// 这里 code,result,message为 后台统一的字段
|
||||
const { data: dataResult } = data;
|
||||
|
||||
// 这里直接返回正常结果,因为拦截器已经拦截了非 200 的请求
|
||||
return dataResult;
|
||||
},
|
||||
|
||||
/**
|
||||
* @description: 请求拦截器处理
|
||||
*/
|
||||
requestInterceptors: (config) => {
|
||||
// 请求之前处理config
|
||||
const currentLocale = localStorage.getItem('CRM-locale') || 'zh-CN';
|
||||
const app = getLocalStorage<Record<string, any>>('app', true);
|
||||
|
||||
const token = getToken();
|
||||
if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
|
||||
const { sessionId, csrfToken } = token;
|
||||
|
||||
(config as Recordable).headers = {
|
||||
...config.headers,
|
||||
'X-AUTH-TOKEN': sessionId,
|
||||
'CSRF-TOKEN': csrfToken,
|
||||
'Accept-Language': currentLocale,
|
||||
'Organization-Id': app?.orgId,
|
||||
};
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
||||
/**
|
||||
* @description: 响应拦截器处理
|
||||
*/
|
||||
responseInterceptors: (res: AxiosResponse<CommonResponse<any>>) => {
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* @description: 响应错误处理
|
||||
*/
|
||||
responseInterceptorsCatch: (error: any) => {
|
||||
const { t } = useI18n();
|
||||
const { response, code, message, config } = error || {};
|
||||
const msg: string = response?.data?.message ?? '';
|
||||
const msgDetail: string = response?.data?.messageDetail ?? '';
|
||||
const err: string = error?.toString?.() ?? '';
|
||||
let errMessage = '';
|
||||
|
||||
try {
|
||||
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
|
||||
errMessage = t('api.apiTimeoutMessage');
|
||||
}
|
||||
if (err?.includes('Network Error')) {
|
||||
errMessage = t('api.networkExceptionMsg');
|
||||
}
|
||||
|
||||
if (errMessage) {
|
||||
opt.showErrorMsg?.({ message: errMessage, duration: 5000 });
|
||||
return Promise.reject(error);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e as unknown as string);
|
||||
}
|
||||
opt.checkStatus?.(response?.status, msg, msgDetail, response?.data?.code, config?.requestOptions?.noErrorTip);
|
||||
return Promise.reject(
|
||||
response?.config?.requestOptions?.isReturnNativeResponse ? response?.data : response?.data?.message || error
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return new CordysAxios(
|
||||
deepMerge(
|
||||
{
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
|
||||
// authentication schemes,e.g: Bearer
|
||||
// authenticationScheme: 'Bearer',
|
||||
authenticationScheme: '',
|
||||
baseURL: `${window.location.origin}/${import.meta.env.VITE_API_BASE_URL as string}`,
|
||||
timeout: 300 * 1000,
|
||||
headers: { 'Content-Type': ContentTypeEnum.JSON },
|
||||
// 如果是form-data格式
|
||||
// headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
|
||||
// 数据处理方式
|
||||
transform,
|
||||
// 配置项,下面的选项都可以在独立的接口请求中覆盖
|
||||
requestOptions: {
|
||||
// 默认将prefix 添加到url
|
||||
joinPrefix: true,
|
||||
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
||||
isReturnNativeResponse: false,
|
||||
// 需要对返回数据进行处理
|
||||
isTransformResponse: true,
|
||||
// post请求的时候添加参数到url
|
||||
joinParamsToUrl: false,
|
||||
// 格式化提交参数时间
|
||||
formatDate: true,
|
||||
// 消息提示类型
|
||||
errorMessageMode: 'message',
|
||||
// 是否加入时间戳
|
||||
joinTime: true,
|
||||
// 忽略取消请求的token
|
||||
ignoreCancelToken: false,
|
||||
// 是否携带token
|
||||
withToken: true,
|
||||
},
|
||||
},
|
||||
opt || {}
|
||||
)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user