397 lines
10 KiB
TypeScript
397 lines
10 KiB
TypeScript
import { getToken, removeToken } from '@/plugins/auth-token';
|
|
import {
|
|
sessionGet,
|
|
sessionGetJSON,
|
|
sessionSetJSON,
|
|
} from '@/utils/cache-session-utils';
|
|
import { localGet } from '@/utils/cache-local-utils';
|
|
import { TOKEN_KEY, TOKEN_KEY_PREFIX } from '@/constants/token-constants';
|
|
import {
|
|
CACHE_LOCAL_I18N,
|
|
CACHE_SESSION_FATCH,
|
|
} from '@/constants/cache-keys-constants';
|
|
import {
|
|
APP_REQUEST_HEADER_CODE,
|
|
APP_REQUEST_HEADER_VERSION,
|
|
APP_DATA_API_KEY,
|
|
} from '@/constants/app-constants';
|
|
import {
|
|
RESULT_CODE_ENCRYPT,
|
|
RESULT_CODE_ERROR,
|
|
RESULT_CODE_SUCCESS,
|
|
RESULT_MSG_ENCRYPT,
|
|
RESULT_MSG_ERROR,
|
|
RESULT_MSG_NOT_TYPE,
|
|
RESULT_MSG_SERVER_ERROR,
|
|
RESULT_MSG_SUCCESS,
|
|
RESULT_MSG_TIMEOUT,
|
|
RESULT_MSG_URL_NOTFOUND,
|
|
RESULT_MSG_URL_RESUBMIT,
|
|
} from '@/constants/result-constants';
|
|
import { decryptAES, encryptAES } from '@/utils/encrypt-utils';
|
|
|
|
/**响应结果类型 */
|
|
export type ResultType = {
|
|
/**响应码 */
|
|
code: number;
|
|
/**信息 */
|
|
msg: string;
|
|
/**数据 */
|
|
data?: any;
|
|
/**未知属性 */
|
|
[key: string]: any;
|
|
};
|
|
|
|
/**防止重复提交类型 */
|
|
type RepeatSubmitType = {
|
|
/**请求地址 */
|
|
url: string;
|
|
/**请求数据 */
|
|
data: string;
|
|
/**请求时间 */
|
|
time: number;
|
|
};
|
|
|
|
/**请求参数类型 */
|
|
type OptionsType = {
|
|
/**请求的根域名地址-不带/后缀 */
|
|
baseUrl?: string;
|
|
/**超时时间,毫秒 */
|
|
timeout?: number;
|
|
/**请求地址 */
|
|
url: string;
|
|
/**请求方法 */
|
|
method: 'get' | 'post' | 'put' | 'delete' | 'PATCH';
|
|
/**请求头 */
|
|
headers?: HeadersInit;
|
|
/**地址栏参数 */
|
|
params?: Record<string, string | number | boolean | undefined>;
|
|
/**发送数据 */
|
|
data?: Record<string, any> | FormData | object;
|
|
/**请求数据类型 */
|
|
dataType?: 'form-data' | 'json';
|
|
/**响应数据类型 */
|
|
responseType?: 'text' | 'json' | 'blob' | 'arrayBuffer';
|
|
/**请求缓存策略 */
|
|
cache?: RequestCache;
|
|
/**请求的凭证,如 omit、same-origin、include */
|
|
credentials?: RequestCredentials;
|
|
/**请求体 */
|
|
body?: BodyInit;
|
|
/**防止数据重复提交 */
|
|
repeatSubmit?: boolean;
|
|
/**接口数据加密 */
|
|
crypto?: boolean;
|
|
/**携带授权Token请求头 */
|
|
whithToken?: boolean;
|
|
/**中断控制信号,timeout不会生效 */
|
|
signal?: AbortSignal;
|
|
};
|
|
|
|
// 多语言处理
|
|
export const language = localGet(CACHE_LOCAL_I18N) || 'en_US';
|
|
|
|
// 兼容旧前端可改配置文件
|
|
const baseUrl = import.meta.env.PROD
|
|
? sessionGet('baseUrl') || import.meta.env.VITE_API_BASE_URL
|
|
: import.meta.env.VITE_API_BASE_URL;
|
|
|
|
/**默认请求参数 */
|
|
const FATCH_OPTIONS: OptionsType = {
|
|
baseUrl: baseUrl,
|
|
timeout: 10_000,
|
|
url: '',
|
|
method: 'get',
|
|
headers: {
|
|
[APP_REQUEST_HEADER_CODE]: import.meta.env.VITE_APP_CODE,
|
|
[APP_REQUEST_HEADER_VERSION]: import.meta.env.VITE_APP_VERSION,
|
|
},
|
|
dataType: 'json',
|
|
responseType: 'json',
|
|
cache: 'no-cache',
|
|
credentials: undefined,
|
|
repeatSubmit: true,
|
|
whithToken: true,
|
|
signal: undefined,
|
|
};
|
|
|
|
/**请求前的拦截 */
|
|
function beforeRequest(options: OptionsType): OptionsType | Promise<any> {
|
|
options.headers = Object.assign({}, options.headers);
|
|
//console.log('请求前的拦截', options);
|
|
|
|
// 给发送数据类型设置请求头
|
|
if (options.dataType === 'json') {
|
|
Reflect.set(
|
|
options.headers,
|
|
'content-type',
|
|
'application/json;charset=utf-8'
|
|
);
|
|
}
|
|
|
|
// 客户端接受语言
|
|
Reflect.set(options.headers, 'Accept-Language', `${language};q=0.9`);
|
|
|
|
// 是否需要设置 token
|
|
const token = getToken();
|
|
if (options.whithToken && token) {
|
|
Reflect.set(options.headers, TOKEN_KEY, TOKEN_KEY_PREFIX + token);
|
|
}
|
|
|
|
// 是否需要防止数据重复提交
|
|
if (
|
|
options.repeatSubmit &&
|
|
options.dataType === 'json' &&
|
|
['post', 'put'].includes(options.method)
|
|
) {
|
|
const requestObj: RepeatSubmitType = {
|
|
url: options.url,
|
|
data: JSON.stringify(options.data) || '',
|
|
time: Date.now(),
|
|
};
|
|
const sessionObj: RepeatSubmitType = sessionGetJSON(CACHE_SESSION_FATCH);
|
|
if (sessionObj) {
|
|
const { url, data, time } = sessionObj;
|
|
const interval = 3000; // 间隔时间(ms),小于此时间视为重复提交
|
|
if (
|
|
requestObj.url === url &&
|
|
requestObj.data === data &&
|
|
requestObj.time - time < interval
|
|
) {
|
|
const message = RESULT_MSG_URL_RESUBMIT[language];
|
|
return Promise.resolve({
|
|
code: RESULT_CODE_ERROR,
|
|
msg: message,
|
|
});
|
|
} else {
|
|
sessionSetJSON(CACHE_SESSION_FATCH, requestObj);
|
|
}
|
|
} else {
|
|
sessionSetJSON(CACHE_SESSION_FATCH, requestObj);
|
|
}
|
|
}
|
|
|
|
// 请求拼接地址栏参数
|
|
if (options.params) {
|
|
const params = options.params;
|
|
const queryParams: string[] = [];
|
|
for (const key in params) {
|
|
const value = params[key];
|
|
// 空字符或未定义的值不作为参数发送
|
|
if (value === '' || value === undefined) continue;
|
|
const str = `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
queryParams.push(str);
|
|
}
|
|
const paramStr = queryParams.join('&');
|
|
if (paramStr) {
|
|
const separator = options.url.includes('?') ? '&' : '?';
|
|
// 请求加密
|
|
if (options.crypto) {
|
|
const data = encryptAES(JSON.stringify(paramStr), APP_DATA_API_KEY);
|
|
options.url += `${separator}data=${encodeURIComponent(data)}`;
|
|
} else {
|
|
options.url += `${separator}${paramStr}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.method === 'get') return options;
|
|
|
|
// 非get参数提交
|
|
let body = options.data;
|
|
if (body instanceof FormData) {
|
|
options.body = body;
|
|
} else if (body) {
|
|
// 请求加密
|
|
if (options.crypto) {
|
|
const data = encryptAES(JSON.stringify(body), APP_DATA_API_KEY);
|
|
body = { data };
|
|
}
|
|
options.body = JSON.stringify(body);
|
|
}
|
|
return options;
|
|
}
|
|
|
|
/**请求后的拦截 */
|
|
function interceptorResponse(res: ResultType): ResultType | Promise<any> {
|
|
// console.log('请求后的拦截', res);
|
|
|
|
// 登录失效时,移除授权令牌并重新刷新页面
|
|
if (res.code === 401) {
|
|
removeToken();
|
|
window.location.reload();
|
|
}
|
|
|
|
// 响应数据解密
|
|
if (res.code === RESULT_CODE_ENCRYPT) {
|
|
const str = decryptAES(res.data, APP_DATA_API_KEY);
|
|
let data = {};
|
|
try {
|
|
data = JSON.parse(str);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
if (Object.keys(data).length === 0) {
|
|
return Promise.resolve({
|
|
code: RESULT_CODE_ERROR,
|
|
msg: RESULT_MSG_ENCRYPT[language],
|
|
});
|
|
}
|
|
return Promise.resolve({
|
|
code: RESULT_CODE_SUCCESS,
|
|
msg: RESULT_MSG_SUCCESS[language],
|
|
data,
|
|
});
|
|
}
|
|
|
|
// 风格处理
|
|
if (!Reflect.has(res, 'code')) {
|
|
return Promise.resolve({
|
|
code: RESULT_CODE_SUCCESS,
|
|
msg: RESULT_MSG_SUCCESS[language],
|
|
data: res,
|
|
});
|
|
}
|
|
if (Reflect.has(res, 'error')) {
|
|
return Promise.resolve({
|
|
code: RESULT_CODE_ERROR,
|
|
msg: RESULT_MSG_ERROR[language],
|
|
data: res.error,
|
|
});
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* 请求http
|
|
*
|
|
* @param options 请求参数
|
|
*
|
|
* responseType改变响应结果类型
|
|
* @returns 返回 Promise<ResultType>
|
|
*/
|
|
export async function request(options: OptionsType): Promise<ResultType> {
|
|
options = Object.assign({}, FATCH_OPTIONS, options);
|
|
let timeoutId: any = 0;
|
|
// 请求超时控制请求终止
|
|
if (!options.signal) {
|
|
const controller = new AbortController();
|
|
const { signal } = controller;
|
|
options.signal = signal;
|
|
timeoutId = setTimeout(() => {
|
|
controller.abort(); // 终止请求
|
|
}, options.timeout);
|
|
}
|
|
|
|
// 检查请求拦截
|
|
const beforeReq = beforeRequest(options);
|
|
if (beforeReq instanceof Promise) {
|
|
return await beforeReq;
|
|
}
|
|
options = beforeReq;
|
|
|
|
// 判断用户传递的URL是否http或/开头
|
|
if (!options.url.startsWith('http')) {
|
|
const uri = options.url.startsWith('/') ? options.url : `/${options.url}`;
|
|
options.url = options.baseUrl + uri;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(options.url, options);
|
|
// console.log('请求结果:', res);
|
|
|
|
// 状态码拦截处理
|
|
const reqNot = stateCode(res);
|
|
if (reqNot != false) {
|
|
return reqNot;
|
|
}
|
|
|
|
// 根据响应数据类型返回
|
|
switch (options.responseType) {
|
|
case 'text': // 文本数据
|
|
const str = await res.text();
|
|
return {
|
|
code: RESULT_CODE_SUCCESS,
|
|
msg: str,
|
|
};
|
|
case 'json': // json格式数据
|
|
const result = await res.json();
|
|
// 请求后的拦截
|
|
const beforeRes = interceptorResponse(result);
|
|
if (beforeRes instanceof Promise) {
|
|
return await beforeRes;
|
|
}
|
|
return result;
|
|
case 'blob': // 二进制数据则直接返回
|
|
case 'arrayBuffer':
|
|
const contentType = res.headers.get('content-type') || '';
|
|
if (contentType.startsWith('application/json')) {
|
|
const result = await res.json();
|
|
return result as ResultType;
|
|
}
|
|
const data =
|
|
options.responseType === 'blob'
|
|
? await res.blob()
|
|
: await res.arrayBuffer();
|
|
return {
|
|
code: RESULT_CODE_SUCCESS,
|
|
msg: RESULT_MSG_SUCCESS[language],
|
|
data: data,
|
|
status: res.status,
|
|
headers: res.headers,
|
|
};
|
|
default:
|
|
return {
|
|
code: RESULT_CODE_ERROR,
|
|
msg: RESULT_MSG_NOT_TYPE[language],
|
|
};
|
|
}
|
|
} catch (error: any) {
|
|
// 请求被终止时
|
|
if (error.name === 'AbortError') {
|
|
return {
|
|
code: RESULT_CODE_ERROR,
|
|
msg: RESULT_MSG_TIMEOUT[language],
|
|
};
|
|
}
|
|
throw error;
|
|
} finally {
|
|
clearTimeout(timeoutId); // 请求成功,清除超时计时器
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 判断状态码处理结果信息(不可处理)
|
|
* @param res 请求结果
|
|
* @returns
|
|
*/
|
|
function stateCode(res: Response) {
|
|
// 网络异常
|
|
if (res.status === 500) {
|
|
return {
|
|
code: RESULT_CODE_ERROR,
|
|
msg: RESULT_MSG_SERVER_ERROR[language],
|
|
};
|
|
}
|
|
// 上传文件成功无内容返回
|
|
if (res.status === 204 || res.statusText === 'No Content') {
|
|
return {
|
|
code: RESULT_CODE_SUCCESS,
|
|
msg: RESULT_MSG_SUCCESS[language],
|
|
};
|
|
}
|
|
// 地址找不到
|
|
if (res.status === 404 || res.status === 405) {
|
|
return {
|
|
code: RESULT_CODE_ERROR,
|
|
msg: RESULT_MSG_URL_NOTFOUND[language],
|
|
};
|
|
}
|
|
// 身份授权
|
|
if (res.status === 401) {
|
|
removeToken();
|
|
window.location.reload();
|
|
}
|
|
return false;
|
|
}
|