Files
fe.ems.vue3/src/plugins/http-fetch.ts
T

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;
}