perf: improve the logic related to login expiration

This commit is contained in:
vince
2024-07-11 20:11:11 +08:00
parent 8e6c1abf19
commit d62a3da009
43 changed files with 552 additions and 347 deletions

View File

@@ -16,6 +16,7 @@ const defaultPreferences: Preferences = {
isMobile: false,
layout: 'sidebar-nav',
locale: 'zh-CN',
loginExpiredMode: 'page',
name: 'Vben Admin Pro',
},
breadcrumb: {

View File

@@ -7,6 +7,12 @@ import type {
ThemeModeType,
} from '@vben-core/typings';
/**
* 登录过期模式
* 'modal' 弹窗模式 | 'page' 页面模式
*/
type LoginExpiredModeType = 'modal' | 'page';
type BreadcrumbStyleType = 'background' | 'normal';
type AccessModeType = 'allow-all' | 'backend' | 'frontend';
@@ -44,6 +50,8 @@ interface AppPreferences {
layout: LayoutType;
/** 支持的语言 */
locale: SupportedLanguagesType;
/** 登录过期模式 */
loginExpiredMode: LoginExpiredModeType;
/** 应用名 */
name: string;
}
@@ -236,6 +244,7 @@ export type {
HeaderPreferences,
LayoutHeaderModeType,
LayoutType,
LoginExpiredModeType,
LogoPreferences,
NavigationPreferences,
NavigationStyleType,

View File

@@ -38,6 +38,7 @@
}
},
"dependencies": {
"@vben-core/locales": "workspace:*",
"@vben-core/toolkit": "workspace:*",
"axios": "^1.7.2",
"vue-request": "^2.0.4"

View File

@@ -1,3 +1,2 @@
export * from './request-client';
export type * from './types';
export * from './util';

View File

@@ -17,16 +17,22 @@ class InterceptorManager {
) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>,
rejected?: (error: any) => any,
) {
this.axiosInstance.interceptors.request.use(fulfilled, rejected);
this.axiosInstance.interceptors.request.use(
fulfilled,
rejected || ((res) => res),
);
}
addResponseInterceptor(
addResponseInterceptor<T = any>(
fulfilled: (
response: AxiosResponse,
response: AxiosResponse<T>,
) => AxiosResponse | Promise<AxiosResponse>,
rejected?: (error: any) => any,
) {
this.axiosInstance.interceptors.response.use(fulfilled, rejected);
this.axiosInstance.interceptors.response.use(
fulfilled,
rejected || ((res) => res),
);
}
}

View File

@@ -8,6 +8,7 @@ import type {
import type { MakeAuthorizationFn, RequestClientOptions } from './types';
import { $t } from '@vben-core/locales';
import { merge } from '@vben-core/toolkit';
import axios from 'axios';
@@ -19,6 +20,7 @@ import { FileUploader } from './modules/uploader';
class RequestClient {
private instance: AxiosInstance;
private makeAuthorization: MakeAuthorizationFn | undefined;
private options: RequestClientOptions;
public addRequestInterceptor: InterceptorManager['addRequestInterceptor'];
public addResponseInterceptor: InterceptorManager['addResponseInterceptor'];
public download: FileDownloader['download'];
@@ -39,6 +41,7 @@ class RequestClient {
timeout: 10_000,
};
const { makeAuthorization, ...axiosConfig } = options;
this.options = options;
const requestConfig = merge(axiosConfig, defaultConfig);
this.instance = axios.create(requestConfig);
@@ -77,24 +80,86 @@ class RequestClient {
});
}
private errorHandler(error: any) {
return Promise.reject(error);
private setupAuthorizationInterceptor() {
this.addRequestInterceptor(
(config: InternalAxiosRequestConfig) => {
const authorization = this.makeAuthorization?.(config);
if (authorization) {
const { token } = authorization.tokenHandler?.() ?? {};
config.headers[authorization.key || 'Authorization'] = token;
}
return config;
},
(error: any) => Promise.reject(error),
);
}
private setupAuthorizationInterceptor() {
this.addRequestInterceptor((config: InternalAxiosRequestConfig) => {
const authorization = this.makeAuthorization?.(config);
if (authorization) {
const { token } = authorization.handler?.() ?? {};
config.headers[authorization.key || 'Authorization'] = token;
}
return config;
}, this.errorHandler);
private setupDefaultResponseInterceptor() {
this.addResponseInterceptor(
(response: AxiosResponse) => {
return response;
},
(error: any) => {
if (axios.isCancel(error)) {
return Promise.reject(error);
}
const err: string = error?.toString?.() ?? '';
let errMsg = '';
if (err?.includes('Network Error')) {
errMsg = $t('fallback.http.networkError');
} else if (error?.message?.includes?.('timeout')) {
errMsg = $t('fallback.http.requestTimeout');
}
const { makeAuthorization, makeErrorMessage } = this.options;
if (errMsg) {
makeErrorMessage?.(errMsg);
return Promise.reject(error);
}
let errorMessage = error?.response?.data?.error?.message ?? '';
const status = error?.response?.status;
switch (status) {
case 400: {
errorMessage = $t('fallback.http.badRequest');
break;
}
case 401: {
errorMessage = $t('fallback.http.unauthorized');
makeAuthorization?.().unAuthorizedHandler?.();
break;
}
case 403: {
errorMessage = $t('fallback.http.forbidden');
break;
}
// 404请求不存在
case 404: {
errorMessage = $t('fallback.http.notFound');
break;
}
case 408: {
errorMessage = $t('fallback.http.requestTimeout');
break;
}
default: {
errorMessage = $t('fallback.http.internalServerError');
}
}
makeErrorMessage?.(errorMessage);
return Promise.reject(error);
},
);
}
private setupInterceptors() {
// 默认拦截器
this.setupAuthorizationInterceptor();
this.setupDefaultResponseInterceptor();
}
/**

View File

@@ -7,18 +7,41 @@ type RequestContentType =
| 'multipart/form-data;charset=utf-8';
interface MakeAuthorization {
handler: () => { refreshToken: string; token: string } | null;
key?: string;
tokenHandler: () => { refreshToken: string; token: string } | null;
unAuthorizedHandler?: () => Promise<void>;
}
type MakeAuthorizationFn = (
config?: InternalAxiosRequestConfig,
) => MakeAuthorization;
type ErrorMessageFn = (message: string) => void;
interface RequestClientOptions extends CreateAxiosDefaults {
/**
* 用于生成Authorization
*/
makeAuthorization?: MakeAuthorizationFn;
/**
* 用于生成错误消息
*/
makeErrorMessage?: ErrorMessageFn;
}
export type { MakeAuthorizationFn, RequestClientOptions, RequestContentType };
interface HttpResponse<T = any> {
/**
* 0 表示成功 其他表示失败
* 0 means success, others means fail
*/
code: number;
data: T;
message: string;
}
export type {
HttpResponse,
MakeAuthorizationFn,
RequestClientOptions,
RequestContentType,
};

View File

@@ -1,25 +0,0 @@
import axios from 'axios';
import { describe, expect, it } from 'vitest';
import { isCancelError } from './util';
describe('isCancelError', () => {
const source = axios.CancelToken.source();
source.cancel('Operation canceled by the user.');
it('should detect cancellation', () => {
const error = new axios.Cancel('Operation canceled by the user.');
const result = isCancelError(error);
expect(result).toBe(true);
});
it('should not detect cancellation on regular errors', () => {
const error = new Error('Regular error');
const result = isCancelError(error);
expect(result).toBe(false);
});
});

View File

@@ -1,7 +0,0 @@
import axios from 'axios';
function isCancelError(error: any) {
return axios.isCancel(error);
}
export { isCancelError };