mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-09-23 12:06:36 +08:00
optimize(request): enhance request options and response handling with generic types
This commit is contained in:
parent
d6c8142bb4
commit
50a5cba088
@ -13,11 +13,12 @@ import type {
|
|||||||
ResponseType
|
ResponseType
|
||||||
} from './type';
|
} from './type';
|
||||||
|
|
||||||
function createCommonRequest<ResponseData = any>(
|
function createCommonRequest<
|
||||||
axiosConfig?: CreateAxiosDefaults,
|
ResponseData,
|
||||||
options?: Partial<RequestOption<ResponseData>>
|
ApiData = ResponseData,
|
||||||
) {
|
State extends Record<string, unknown> = Record<string, unknown>
|
||||||
const opts = createDefaultOptions<ResponseData>(options);
|
>(axiosConfig?: CreateAxiosDefaults, options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
|
||||||
|
const opts = createDefaultOptions<ResponseData, ApiData, State>(options);
|
||||||
|
|
||||||
const axiosConf = createAxiosConfig(axiosConfig);
|
const axiosConf = createAxiosConfig(axiosConfig);
|
||||||
const instance = axios.create(axiosConf);
|
const instance = axios.create(axiosConf);
|
||||||
@ -109,15 +110,19 @@ function createCommonRequest<ResponseData = any>(
|
|||||||
* @param axiosConfig axios config
|
* @param axiosConfig axios config
|
||||||
* @param options request options
|
* @param options request options
|
||||||
*/
|
*/
|
||||||
export function createRequest<ResponseData = any, State = Record<string, unknown>>(
|
export function createRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||||
axiosConfig?: CreateAxiosDefaults,
|
axiosConfig?: CreateAxiosDefaults,
|
||||||
options?: Partial<RequestOption<ResponseData>>
|
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||||
) {
|
) {
|
||||||
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
|
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(
|
||||||
|
axiosConfig,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
const request: RequestInstance<State> = async function request<T = any, R extends ResponseType = 'json'>(
|
const request: RequestInstance<ApiData, State> = async function request<
|
||||||
config: CustomAxiosRequestConfig
|
T extends ApiData = ApiData,
|
||||||
) {
|
R extends ResponseType = 'json'
|
||||||
|
>(config: CustomAxiosRequestConfig) {
|
||||||
const response: AxiosResponse<ResponseData> = await instance(config);
|
const response: AxiosResponse<ResponseData> = await instance(config);
|
||||||
|
|
||||||
const responseType = response.config?.responseType || 'json';
|
const responseType = response.config?.responseType || 'json';
|
||||||
@ -127,7 +132,7 @@ export function createRequest<ResponseData = any, State = Record<string, unknown
|
|||||||
}
|
}
|
||||||
|
|
||||||
return response.data as MappedType<R, T>;
|
return response.data as MappedType<R, T>;
|
||||||
} as RequestInstance<State>;
|
} as RequestInstance<ApiData, State>;
|
||||||
|
|
||||||
request.cancelRequest = cancelRequest;
|
request.cancelRequest = cancelRequest;
|
||||||
request.cancelAllRequest = cancelAllRequest;
|
request.cancelAllRequest = cancelAllRequest;
|
||||||
@ -144,14 +149,17 @@ export function createRequest<ResponseData = any, State = Record<string, unknown
|
|||||||
* @param axiosConfig axios config
|
* @param axiosConfig axios config
|
||||||
* @param options request options
|
* @param options request options
|
||||||
*/
|
*/
|
||||||
export function createFlatRequest<ResponseData = any, State = Record<string, unknown>>(
|
export function createFlatRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||||
axiosConfig?: CreateAxiosDefaults,
|
axiosConfig?: CreateAxiosDefaults,
|
||||||
options?: Partial<RequestOption<ResponseData>>
|
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||||
) {
|
) {
|
||||||
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
|
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(
|
||||||
|
axiosConfig,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
const flatRequest: FlatRequestInstance<State, ResponseData> = async function flatRequest<
|
const flatRequest: FlatRequestInstance<ResponseData, ApiData, State> = async function flatRequest<
|
||||||
T = any,
|
T extends ApiData = ApiData,
|
||||||
R extends ResponseType = 'json'
|
R extends ResponseType = 'json'
|
||||||
>(config: CustomAxiosRequestConfig) {
|
>(config: CustomAxiosRequestConfig) {
|
||||||
try {
|
try {
|
||||||
@ -160,20 +168,22 @@ export function createFlatRequest<ResponseData = any, State = Record<string, unk
|
|||||||
const responseType = response.config?.responseType || 'json';
|
const responseType = response.config?.responseType || 'json';
|
||||||
|
|
||||||
if (responseType === 'json') {
|
if (responseType === 'json') {
|
||||||
const data = opts.transformBackendResponse(response);
|
const data = await opts.transformBackendResponse(response);
|
||||||
|
|
||||||
return { data, error: null, response };
|
return { data, error: null, response };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data: response.data as MappedType<R, T>, error: null };
|
return { data: response.data as MappedType<R, T>, error: null, response };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { data: null, error, response: (error as AxiosError<ResponseData>).response };
|
return { data: null, error, response: (error as AxiosError<ResponseData>).response };
|
||||||
}
|
}
|
||||||
} as FlatRequestInstance<State, ResponseData>;
|
} as FlatRequestInstance<ResponseData, ApiData, State>;
|
||||||
|
|
||||||
flatRequest.cancelRequest = cancelRequest;
|
flatRequest.cancelRequest = cancelRequest;
|
||||||
flatRequest.cancelAllRequest = cancelAllRequest;
|
flatRequest.cancelAllRequest = cancelAllRequest;
|
||||||
flatRequest.state = {} as State;
|
flatRequest.state = {
|
||||||
|
...opts.defaultState
|
||||||
|
} as State;
|
||||||
|
|
||||||
return flatRequest;
|
return flatRequest;
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,16 @@ import { stringify } from 'qs';
|
|||||||
import { isHttpSuccess } from './shared';
|
import { isHttpSuccess } from './shared';
|
||||||
import type { RequestOption } from './type';
|
import type { RequestOption } from './type';
|
||||||
|
|
||||||
export function createDefaultOptions<ResponseData = any>(options?: Partial<RequestOption<ResponseData>>) {
|
export function createDefaultOptions<
|
||||||
const opts: RequestOption<ResponseData> = {
|
ResponseData,
|
||||||
|
ApiData = ResponseData,
|
||||||
|
State extends Record<string, unknown> = Record<string, unknown>
|
||||||
|
>(options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
|
||||||
|
const opts: RequestOption<ResponseData, ApiData, State> = {
|
||||||
onRequest: async config => config,
|
onRequest: async config => config,
|
||||||
isBackendSuccess: _response => true,
|
isBackendSuccess: _response => true,
|
||||||
onBackendFail: async () => {},
|
onBackendFail: async () => {},
|
||||||
transformBackendResponse: async response => response.data,
|
transformBackendResponse: async response => response.data as unknown as ApiData,
|
||||||
onError: async () => {}
|
onError: async () => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,7 +8,23 @@ export type ContentType =
|
|||||||
| 'application/x-www-form-urlencoded'
|
| 'application/x-www-form-urlencoded'
|
||||||
| 'application/octet-stream';
|
| 'application/octet-stream';
|
||||||
|
|
||||||
export interface RequestOption<ResponseData = any> {
|
export type ResponseTransform<Input = any, Output = any> = (input: Input) => Output | Promise<Output>;
|
||||||
|
|
||||||
|
export interface RequestOption<
|
||||||
|
ResponseData,
|
||||||
|
ApiData = ResponseData,
|
||||||
|
State extends Record<string, unknown> = Record<string, unknown>
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* The default state
|
||||||
|
*/
|
||||||
|
defaultState?: State;
|
||||||
|
/**
|
||||||
|
* transform the response data to the api data
|
||||||
|
*
|
||||||
|
* @param response Axios response
|
||||||
|
*/
|
||||||
|
transformBackendResponse: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
|
||||||
/**
|
/**
|
||||||
* The hook before request
|
* The hook before request
|
||||||
*
|
*
|
||||||
@ -35,12 +51,6 @@ export interface RequestOption<ResponseData = any> {
|
|||||||
response: AxiosResponse<ResponseData>,
|
response: AxiosResponse<ResponseData>,
|
||||||
instance: AxiosInstance
|
instance: AxiosInstance
|
||||||
) => Promise<AxiosResponse | null> | Promise<void>;
|
) => Promise<AxiosResponse | null> | Promise<void>;
|
||||||
/**
|
|
||||||
* transform backend response when the responseType is json
|
|
||||||
*
|
|
||||||
* @param response Axios response
|
|
||||||
*/
|
|
||||||
transformBackendResponse(response: AxiosResponse<ResponseData>): any | Promise<any>;
|
|
||||||
/**
|
/**
|
||||||
* The hook to handle error
|
* The hook to handle error
|
||||||
*
|
*
|
||||||
@ -68,7 +78,7 @@ export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<Axi
|
|||||||
responseType?: R;
|
responseType?: R;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface RequestInstanceCommon<T> {
|
export interface RequestInstanceCommon<State extends Record<string, unknown>> {
|
||||||
/**
|
/**
|
||||||
* cancel the request by request id
|
* cancel the request by request id
|
||||||
*
|
*
|
||||||
@ -84,32 +94,35 @@ export interface RequestInstanceCommon<T> {
|
|||||||
*/
|
*/
|
||||||
cancelAllRequest: () => void;
|
cancelAllRequest: () => void;
|
||||||
/** you can set custom state in the request instance */
|
/** you can set custom state in the request instance */
|
||||||
state: T;
|
state: State;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The request instance */
|
/** The request instance */
|
||||||
export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
|
export interface RequestInstance<ApiData, State extends Record<string, unknown>> extends RequestInstanceCommon<State> {
|
||||||
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
|
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||||
|
config: CustomAxiosRequestConfig<R>
|
||||||
|
): Promise<MappedType<R, T>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FlatResponseSuccessData<T = any, ResponseData = any> = {
|
export type FlatResponseSuccessData<ResponseData, ApiData> = {
|
||||||
data: T;
|
data: ApiData;
|
||||||
error: null;
|
error: null;
|
||||||
response: AxiosResponse<ResponseData>;
|
response: AxiosResponse<ResponseData>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FlatResponseFailData<ResponseData = any> = {
|
export type FlatResponseFailData<ResponseData> = {
|
||||||
data: null;
|
data: null;
|
||||||
error: AxiosError<ResponseData>;
|
error: AxiosError<ResponseData>;
|
||||||
response: AxiosResponse<ResponseData>;
|
response: AxiosResponse<ResponseData>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FlatResponseData<T = any, ResponseData = any> =
|
export type FlatResponseData<ResponseData, ApiData> =
|
||||||
| FlatResponseSuccessData<T, ResponseData>
|
| FlatResponseSuccessData<ResponseData, ApiData>
|
||||||
| FlatResponseFailData<ResponseData>;
|
| FlatResponseFailData<ResponseData>;
|
||||||
|
|
||||||
export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> {
|
export interface FlatRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
|
||||||
<T = any, R extends ResponseType = 'json'>(
|
extends RequestInstanceCommon<State> {
|
||||||
|
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||||
config: CustomAxiosRequestConfig<R>
|
config: CustomAxiosRequestConfig<R>
|
||||||
): Promise<FlatResponseData<MappedType<R, T>, ResponseData>>;
|
): Promise<FlatResponseData<ResponseData, MappedType<R, T>>>;
|
||||||
}
|
}
|
||||||
|
@ -6,31 +6,31 @@ import type {
|
|||||||
CreateAxiosDefaults,
|
CreateAxiosDefaults,
|
||||||
CustomAxiosRequestConfig,
|
CustomAxiosRequestConfig,
|
||||||
MappedType,
|
MappedType,
|
||||||
|
RequestInstanceCommon,
|
||||||
RequestOption,
|
RequestOption,
|
||||||
ResponseType
|
ResponseType
|
||||||
} from '@sa/axios';
|
} from '@sa/axios';
|
||||||
import useLoading from './use-loading';
|
import useLoading from './use-loading';
|
||||||
|
|
||||||
export type HookRequestInstanceResponseSuccessData<T = any> = {
|
export type HookRequestInstanceResponseSuccessData<ApiData> = {
|
||||||
data: Ref<T>;
|
data: Ref<ApiData>;
|
||||||
error: Ref<null>;
|
error: Ref<null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HookRequestInstanceResponseFailData<ResponseData = any> = {
|
export type HookRequestInstanceResponseFailData<ResponseData> = {
|
||||||
data: Ref<null>;
|
data: Ref<null>;
|
||||||
error: Ref<AxiosError<ResponseData>>;
|
error: Ref<AxiosError<ResponseData>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HookRequestInstanceResponseData<T = any, ResponseData = any> = {
|
export type HookRequestInstanceResponseData<ResponseData, ApiData> = {
|
||||||
loading: Ref<boolean>;
|
loading: Ref<boolean>;
|
||||||
} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>);
|
} & (HookRequestInstanceResponseSuccessData<ApiData> | HookRequestInstanceResponseFailData<ResponseData>);
|
||||||
|
|
||||||
export interface HookRequestInstance<ResponseData = any> {
|
export interface HookRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
|
||||||
<T = any, R extends ResponseType = 'json'>(
|
extends RequestInstanceCommon<State> {
|
||||||
|
<T extends ApiData = ApiData, R extends ResponseType = 'json'>(
|
||||||
config: CustomAxiosRequestConfig
|
config: CustomAxiosRequestConfig
|
||||||
): HookRequestInstanceResponseData<MappedType<R, T>, ResponseData>;
|
): HookRequestInstanceResponseData<ResponseData, MappedType<R, T>>;
|
||||||
cancelRequest: (requestId: string) => void;
|
|
||||||
cancelAllRequest: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,25 +39,26 @@ export interface HookRequestInstance<ResponseData = any> {
|
|||||||
* @param axiosConfig
|
* @param axiosConfig
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
export default function createHookRequest<ResponseData = any>(
|
export default function createHookRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
|
||||||
axiosConfig?: CreateAxiosDefaults,
|
axiosConfig?: CreateAxiosDefaults,
|
||||||
options?: Partial<RequestOption<ResponseData>>
|
options?: Partial<RequestOption<ResponseData, ApiData, State>>
|
||||||
) {
|
) {
|
||||||
const request = createFlatRequest<ResponseData>(axiosConfig, options);
|
const request = createFlatRequest<ResponseData, ApiData, State>(axiosConfig, options);
|
||||||
|
|
||||||
const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>(
|
const hookRequest: HookRequestInstance<ResponseData, ApiData, State> = function hookRequest<
|
||||||
config: CustomAxiosRequestConfig
|
T extends ApiData = ApiData,
|
||||||
) {
|
R extends ResponseType = 'json'
|
||||||
|
>(config: CustomAxiosRequestConfig) {
|
||||||
const { loading, startLoading, endLoading } = useLoading();
|
const { loading, startLoading, endLoading } = useLoading();
|
||||||
|
|
||||||
const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>;
|
const data = ref(null) as Ref<MappedType<R, T>>;
|
||||||
const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>;
|
const error = ref(null) as Ref<AxiosError<ResponseData> | null>;
|
||||||
|
|
||||||
startLoading();
|
startLoading();
|
||||||
|
|
||||||
request(config).then(res => {
|
request(config).then(res => {
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
data.value = res.data;
|
data.value = res.data as MappedType<R, T>;
|
||||||
} else {
|
} else {
|
||||||
error.value = res.error;
|
error.value = res.error;
|
||||||
}
|
}
|
||||||
@ -70,7 +71,7 @@ export default function createHookRequest<ResponseData = any>(
|
|||||||
data,
|
data,
|
||||||
error
|
error
|
||||||
};
|
};
|
||||||
} as HookRequestInstance<ResponseData>;
|
} as HookRequestInstance<ResponseData, ApiData, State>;
|
||||||
|
|
||||||
hookRequest.cancelRequest = request.cancelRequest;
|
hookRequest.cancelRequest = request.cancelRequest;
|
||||||
hookRequest.cancelAllRequest = request.cancelAllRequest;
|
hookRequest.cancelAllRequest = request.cancelAllRequest;
|
||||||
|
@ -10,7 +10,7 @@ import type { RequestInstanceState } from './type';
|
|||||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||||
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
||||||
|
|
||||||
export const request = createFlatRequest<App.Service.Response, RequestInstanceState>(
|
export const request = createFlatRequest(
|
||||||
{
|
{
|
||||||
baseURL,
|
baseURL,
|
||||||
headers: {
|
headers: {
|
||||||
@ -18,6 +18,13 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
defaultState: {
|
||||||
|
errMsgStack: [],
|
||||||
|
refreshTokenPromise: null
|
||||||
|
} as RequestInstanceState,
|
||||||
|
transformBackendResponse(response: AxiosResponse<App.Service.Response<any>>) {
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
async onRequest(config) {
|
async onRequest(config) {
|
||||||
const Authorization = getAuthorization();
|
const Authorization = getAuthorization();
|
||||||
Object.assign(config.headers, { Authorization });
|
Object.assign(config.headers, { Authorization });
|
||||||
@ -91,9 +98,6 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
transformBackendResponse(response) {
|
|
||||||
return response.data.data;
|
|
||||||
},
|
|
||||||
onError(error) {
|
onError(error) {
|
||||||
// when the request is fail, you can show error message
|
// when the request is fail, you can show error message
|
||||||
|
|
||||||
@ -123,11 +127,14 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const demoRequest = createRequest<App.Service.DemoResponse>(
|
export const demoRequest = createRequest(
|
||||||
{
|
{
|
||||||
baseURL: otherBaseURL.demo
|
baseURL: otherBaseURL.demo
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
transformBackendResponse(response: AxiosResponse<App.Service.DemoResponse>) {
|
||||||
|
return response.data.result;
|
||||||
|
},
|
||||||
async onRequest(config) {
|
async onRequest(config) {
|
||||||
const { headers } = config;
|
const { headers } = config;
|
||||||
|
|
||||||
@ -147,9 +154,6 @@ export const demoRequest = createRequest<App.Service.DemoResponse>(
|
|||||||
// when the backend response code is not "200", it means the request is fail
|
// when the backend response code is not "200", it means the request is fail
|
||||||
// for example: the token is expired, refresh token and retry request
|
// for example: the token is expired, refresh token and retry request
|
||||||
},
|
},
|
||||||
transformBackendResponse(response) {
|
|
||||||
return response.data.result;
|
|
||||||
},
|
|
||||||
onError(error) {
|
onError(error) {
|
||||||
// when the request is fail, you can show error message
|
// when the request is fail, you can show error message
|
||||||
|
|
||||||
|
@ -28,14 +28,14 @@ async function handleRefreshToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function handleExpiredRequest(state: RequestInstanceState) {
|
export async function handleExpiredRequest(state: RequestInstanceState) {
|
||||||
if (!state.refreshTokenFn) {
|
if (!state.refreshTokenPromise) {
|
||||||
state.refreshTokenFn = handleRefreshToken();
|
state.refreshTokenPromise = handleRefreshToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = await state.refreshTokenFn;
|
const success = await state.refreshTokenPromise;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
state.refreshTokenFn = null;
|
state.refreshTokenPromise = null;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export interface RequestInstanceState {
|
export interface RequestInstanceState {
|
||||||
/** whether the request is refreshing token */
|
/** the promise of refreshing token */
|
||||||
refreshTokenFn: Promise<boolean> | null;
|
refreshTokenPromise: Promise<boolean> | null;
|
||||||
/** the request error message stack */
|
/** the request error message stack */
|
||||||
errMsgStack: string[];
|
errMsgStack: string[];
|
||||||
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user