Skip to content

Commit

Permalink
feat(projects): @sa/axios: createRequest, createFlatRequest, createHo…
Browse files Browse the repository at this point in the history
…okRequest
  • Loading branch information
honghuangdc committed Jan 15, 2024
1 parent 9112193 commit c5c456b
Show file tree
Hide file tree
Showing 14 changed files with 403 additions and 119 deletions.
5 changes: 4 additions & 1 deletion packages/axios/src/constant.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
/** Request id key */
/** request id key */
export const REQUEST_ID_KEY = 'X-Request-Id';

/** the backend error code key */
export const BACKEND_ERROR_CODE = 'BACKEND_ERROR';
128 changes: 111 additions & 17 deletions packages/axios/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import axios from 'axios';
import type { CancelTokenSource, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
import axios, { AxiosError } from 'axios';
import type { AxiosResponse, CancelTokenSource, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
import axiosRetry from 'axios-retry';
import { nanoid } from '@sa/utils';
import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options';
import { REQUEST_ID_KEY } from './constant';
import type { RequestInstance, RequestOption } from './type';

export function createRequest(axiosConfig?: CreateAxiosDefaults, options?: Partial<RequestOption>) {
const opts = createDefaultOptions(options);
import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant';
import type {
CustomAxiosRequestConfig,
FlatRequestInstance,
MappedType,
RequestInstance,
RequestOption,
ResponseType
} from './type';

function createCommonRequest<ResponseData = any>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const opts = createDefaultOptions<ResponseData>(options);

const axiosConf = createAxiosConfig(axiosConfig);
const instance = axios.create(axiosConf);
Expand Down Expand Up @@ -38,9 +48,7 @@ export function createRequest(axiosConfig?: CreateAxiosDefaults, options?: Parti

instance.interceptors.response.use(
async response => {
const backendSuccess = opts.onBackendSuccess(response);

if (backendSuccess) {
if (opts.isBackendSuccess(response)) {
return Promise.resolve(response);
}

Expand All @@ -49,9 +57,21 @@ export function createRequest(axiosConfig?: CreateAxiosDefaults, options?: Parti
return fail;
}

return Promise.reject(response);
const backendError = new AxiosError<ResponseData>(
'the backend request error',
BACKEND_ERROR_CODE,
response.config,
response,
response.request
);

await opts.onError(backendError);

return Promise.reject(backendError);
},
error => {
async (error: AxiosError<ResponseData>) => {
await opts.onError(error);

return Promise.reject(error);
}
);
Expand All @@ -71,12 +91,86 @@ export function createRequest(axiosConfig?: CreateAxiosDefaults, options?: Parti
cancelTokenSourceMap.clear();
}

const requestInstance: RequestInstance = instance as RequestInstance;
return {
instance,
opts,
cancelRequest,
cancelAllRequest
};
}

/**
* create a request instance
*
* @param axiosConfig axios config
* @param options request options
*/
export function createRequest<ResponseData = any>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);

const request: RequestInstance = async function request<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
) {
const response: AxiosResponse<ResponseData> = await instance(config);

const responseType = response.config?.responseType || 'json';

if (responseType === 'json') {
return opts.transformBackendResponse(response);
}

return response.data as MappedType<R, T>;
} as RequestInstance;

request.cancelRequest = cancelRequest;
request.cancelAllRequest = cancelAllRequest;

return request;
}

/**
* create a flat request instance
*
* The response data is a flat object: { data: any, error: AxiosError }
*
* @param axiosConfig axios config
* @param options request options
*/
export function createFlatRequest<ResponseData = any>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);

const flatRequest: FlatRequestInstance = async function flatRequest<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
) {
try {
const response: AxiosResponse<ResponseData> = await instance(config);

const responseType = response.config?.responseType || 'json';

if (responseType === 'json') {
const data = opts.transformBackendResponse(response);

return { data, error: null };
}

return { data: response.data as MappedType<R, T>, error: null };
} catch (error) {
return { data: null, error };
}
} as FlatRequestInstance;

requestInstance.cancelRequest = cancelRequest;
requestInstance.cancelAllRequest = cancelAllRequest;
flatRequest.cancelRequest = cancelRequest;
flatRequest.cancelAllRequest = cancelAllRequest;

return requestInstance;
return flatRequest;
}

export default createRequest;
export { BACKEND_ERROR_CODE, REQUEST_ID_KEY };
export type * from './type';
export type { CreateAxiosDefaults, AxiosError };
7 changes: 4 additions & 3 deletions packages/axios/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import type { IAxiosRetryConfig } from 'axios-retry';
import { isHttpSuccess } from './shared';
import type { RequestOption } from './type';

export function createDefaultOptions(options?: Partial<RequestOption>) {
const opts: RequestOption = {
export function createDefaultOptions<ResponseData = any>(options?: Partial<RequestOption<ResponseData>>) {
const opts: RequestOption<ResponseData> = {
onRequest: async config => config,
onBackendSuccess: _response => true,
isBackendSuccess: _response => true,
onBackendFail: async () => {},
transformBackendResponse: async response => response.data,
onError: async () => {}
};

Expand Down
63 changes: 55 additions & 8 deletions packages/axios/src/type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';

export type ContentType =
| 'text/html'
Expand All @@ -8,21 +8,21 @@ export type ContentType =
| 'application/x-www-form-urlencoded'
| 'application/octet-stream';

export interface RequestOption {
export interface RequestOption<ResponseData = any> {
/**
* The hook before request
*
* For example: You can add header token in this hook
*
* @param config Axios config
*/
onRequest: (config: InternalAxiosRequestConfig) => Promise<InternalAxiosRequestConfig>;
onRequest: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
/**
* The hook to check backend response is success or not
*
* @param response Axios response
*/
onBackendSuccess: (response: AxiosResponse) => boolean;
isBackendSuccess: (response: AxiosResponse<ResponseData>) => boolean;
/**
* The hook after backend request fail
*
Expand All @@ -32,19 +32,66 @@ export interface RequestOption {
* @param instance Axios instance
* @returns
*/
onBackendFail: (response: AxiosResponse, instance: AxiosInstance) => Promise<AxiosResponse> | Promise<void>;
onBackendFail: (
response: AxiosResponse<ResponseData>,
instance: AxiosInstance
) => Promise<AxiosResponse> | 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
*
* For example: You can show error message in this hook
*
* @param error
*/
onError: (error: AxiosError) => Promise<void>;
onError: (error: AxiosError<ResponseData>) => void | Promise<void>;
}

/** The axios instance with cancel request function */
export type RequestInstance = AxiosInstance & {
interface ResponseMap {
blob: Blob;
text: string;
arrayBuffer: ArrayBuffer;
stream: ReadableStream<Uint8Array>;
document: Document;
}
export type ResponseType = keyof ResponseMap | 'json';

export type MappedType<R extends ResponseType, JsonType = any> = R extends keyof ResponseMap
? ResponseMap[R]
: JsonType;

export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<AxiosRequestConfig, 'responseType'> & {
responseType?: R;
};

/** The request instance */
export interface RequestInstance {
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
cancelRequest: (requestId: string) => void;
cancelAllRequest: () => void;
}

export type FlatResponseSuccessData<T = any> = {
data: T;
error: null;
};

export type FlatResponseFailData<T = any> = {
data: null;
error: AxiosError<T>;
};

export type FlatResponseData<T = any> = FlatResponseSuccessData<T> | FlatResponseFailData<T>;

export interface FlatRequestInstance {
<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig<R>
): Promise<FlatResponseData<MappedType<R, T>>>;
cancelRequest: (requestId: string) => void;
cancelAllRequest: () => void;
}
3 changes: 3 additions & 0 deletions packages/hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/axios": "workspace:*"
}
}
79 changes: 79 additions & 0 deletions packages/hooks/src/use-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ref } from 'vue';
import type { Ref } from 'vue';
import { createFlatRequest } from '@sa/axios';
import type {
AxiosError,
CreateAxiosDefaults,
CustomAxiosRequestConfig,
MappedType,
RequestOption,
ResponseType
} from '@sa/axios';
import useLoading from './use-loading';

export type HookRequestInstanceResponseSuccessData<T = any> = {
data: Ref<T>;
error: Ref<null>;
};

export type HookRequestInstanceResponseFailData<T = any> = {
data: Ref<null>;
error: Ref<AxiosError<T>>;
};

export type HookRequestInstanceResponseData<T = any> = {
loading: Ref<boolean>;
} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<T>);

export interface HookRequestInstance {
<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
): HookRequestInstanceResponseData<MappedType<R, T>>;
cancelRequest: (requestId: string) => void;
cancelAllRequest: () => void;
}

/**
* create a hook request instance
*
* @param axiosConfig
* @param options
*/
export default function createHookRequest<ResponseData = any>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const request = createFlatRequest<ResponseData>(axiosConfig, options);

const hookRequest: HookRequestInstance = function hookRequest<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
) {
const { loading, startLoading, endLoading } = useLoading();

const data = ref<MappedType<R, T> | null>(null);
const error = ref<AxiosError<MappedType<R, T>> | null>(null);

startLoading();

request(config).then(res => {
if (res.data) {
data.value = res.data;
} else {
error.value = res.error;
}

endLoading();
});

return {
loading,
data,
error
};
} as HookRequestInstance;

hookRequest.cancelRequest = request.cancelRequest;
hookRequest.cancelAllRequest = request.cancelAllRequest;

return hookRequest;
}
6 changes: 5 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c5c456b

Please sign in to comment.