mirror of
https://github.com/Tencent/tdesign-vue-next-starter.git
synced 2024-12-23 00:16:32 +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": "^_"
|
"varsIgnorePattern": "^_"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"no-use-before-define": "off",
|
||||||
|
"@typescript-eslint/no-use-before-define": "off",
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
"@typescript-eslint/ban-types": "off"
|
"@typescript-eslint/ban-types": "off",
|
||||||
|
"class-methods-use-this": "off" // 因为AxiosCancel必须实例化而能静态化所以加的规则,如果有办法解决可以取消
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -21,9 +21,11 @@
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"dayjs": "^1.10.6",
|
"dayjs": "^1.10.6",
|
||||||
"echarts": "~5.1.2",
|
"echarts": "~5.1.2",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^2.0.11",
|
"pinia": "^2.0.11",
|
||||||
"qrcode.vue": "^3.2.2",
|
"qrcode.vue": "^3.2.2",
|
||||||
|
"qs": "^6.10.5",
|
||||||
"tdesign-icons-vue-next": "^0.1.1",
|
"tdesign-icons-vue-next": "^0.1.1",
|
||||||
"tdesign-vue-next": "^0.16.0",
|
"tdesign-vue-next": "^0.16.0",
|
||||||
"tvision-color": "^1.3.1",
|
"tvision-color": "^1.3.1",
|
||||||
|
@ -35,6 +37,8 @@
|
||||||
"@commitlint/cli": "^16.2.1",
|
"@commitlint/cli": "^16.2.1",
|
||||||
"@commitlint/config-conventional": "^16.2.1",
|
"@commitlint/config-conventional": "^16.2.1",
|
||||||
"@types/echarts": "^4.9.10",
|
"@types/echarts": "^4.9.10",
|
||||||
|
"@types/lodash": "^4.14.182",
|
||||||
|
"@types/qs": "^6.9.7",
|
||||||
"@types/ws": "^8.2.2",
|
"@types/ws": "^8.2.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.29.3",
|
"@typescript-eslint/eslint-plugin": "^4.29.3",
|
||||||
"@typescript-eslint/parser": "^4.29.3",
|
"@typescript-eslint/parser": "^4.29.3",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export default {
|
export default {
|
||||||
|
isRequestProxy: true,
|
||||||
development: {
|
development: {
|
||||||
// 开发环境接口请求
|
// 开发环境接口请求
|
||||||
host: 'https://service-exndqyuk-1257786608.gz.apigw.tencentcs.com',
|
host: 'https://service-exndqyuk-1257786608.gz.apigw.tencentcs.com',
|
||||||
|
|
|
@ -120,7 +120,7 @@ export default {
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { prefix } from '@/config/global';
|
import { prefix } from '@/config/global';
|
||||||
import { BASE_INFO_DATA, TABLE_COLUMNS_DATA as columns, PRODUCT_LIST } from './constants';
|
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 { ResDataType } from '@/types/interface';
|
||||||
|
|
||||||
import Product from './components/Product.vue';
|
import Product from './components/Product.vue';
|
||||||
|
@ -145,7 +145,7 @@ const stepUpdate = () => {
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
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) {
|
if (res.code === 0) {
|
||||||
const { list = [] } = res.data;
|
const { list = [] } = res.data;
|
||||||
data.value = list;
|
data.value = list;
|
||||||
|
|
|
@ -90,7 +90,7 @@ import { changeChartsTheme } from '@/utils/color';
|
||||||
|
|
||||||
import { prefix } from '@/config/global';
|
import { prefix } from '@/config/global';
|
||||||
import { ResDataType } from '@/types/interface';
|
import { ResDataType } from '@/types/interface';
|
||||||
import request from '@/utils/request';
|
import { request } from '@/utils/request';
|
||||||
|
|
||||||
echarts.use([
|
echarts.use([
|
||||||
TitleComponent,
|
TitleComponent,
|
||||||
|
@ -115,7 +115,7 @@ const pagination = ref({
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
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) {
|
if (res.code === 0) {
|
||||||
const { list = [] } = res.data;
|
const { list = [] } = res.data;
|
||||||
data.value = list;
|
data.value = list;
|
||||||
|
|
|
@ -83,7 +83,7 @@ import { MessagePlugin } from 'tdesign-vue-next';
|
||||||
import { CONTRACT_STATUS, CONTRACT_TYPES, CONTRACT_PAYMENT_TYPES } from '@/constants';
|
import { CONTRACT_STATUS, CONTRACT_TYPES, CONTRACT_PAYMENT_TYPES } from '@/constants';
|
||||||
import Trend from '@/components/trend/index.vue';
|
import Trend from '@/components/trend/index.vue';
|
||||||
import { ResDataType } from '@/types/interface';
|
import { ResDataType } from '@/types/interface';
|
||||||
import request from '@/utils/request';
|
import { request } from '@/utils/request';
|
||||||
import { useSettingStore } from '@/store';
|
import { useSettingStore } from '@/store';
|
||||||
|
|
||||||
import { COLUMNS } from './constants';
|
import { COLUMNS } from './constants';
|
||||||
|
@ -103,7 +103,7 @@ const dataLoading = ref(false);
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
dataLoading.value = true;
|
dataLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const res: ResDataType = await request.get('/api/get-list');
|
const res: ResDataType = await request.get({ url: '/api/get-list' });
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
const { list = [] } = res.data;
|
const { list = [] } = res.data;
|
||||||
data.value = list;
|
data.value = list;
|
||||||
|
|
|
@ -73,7 +73,7 @@ import { SearchIcon } from 'tdesign-icons-vue-next';
|
||||||
import { MessagePlugin } from 'tdesign-vue-next';
|
import { MessagePlugin } from 'tdesign-vue-next';
|
||||||
import ProductCard from '@/components/product-card/index.vue';
|
import ProductCard from '@/components/product-card/index.vue';
|
||||||
import DialogForm from './components/DialogForm.vue';
|
import DialogForm from './components/DialogForm.vue';
|
||||||
import request from '@/utils/request';
|
import { request } from '@/utils/request';
|
||||||
import { ResDataType } from '@/types/interface';
|
import { ResDataType } from '@/types/interface';
|
||||||
|
|
||||||
const INITIAL_DATA = {
|
const INITIAL_DATA = {
|
||||||
|
@ -93,7 +93,7 @@ const dataLoading = ref(true);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
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) {
|
if (res.code === 0) {
|
||||||
const { list = [] } = res.data;
|
const { list = [] } = res.data;
|
||||||
productList.value = list;
|
productList.value = list;
|
||||||
|
|
|
@ -117,7 +117,7 @@
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { MessagePlugin } from 'tdesign-vue-next';
|
import { MessagePlugin } from 'tdesign-vue-next';
|
||||||
import Trend from '@/components/trend/index.vue';
|
import Trend from '@/components/trend/index.vue';
|
||||||
import request from '@/utils/request';
|
import { request } from '@/utils/request';
|
||||||
import { ResDataType } from '@/types/interface';
|
import { ResDataType } from '@/types/interface';
|
||||||
import { useSettingStore } from '@/store';
|
import { useSettingStore } from '@/store';
|
||||||
|
|
||||||
|
@ -199,7 +199,7 @@ const dataLoading = ref(false);
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
dataLoading.value = true;
|
dataLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const res: ResDataType = await request.get('/api/get-list');
|
const res: ResDataType = await request.get({ url: '/api/get-list' });
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
const { list = [] } = res.data;
|
const { list = [] } = res.data;
|
||||||
data.value = list;
|
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;
|
const CONTENT: string;
|
||||||
export default CONTENT;
|
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