mirror of
https://github.com/Tencent/tdesign-vue-next-starter.git
synced 2024-12-23 00:26:33 +08:00
feat: 请求部分代码改造 (#185)
* feat: 重新封装axios * chore: 将请求封装规范为request * fix: 修正引用方式 * perf: 使用lodash替换部分函数 * feat: axios新增支持过滤重复请求 * feat: axios新增切换地址直连或Vite反向代理 * chore: 将lodash-es切换至lodash并将引入方式改为路径引入
This commit is contained in:
parent
5be1b14b07
commit
704c162e36
|
@ -60,8 +60,11 @@
|
|||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/ban-types": "off"
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"class-methods-use-this": "off" // 因为AxiosCancel必须实例化而能静态化所以加的规则,如果有办法解决可以取消
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
|
|
@ -21,9 +21,11 @@
|
|||
"axios": "^0.27.2",
|
||||
"dayjs": "^1.10.6",
|
||||
"echarts": "~5.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.0.11",
|
||||
"qrcode.vue": "^3.2.2",
|
||||
"qs": "^6.10.5",
|
||||
"tdesign-icons-vue-next": "^0.1.1",
|
||||
"tdesign-vue-next": "^0.16.0",
|
||||
"tvision-color": "^1.3.1",
|
||||
|
@ -35,6 +37,8 @@
|
|||
"@commitlint/cli": "^16.2.1",
|
||||
"@commitlint/config-conventional": "^16.2.1",
|
||||
"@types/echarts": "^4.9.10",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/ws": "^8.2.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.3",
|
||||
"@typescript-eslint/parser": "^4.29.3",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export default {
|
||||
isRequestProxy: true,
|
||||
development: {
|
||||
// 开发环境接口请求
|
||||
host: 'https://service-exndqyuk-1257786608.gz.apigw.tencentcs.com',
|
||||
|
|
|
@ -120,7 +120,7 @@ export default {
|
|||
import { ref, onMounted } from 'vue';
|
||||
import { prefix } from '@/config/global';
|
||||
import { BASE_INFO_DATA, TABLE_COLUMNS_DATA as columns, PRODUCT_LIST } from './constants';
|
||||
import request from '@/utils/request';
|
||||
import { request } from '@/utils/request';
|
||||
import { ResDataType } from '@/types/interface';
|
||||
|
||||
import Product from './components/Product.vue';
|
||||
|
@ -145,7 +145,7 @@ const stepUpdate = () => {
|
|||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res: ResDataType = await request.get('/api/get-purchase-list');
|
||||
const res: ResDataType = await request.get({ url: '/api/get-purchase-list' });
|
||||
if (res.code === 0) {
|
||||
const { list = [] } = res.data;
|
||||
data.value = list;
|
||||
|
|
|
@ -90,7 +90,7 @@ import { changeChartsTheme } from '@/utils/color';
|
|||
|
||||
import { prefix } from '@/config/global';
|
||||
import { ResDataType } from '@/types/interface';
|
||||
import request from '@/utils/request';
|
||||
import { request } from '@/utils/request';
|
||||
|
||||
echarts.use([
|
||||
TitleComponent,
|
||||
|
@ -115,7 +115,7 @@ const pagination = ref({
|
|||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res: ResDataType = await request.get('/api/get-project-list');
|
||||
const res: ResDataType = await request.get({ url: '/api/get-project-list' });
|
||||
if (res.code === 0) {
|
||||
const { list = [] } = res.data;
|
||||
data.value = list;
|
||||
|
|
|
@ -83,7 +83,7 @@ import { MessagePlugin } from 'tdesign-vue-next';
|
|||
import { CONTRACT_STATUS, CONTRACT_TYPES, CONTRACT_PAYMENT_TYPES } from '@/constants';
|
||||
import Trend from '@/components/trend/index.vue';
|
||||
import { ResDataType } from '@/types/interface';
|
||||
import request from '@/utils/request';
|
||||
import { request } from '@/utils/request';
|
||||
import { useSettingStore } from '@/store';
|
||||
|
||||
import { COLUMNS } from './constants';
|
||||
|
@ -103,7 +103,7 @@ const dataLoading = ref(false);
|
|||
const fetchData = async () => {
|
||||
dataLoading.value = true;
|
||||
try {
|
||||
const res: ResDataType = await request.get('/api/get-list');
|
||||
const res: ResDataType = await request.get({ url: '/api/get-list' });
|
||||
if (res.code === 0) {
|
||||
const { list = [] } = res.data;
|
||||
data.value = list;
|
||||
|
|
|
@ -73,7 +73,7 @@ import { SearchIcon } from 'tdesign-icons-vue-next';
|
|||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import ProductCard from '@/components/product-card/index.vue';
|
||||
import DialogForm from './components/DialogForm.vue';
|
||||
import request from '@/utils/request';
|
||||
import { request } from '@/utils/request';
|
||||
import { ResDataType } from '@/types/interface';
|
||||
|
||||
const INITIAL_DATA = {
|
||||
|
@ -93,7 +93,7 @@ const dataLoading = ref(true);
|
|||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res: ResDataType = await request.get('/api/get-card-list');
|
||||
const res: ResDataType = await request.get({ url: '/api/get-card-list' });
|
||||
if (res.code === 0) {
|
||||
const { list = [] } = res.data;
|
||||
productList.value = list;
|
||||
|
|
|
@ -117,7 +117,7 @@
|
|||
import { ref, computed, onMounted } from 'vue';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import Trend from '@/components/trend/index.vue';
|
||||
import request from '@/utils/request';
|
||||
import { request } from '@/utils/request';
|
||||
import { ResDataType } from '@/types/interface';
|
||||
import { useSettingStore } from '@/store';
|
||||
|
||||
|
@ -199,7 +199,7 @@ const dataLoading = ref(false);
|
|||
const fetchData = async () => {
|
||||
dataLoading.value = true;
|
||||
try {
|
||||
const res: ResDataType = await request.get('/api/get-list');
|
||||
const res: ResDataType = await request.get({ url: '/api/get-list' });
|
||||
if (res.code === 0) {
|
||||
const { list = [] } = res.data;
|
||||
data.value = list;
|
||||
|
|
29
src/types/axios.d.ts
vendored
Normal file
29
src/types/axios.d.ts
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { AxiosRequestConfig } from 'axios';
|
||||
|
||||
export interface RequestOptions {
|
||||
apiUrl?: string;
|
||||
isJoinPrefix?: boolean;
|
||||
urlPrefix?: string;
|
||||
joinParamsToUrl?: boolean;
|
||||
formatDate?: boolean;
|
||||
isTransformResponse?: boolean;
|
||||
isReturnNativeResponse?: boolean;
|
||||
ignoreRepeatRequest?: boolean;
|
||||
joinTime?: boolean;
|
||||
withToken?: boolean;
|
||||
retry?: {
|
||||
count: number;
|
||||
delay: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Result<T = any> {
|
||||
code: number;
|
||||
type: 'success' | 'error' | 'warning';
|
||||
message: string;
|
||||
result: T;
|
||||
}
|
||||
|
||||
export interface AxiosRequestConfigRetry extends AxiosRequestConfig {
|
||||
retryCount?: number;
|
||||
}
|
2
src/types/globals.d.ts
vendored
2
src/types/globals.d.ts
vendored
|
@ -21,3 +21,5 @@ declare module '*.svg' {
|
|||
const CONTENT: string;
|
||||
export default CONTENT;
|
||||
}
|
||||
|
||||
declare type Recordable<T = any> = Record<string, T>;
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
import axios, { AxiosInstance } from 'axios';
|
||||
import proxy from '../config/proxy';
|
||||
|
||||
const env = import.meta.env.MODE || 'development';
|
||||
|
||||
const host = env === 'mock' ? '/' : proxy[env].host; // 如果是mock模式 就不配置host 会走本地Mock拦截
|
||||
|
||||
const CODE = {
|
||||
LOGIN_TIMEOUT: 1000,
|
||||
REQUEST_SUCCESS: 0,
|
||||
REQUEST_FAILED: 1001,
|
||||
};
|
||||
|
||||
const instance: AxiosInstance = axios.create({
|
||||
baseURL: host,
|
||||
timeout: 5000,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(response) => {
|
||||
if (response.status === 200) {
|
||||
const { data } = response;
|
||||
if (data.code === CODE.REQUEST_SUCCESS) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return response;
|
||||
},
|
||||
(err) => {
|
||||
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(null);
|
||||
}, config.retryDelay || 1);
|
||||
});
|
||||
|
||||
return backoff.then(() => instance(config));
|
||||
},
|
||||
);
|
||||
|
||||
export default instance;
|
182
src/utils/request/Axios.ts
Normal file
182
src/utils/request/Axios.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
import axios, { AxiosRequestConfig, AxiosInstance, AxiosResponse, AxiosError } from 'axios';
|
||||
import { stringify } from 'qs';
|
||||
import isFunction from 'lodash/isFunction';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { CreateAxiosOptions } from './AxiosTransform';
|
||||
import { AxiosCanceler } from './AxiosCancel';
|
||||
import { AxiosRequestConfigRetry, RequestOptions, Result } from '@/types/axios';
|
||||
|
||||
// Axios模块
|
||||
export class VAxios {
|
||||
// axios句柄
|
||||
private instance: AxiosInstance;
|
||||
|
||||
// axios选项
|
||||
private readonly options: CreateAxiosOptions;
|
||||
|
||||
constructor(options: CreateAxiosOptions) {
|
||||
this.options = options;
|
||||
this.instance = axios.create(options);
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
// 创建axios句柄
|
||||
private createAxios(config: CreateAxiosOptions): void {
|
||||
this.instance = axios.create(config);
|
||||
}
|
||||
|
||||
// 获取数据处理
|
||||
private getTransform() {
|
||||
const { transform } = this.options;
|
||||
return transform;
|
||||
}
|
||||
|
||||
// 获取句柄
|
||||
getAxios(): AxiosInstance {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
// 配置 axios
|
||||
configAxios(config: CreateAxiosOptions) {
|
||||
if (!this.instance) {
|
||||
return;
|
||||
}
|
||||
this.createAxios(config);
|
||||
}
|
||||
|
||||
// 设置通用头信息
|
||||
setHeader(headers: Record<string, string>): void {
|
||||
if (!this.instance) {
|
||||
return;
|
||||
}
|
||||
Object.assign(this.instance.defaults.headers, headers);
|
||||
}
|
||||
|
||||
// 设置拦截器
|
||||
private setupInterceptors() {
|
||||
const transform = this.getTransform();
|
||||
if (!transform) {
|
||||
return;
|
||||
}
|
||||
const { requestInterceptors, requestInterceptorsCatch, responseInterceptors, responseInterceptorsCatch } =
|
||||
transform;
|
||||
const axiosCanceler = new AxiosCanceler();
|
||||
|
||||
// 请求配置处理
|
||||
this.instance.interceptors.request.use((config: AxiosRequestConfig) => {
|
||||
const {
|
||||
headers: { ignoreRepeatRequest },
|
||||
} = config;
|
||||
const ignoreRepeat = ignoreRepeatRequest ?? this.options.requestOptions?.ignoreRepeatRequest;
|
||||
if (!ignoreRepeat) axiosCanceler.addPending(config);
|
||||
|
||||
if (requestInterceptors && isFunction(requestInterceptors)) {
|
||||
config = requestInterceptors(config, this.options);
|
||||
}
|
||||
return config;
|
||||
}, undefined);
|
||||
|
||||
// 请求错误处理
|
||||
if (requestInterceptorsCatch && isFunction(requestInterceptorsCatch)) {
|
||||
this.instance.interceptors.request.use(undefined, requestInterceptorsCatch);
|
||||
}
|
||||
|
||||
// 响应结果处理
|
||||
this.instance.interceptors.response.use((res: AxiosResponse) => {
|
||||
if (res) axiosCanceler.removePending(res.config);
|
||||
if (responseInterceptors && isFunction(responseInterceptors)) {
|
||||
res = responseInterceptors(res);
|
||||
}
|
||||
return res;
|
||||
}, undefined);
|
||||
|
||||
// 响应错误处理
|
||||
if (responseInterceptorsCatch && isFunction(responseInterceptorsCatch)) {
|
||||
this.instance.interceptors.response.use(undefined, responseInterceptorsCatch);
|
||||
}
|
||||
}
|
||||
|
||||
// 支持Form Data
|
||||
supportFormData(config: AxiosRequestConfig) {
|
||||
const headers = config.headers || this.options.headers;
|
||||
const contentType = headers?.['Content-Type'] || headers?.['content-type'];
|
||||
|
||||
if (
|
||||
contentType !== 'application/x-www-form-urlencoded;charset=UTF-8' ||
|
||||
!Reflect.has(config, 'data') ||
|
||||
config.method?.toUpperCase() === 'GET'
|
||||
) {
|
||||
return config;
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
data: stringify(config.data, { arrayFormat: 'brackets' }),
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
patch<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
|
||||
return this.request({ ...config, method: 'PATCH' }, options);
|
||||
}
|
||||
|
||||
// 请求
|
||||
async request<T = any>(config: AxiosRequestConfigRetry, options?: RequestOptions): Promise<T> {
|
||||
let conf: CreateAxiosOptions = cloneDeep(config);
|
||||
const transform = this.getTransform();
|
||||
|
||||
const { requestOptions } = this.options;
|
||||
|
||||
const opt: RequestOptions = { ...requestOptions, ...options };
|
||||
|
||||
const { beforeRequestHook, requestCatchHook, transformRequestHook } = transform || {};
|
||||
if (beforeRequestHook && isFunction(beforeRequestHook)) {
|
||||
conf = beforeRequestHook(conf, opt);
|
||||
}
|
||||
conf.requestOptions = opt;
|
||||
|
||||
conf = this.supportFormData(conf);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.instance
|
||||
.request<any, AxiosResponse<Result>>(!config.retryCount ? conf : config)
|
||||
.then((res: AxiosResponse<Result>) => {
|
||||
if (transformRequestHook && isFunction(transformRequestHook)) {
|
||||
try {
|
||||
const ret = transformRequestHook(res, opt);
|
||||
resolve(ret);
|
||||
} catch (err) {
|
||||
reject(err || new Error('请求错误!'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
resolve(res as unknown as Promise<T>);
|
||||
})
|
||||
.catch((e: Error | AxiosError) => {
|
||||
if (requestCatchHook && isFunction(requestCatchHook)) {
|
||||
reject(requestCatchHook(e, opt));
|
||||
return;
|
||||
}
|
||||
if (axios.isAxiosError(e)) {
|
||||
// 在这里重写Axios的错误信息
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
49
src/utils/request/AxiosCancel.ts
Normal file
49
src/utils/request/AxiosCancel.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import type { AxiosRequestConfig, Canceler } from 'axios';
|
||||
import axios from 'axios';
|
||||
import isFunction from 'lodash/isFunction';
|
||||
|
||||
// 存储请求与取消令牌的键值对列表
|
||||
let pendingMap = new Map<string, Canceler>();
|
||||
|
||||
export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&');
|
||||
|
||||
export class AxiosCanceler {
|
||||
// 添加请求到列表
|
||||
addPending(config: AxiosRequestConfig) {
|
||||
this.removePending(config);
|
||||
const url = getPendingUrl(config);
|
||||
config.cancelToken =
|
||||
config.cancelToken ||
|
||||
new axios.CancelToken((cancel) => {
|
||||
if (!pendingMap.has(url)) {
|
||||
// 如果当前没有相同请求就添加
|
||||
pendingMap.set(url, cancel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 清空所有请求
|
||||
removeAllPending() {
|
||||
pendingMap.forEach((cancel) => {
|
||||
if (cancel && isFunction(cancel)) cancel();
|
||||
});
|
||||
pendingMap.clear();
|
||||
}
|
||||
|
||||
// 移除某个请求
|
||||
removePending(config: AxiosRequestConfig) {
|
||||
const url = getPendingUrl(config);
|
||||
|
||||
if (pendingMap.has(url)) {
|
||||
// If there is a current request identifier in pending,
|
||||
// the current request needs to be cancelled and removed
|
||||
const cancel = pendingMap.get(url);
|
||||
if (cancel) cancel(url);
|
||||
pendingMap.delete(url);
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
pendingMap = new Map<string, Canceler>();
|
||||
}
|
||||
}
|
37
src/utils/request/AxiosTransform.ts
Normal file
37
src/utils/request/AxiosTransform.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { AxiosError } from 'axios';
|
||||
import type { RequestOptions, Result } from '@/types/axios';
|
||||
|
||||
// 创建Axios选项
|
||||
export interface CreateAxiosOptions extends AxiosRequestConfig {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
|
||||
authenticationScheme?: string;
|
||||
// 数据处理
|
||||
transform?: AxiosTransform;
|
||||
// 请求选项
|
||||
requestOptions?: RequestOptions;
|
||||
}
|
||||
|
||||
// Axios 数据处理
|
||||
export abstract class AxiosTransform {
|
||||
// 请求前Hook
|
||||
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
|
||||
|
||||
// 转换前Hook
|
||||
transformRequestHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
|
||||
|
||||
// 请求失败处理
|
||||
requestCatchHook?: (e: Error | AxiosError, options: RequestOptions) => Promise<any>;
|
||||
|
||||
// 请求前的拦截器
|
||||
requestInterceptors?: (config: AxiosRequestConfig, options: CreateAxiosOptions) => AxiosRequestConfig;
|
||||
|
||||
// 请求后的拦截器
|
||||
responseInterceptors?: (res: AxiosResponse) => AxiosResponse;
|
||||
|
||||
// 请求前的拦截器错误处理
|
||||
requestInterceptorsCatch?: (error: AxiosError) => void;
|
||||
|
||||
// 请求后的拦截器错误处理
|
||||
responseInterceptorsCatch?: (error: AxiosError) => void;
|
||||
}
|
195
src/utils/request/index.ts
Normal file
195
src/utils/request/index.ts
Normal file
|
@ -0,0 +1,195 @@
|
|||
// axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
|
||||
import isString from 'lodash/isFunction';
|
||||
import merge from 'lodash/merge';
|
||||
import type { AxiosTransform, CreateAxiosOptions } from './AxiosTransform';
|
||||
import { VAxios } from './Axios';
|
||||
import proxy from '@/config/proxy';
|
||||
import { joinTimestamp, formatRequestDate, setObjToUrlParams } from './utils';
|
||||
import { TOKEN_NAME } from '@/config/global';
|
||||
|
||||
const env = import.meta.env.MODE || 'development';
|
||||
|
||||
// 如果是mock模式 或 没启用直连代理 就不配置host 会走本地Mock拦截 或 Vite 代理
|
||||
const host = env === 'mock' || !proxy.isRequestProxy ? '' : proxy[env].host;
|
||||
|
||||
// 数据处理,方便区分多种处理方式
|
||||
const transform: AxiosTransform = {
|
||||
// 处理请求数据。如果数据不是预期格式,可直接抛出错误
|
||||
transformRequestHook: (res, options) => {
|
||||
const { isTransformResponse, isReturnNativeResponse } = options;
|
||||
|
||||
// 如果204无内容直接返回
|
||||
const method = res.config.method?.toLowerCase();
|
||||
if (res.status === 204 || method === 'put' || method === 'patch') {
|
||||
return res;
|
||||
}
|
||||
|
||||
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
||||
if (isReturnNativeResponse) {
|
||||
return res;
|
||||
}
|
||||
// 不进行任何处理,直接返回
|
||||
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
|
||||
if (!isTransformResponse) {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// 错误的时候返回
|
||||
const { data } = res;
|
||||
if (!data) {
|
||||
throw new Error('请求接口错误');
|
||||
}
|
||||
|
||||
// 这里 message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
|
||||
const { message } = data;
|
||||
|
||||
// 这里逻辑可以根据项目进行修改
|
||||
const hasSuccess = data && !Reflect.has(data, 'error');
|
||||
if (hasSuccess) {
|
||||
return data;
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
},
|
||||
|
||||
// 请求前处理配置
|
||||
beforeRequestHook: (config, options) => {
|
||||
const { apiUrl, isJoinPrefix, urlPrefix, joinParamsToUrl, formatDate, joinTime = true } = options;
|
||||
|
||||
// 添加接口前缀
|
||||
if (isJoinPrefix) {
|
||||
config.url = `${urlPrefix}${config.url}`;
|
||||
}
|
||||
|
||||
// 将baseUrl拼接
|
||||
if (apiUrl && isString(apiUrl)) {
|
||||
config.url = `${apiUrl}${config.url}`;
|
||||
}
|
||||
const params = config.params || {};
|
||||
const data = config.data || false;
|
||||
|
||||
if (formatDate && data && !isString(data)) {
|
||||
formatRequestDate(data);
|
||||
}
|
||||
if (config.method?.toUpperCase() === '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)) {
|
||||
if (formatDate) {
|
||||
formatRequestDate(params);
|
||||
}
|
||||
if (Reflect.has(config, 'data') && config.data && Object.keys(config.data).length > 0) {
|
||||
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 });
|
||||
}
|
||||
} else {
|
||||
// 兼容restful风格
|
||||
config.url += params;
|
||||
config.params = undefined;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
||||
// 请求拦截器处理
|
||||
requestInterceptors: (config, options) => {
|
||||
// 请求之前处理config
|
||||
const token = localStorage.getItem(TOKEN_NAME);
|
||||
if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
|
||||
// jwt token
|
||||
(config as Recordable).headers.Authorization = options.authenticationScheme
|
||||
? `${options.authenticationScheme} ${token}`
|
||||
: token;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
||||
// 响应拦截器处理
|
||||
responseInterceptors: (res) => {
|
||||
return res;
|
||||
},
|
||||
|
||||
// 响应错误处理
|
||||
responseInterceptorsCatch: (error: any) => {
|
||||
const { config } = error;
|
||||
if (!config || !config.requestOptions.retry) return Promise.reject(error);
|
||||
|
||||
config.retryCount = config.retryCount || 0;
|
||||
|
||||
if (config.retryCount >= config.requestOptions.retry.count) return Promise.reject(error);
|
||||
|
||||
config.retryCount += 1;
|
||||
|
||||
const backoff = new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(config);
|
||||
}, config.requestOptions.retry.delay || 1);
|
||||
});
|
||||
|
||||
return backoff.then((config) => request.request(config));
|
||||
},
|
||||
};
|
||||
|
||||
function createAxios(opt?: Partial<CreateAxiosOptions>) {
|
||||
return new VAxios(
|
||||
merge(
|
||||
<CreateAxiosOptions>{
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
|
||||
// 例如: authenticationScheme: 'Bearer'
|
||||
authenticationScheme: '',
|
||||
// 超时
|
||||
timeout: 10 * 1000,
|
||||
// 携带Cookie
|
||||
withCredentials: true,
|
||||
// 头信息
|
||||
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
|
||||
// 数据处理方式
|
||||
transform,
|
||||
// 配置项,下面的选项都可以在独立的接口请求中覆盖
|
||||
requestOptions: {
|
||||
// 接口地址
|
||||
apiUrl: host,
|
||||
// 是否自动添加接口前缀
|
||||
isJoinPrefix: false,
|
||||
// 接口前缀
|
||||
// 例如: https://www.baidu.com/api
|
||||
// urlPrefix: '/api'
|
||||
urlPrefix: '/api',
|
||||
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
||||
isReturnNativeResponse: false,
|
||||
// 需要对返回数据进行处理
|
||||
isTransformResponse: true,
|
||||
// post请求的时候添加参数到url
|
||||
joinParamsToUrl: false,
|
||||
// 格式化提交参数时间
|
||||
formatDate: true,
|
||||
// 是否加入时间戳
|
||||
joinTime: true,
|
||||
// 忽略重复请求
|
||||
ignoreRepeatRequest: true,
|
||||
// 是否携带token
|
||||
withToken: true,
|
||||
// 重试
|
||||
retry: {
|
||||
count: 3,
|
||||
delay: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
opt || {},
|
||||
),
|
||||
);
|
||||
}
|
||||
export const request = createAxios();
|
54
src/utils/request/utils.ts
Normal file
54
src/utils/request/utils.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import isString from 'lodash/isString';
|
||||
import isObject from 'lodash/isObject';
|
||||
|
||||
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
// 格式化提交参数时间
|
||||
export function formatRequestDate(params: Recordable) {
|
||||
if (Object.prototype.toString.call(params) !== '[object Object]') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key in params) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
if (params[key] && params[key]._isAMomentObject) {
|
||||
params[key] = params[key].format(DATE_TIME_FORMAT);
|
||||
}
|
||||
if (isString(key)) {
|
||||
const value = params[key];
|
||||
if (value) {
|
||||
try {
|
||||
params[key] = isString(value) ? value.trim() : value;
|
||||
} catch (error: any) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isObject(params[key])) {
|
||||
formatRequestDate(params[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将对象转为Url参数
|
||||
export function setObjToUrlParams(baseUrl: string, obj: object): string {
|
||||
let parameters = '';
|
||||
for (const key in obj) {
|
||||
parameters += `${key}=${encodeURIComponent(obj[key])}&`;
|
||||
}
|
||||
parameters = parameters.replace(/&$/, '');
|
||||
return /\?$/.test(baseUrl) ? baseUrl + parameters : baseUrl.replace(/\/?$/, '?') + parameters;
|
||||
}
|
Loading…
Reference in New Issue
Block a user