feat: refactor and improve the request client and support refreshToken (#4157)

* feat: refreshToken

* chore: store refreshToken

* chore: generate token using jsonwebtoken

* chore: set refreshToken in httpOnly cookie

* perf: authHeader verify

* chore: add add response interceptor

* chore: test refresh

* chore: handle logout

* chore: type

* chore: update pnpm-lock.yaml

* chore: remove test code

* chore: add todo comment

* chore: update pnpm-lock.yaml

* chore: remove default interceptors

* chore: copy codes

* chore: handle refreshToken invalid

* chore: add refreshToken preference

* chore: typo

* chore: refresh token逻辑调整

* refactor: interceptor presets

* chore: copy codes

* fix: ci errors

* chore: add missing await

* feat: 完善refresh-token逻辑及文档

* fix: ci error

* chore: filename

---------

Co-authored-by: vince <vince292007@gmail.com>
This commit is contained in:
Li Kui
2024-08-19 22:59:42 +08:00
committed by GitHub
parent f8485e8861
commit 01d60336a6
40 changed files with 1055 additions and 523 deletions

View File

@@ -3,17 +3,8 @@ import type {
AxiosRequestConfig,
AxiosResponse,
CreateAxiosDefaults,
InternalAxiosRequestConfig,
} from 'axios';
import type {
MakeAuthorizationFn,
MakeErrorMessageFn,
MakeRequestHeadersFn,
RequestClientOptions,
} from './types';
import { $t } from '@vben/locales';
import { merge } from '@vben/utils';
import axios from 'axios';
@@ -21,16 +12,19 @@ import axios from 'axios';
import { FileDownloader } from './modules/downloader';
import { InterceptorManager } from './modules/interceptor';
import { FileUploader } from './modules/uploader';
import { type RequestClientOptions } from './types';
class RequestClient {
private instance: AxiosInstance;
private makeAuthorization: MakeAuthorizationFn | undefined;
private makeErrorMessage: MakeErrorMessageFn | undefined;
private makeRequestHeaders: MakeRequestHeadersFn | undefined;
private readonly instance: AxiosInstance;
public addRequestInterceptor: InterceptorManager['addRequestInterceptor'];
public addResponseInterceptor: InterceptorManager['addResponseInterceptor'];
public download: FileDownloader['download'];
// 是否正在刷新token
public isRefreshing = false;
// 刷新token队列
public refreshTokenQueue: ((token: string) => void)[] = [];
public upload: FileUploader['upload'];
/**
@@ -38,7 +32,6 @@ class RequestClient {
* @param options - Axios请求配置可选
*/
constructor(options: RequestClientOptions = {}) {
this.bindMethods();
// 合并默认配置和传入的配置
const defaultConfig: CreateAxiosDefaults = {
headers: {
@@ -47,18 +40,11 @@ class RequestClient {
// 默认超时时间
timeout: 10_000,
};
const {
makeAuthorization,
makeErrorMessage,
makeRequestHeaders,
...axiosConfig
} = options;
const { ...axiosConfig } = options;
const requestConfig = merge(axiosConfig, defaultConfig);
this.instance = axios.create(requestConfig);
this.makeAuthorization = makeAuthorization;
this.makeRequestHeaders = makeRequestHeaders;
this.makeErrorMessage = makeErrorMessage;
this.bindMethods();
// 实例化拦截器管理器
const interceptorManager = new InterceptorManager(this.instance);
@@ -73,9 +59,6 @@ class RequestClient {
// 实例化文件下载器
const fileDownloader = new FileDownloader(this);
this.download = fileDownloader.download.bind(fileDownloader);
// 设置默认的拦截器
this.setupInterceptors();
}
private bindMethods() {
@@ -93,92 +76,6 @@ class RequestClient {
});
}
private setupDefaultResponseInterceptor() {
this.addRequestInterceptor(
(config: InternalAxiosRequestConfig) => {
const authorization = this.makeAuthorization?.(config);
if (authorization) {
const { token } = authorization.tokenHandler?.() ?? {};
config.headers[authorization.key || 'Authorization'] = token;
}
const requestHeader = this.makeRequestHeaders?.(config);
if (requestHeader) {
for (const [key, value] of Object.entries(requestHeader)) {
config.headers[key] = value;
}
}
return config;
},
(error: any) => Promise.reject(error),
);
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');
}
if (errMsg) {
this.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');
this.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');
}
}
this.makeErrorMessage?.(errorMessage);
return Promise.reject(error);
},
);
}
private setupInterceptors() {
// 默认拦截器
this.setupDefaultResponseInterceptor();
}
/**
* DELETE请求方法
*/