refactor: 重建响应数据类型

pull/41/head
zjx0905 2023-04-09 15:20:10 +08:00
parent ee6a31b4bb
commit 4c75fa3d32
33 changed files with 544 additions and 305 deletions

View File

@ -34,6 +34,7 @@
"cz": "czg", "cz": "czg",
"build": "esno scripts/build.ts", "build": "esno scripts/build.ts",
"build:asset": "esno scripts/build-asset.ts", "build:asset": "esno scripts/build-asset.ts",
"watch": "pnpm build -a -w",
"release": "esno scripts/release.ts", "release": "esno scripts/release.ts",
"publish:ci": "esno scripts/publish.ts", "publish:ci": "esno scripts/publish.ts",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",

View File

@ -28,7 +28,7 @@ export function noop() {
return; return;
} }
export function mockResponseBase( export function mockResponse(
status: number, status: number,
statusText: string, statusText: string,
headers: AnyObject, headers: AnyObject,
@ -42,17 +42,6 @@ export function mockResponseBase(
}; };
} }
export function mockResponse(headers: AnyObject = {}, data: AnyObject = {}) {
return mockResponseBase(200, 'OK', headers, data);
}
export function mockResponseError(
headers: AnyObject = {},
data: AnyObject = {},
) {
return mockResponseBase(400, 'FAIL', headers, data);
}
export interface MockAdapterOptions { export interface MockAdapterOptions {
headers?: AnyObject; headers?: AnyObject;
data?: AnyObject; data?: AnyObject;
@ -68,21 +57,33 @@ export function mockAdapterBase(
const { headers = {}, data = {}, delay = 0, before, after } = options; const { headers = {}, data = {}, delay = 0, before, after } = options;
return (config: AxiosAdapterRequestConfig) => { return (config: AxiosAdapterRequestConfig) => {
let canceled = false;
before?.(config); before?.(config);
setTimeout(() => { setTimeout(() => {
if (!canceled) {
switch (type) { switch (type) {
case 'success': case 'success':
config.success(mockResponse(headers, data)); config.success(mockResponse(200, 'OK', headers, data));
break; break;
case 'error': case 'error':
config.success(mockResponseError(headers, data)); config.success(mockResponse(500, 'ERROR', headers, data));
break; break;
case 'fail': case 'fail':
config.fail(mockResponseError(headers)); config.fail(mockResponse(400, 'FAIL', headers, data));
break; break;
} }
after?.(); after?.();
}
}, delay); }, delay);
return {
abort() {
canceled = true;
},
};
}; };
} }

View File

@ -9,7 +9,6 @@ import {
AxiosProgressCallback, AxiosProgressCallback,
AxiosRequestFormData, AxiosRequestFormData,
AxiosRequestHeaders, AxiosRequestHeaders,
AxiosResponse,
} from './core/Axios'; } from './core/Axios';
export type AxiosAdapterRequestType = 'request' | 'download' | 'upload'; export type AxiosAdapterRequestType = 'request' | 'download' | 'upload';
@ -24,7 +23,7 @@ export type AxiosAdapterRequestMethod =
| 'TRACE' | 'TRACE'
| 'CONNECT'; | 'CONNECT';
export interface AxiosAdapterResponse<TData = unknown> extends AnyObject { export interface AxiosAdapterResponse extends AnyObject {
/** /**
* *
*/ */
@ -40,7 +39,7 @@ export interface AxiosAdapterResponse<TData = unknown> extends AnyObject {
/** /**
* *
*/ */
data: TData; data: string | ArrayBuffer | AnyObject;
} }
export interface AxiosAdapterResponseError extends AnyObject { export interface AxiosAdapterResponseError extends AnyObject {
@ -107,8 +106,8 @@ export interface AxiosAdapterRequestConfig extends AnyObject {
export interface AxiosAdapterBaseOptions extends AxiosAdapterRequestConfig { export interface AxiosAdapterBaseOptions extends AxiosAdapterRequestConfig {
header?: AxiosRequestHeaders; header?: AxiosRequestHeaders;
success(response: unknown): void; success(response: AxiosAdapterResponse): void;
fail(error: unknown): void; fail(error: AxiosAdapterResponseError): void;
} }
export interface AxiosAdapterUploadOptions export interface AxiosAdapterUploadOptions
@ -239,10 +238,10 @@ export function createAdapter(platform: AxiosPlatform): AxiosAdapter {
download: AxiosAdapterDownload, download: AxiosAdapterDownload,
baseOptions: AxiosAdapterBaseOptions, baseOptions: AxiosAdapterBaseOptions,
): AxiosAdapterTask { ): AxiosAdapterTask {
const options = { const options: AxiosAdapterDownloadOptions = {
...baseOptions, ...baseOptions,
filePath: baseOptions.params?.filePath, filePath: baseOptions.params?.filePath,
success(response: AnyObject): void { success(response): void {
injectDownloadData(response); injectDownloadData(response);
baseOptions.success(response); baseOptions.success(response);
}, },
@ -275,7 +274,7 @@ export function createAdapter(platform: AxiosPlatform): AxiosAdapter {
return { return {
...config, ...config,
header: config.headers, header: config.headers,
success(response: AxiosResponse<unknown>): void { success(response): void {
transformResult(response); transformResult(response);
config.success(response); config.success(response);
}, },

View File

@ -61,7 +61,7 @@ function createInstance(defaults: AxiosRequestConfig) {
Object.assign(instance, context, { Object.assign(instance, context, {
// instance.fork 内部调用了 context 的私有方法 // instance.fork 内部调用了 context 的私有方法
// 所以直接调用 instance.fork 会导致程序抛出无法访问私有方法的异常 // 所以直接调用 instance.fork 会导致程序抛出无法访问私有方法的异常
// instance.fork 调用时 this 重新指向 context解决此问题 // instance.fork 调用时 this 重新指向 context解决此问题
fork: context.fork.bind(context), fork: context.fork.bind(context),
}); });

View File

@ -11,7 +11,7 @@ import {
AxiosAdapterResponseError, AxiosAdapterResponseError,
} from '../adapter'; } from '../adapter';
import { CancelToken } from './cancel'; import { CancelToken } from './cancel';
import dispatchRequest from './dispatchRequest'; import { dispatchRequest } from './dispatchRequest';
import InterceptorManager from './InterceptorManager'; import InterceptorManager from './InterceptorManager';
import { AxiosTransformer } from './transformData'; import { AxiosTransformer } from './transformData';
import AxiosDomain from './AxiosDomain'; import AxiosDomain from './AxiosDomain';
@ -79,6 +79,13 @@ export interface AxiosRequestFormData extends AnyObject {
export type AxiosRequestData = AnyObject | AxiosRequestFormData; export type AxiosRequestData = AnyObject | AxiosRequestFormData;
export type AxiosResponseData =
| undefined
| number
| string
| ArrayBuffer
| AnyObject;
export interface AxiosProgressEvent { export interface AxiosProgressEvent {
progress: number; progress: number;
totalBytesSent: number; totalBytesSent: number;
@ -90,9 +97,8 @@ export interface AxiosProgressCallback {
} }
export interface AxiosRequestConfig export interface AxiosRequestConfig
extends Omit< extends Partial<
Partial<AxiosAdapterRequestConfig>, Omit<AxiosAdapterRequestConfig, 'type' | 'success' | 'fail'>
'type' | 'method' | 'success' | 'fail'
> { > {
/** /**
* *
@ -102,6 +108,10 @@ export interface AxiosRequestConfig
* *
*/ */
baseURL?: string; baseURL?: string;
/**
* URL
*/
url?: string;
/** /**
* *
*/ */
@ -133,11 +143,11 @@ export interface AxiosRequestConfig
/** /**
* *
*/ */
transformRequest?: AxiosTransformer | AxiosTransformer[]; transformRequest?: AxiosTransformer<AxiosRequestData>;
/** /**
* *
*/ */
transformResponse?: AxiosTransformer | AxiosTransformer[]; transformResponse?: AxiosTransformer<AxiosResponseData>;
/** /**
* *
*/ */
@ -160,8 +170,9 @@ export interface AxiosRequestConfig
validateStatus?: (status: number) => boolean; validateStatus?: (status: number) => boolean;
} }
export interface AxiosResponse<TData = unknown> export interface AxiosResponse<
extends AxiosAdapterResponse<TData> { TData extends AxiosResponseData = AxiosResponseData,
> extends Omit<AxiosAdapterResponse, 'data'> {
/** /**
* *
*/ */
@ -170,6 +181,10 @@ export interface AxiosResponse<TData = unknown>
* *
*/ */
request?: AxiosAdapterTask; request?: AxiosAdapterTask;
/**
*
*/
data: TData;
} }
export interface AxiosResponseError extends AxiosAdapterResponseError { export interface AxiosResponseError extends AxiosAdapterResponseError {
@ -233,22 +248,17 @@ export default class Axios extends AxiosDomain {
); );
} }
#processRequest<TData = unknown>(config: AxiosRequestConfig) { #processRequest(config: AxiosRequestConfig) {
const { request, response } = this.interceptors; const { request, response } = this.interceptors;
let promiseRequest = Promise.resolve(config); let promiseRequest = Promise.resolve(config);
request.forEach(({ resolved, rejected }) => { request.forEach(({ resolved, rejected }) => {
promiseRequest = promiseRequest.then( promiseRequest = promiseRequest.then(resolved, rejected);
resolved,
rejected,
) as Promise<AxiosRequestConfig>;
}, true); }, true);
let promiseResponse = promiseRequest.then(dispatchRequest); let promiseResponse = promiseRequest.then(dispatchRequest);
response.forEach(({ resolved, rejected }) => { response.forEach(({ resolved, rejected }) => {
promiseResponse = promiseResponse.then(resolved, rejected) as Promise< promiseResponse = promiseResponse.then(resolved, rejected);
AxiosResponse<unknown>
>;
}); });
return promiseResponse as Promise<AxiosResponse<TData>>; return promiseResponse;
} }
} }

View File

@ -1,16 +1,21 @@
import { isString, isUndefined } from '../helpers/isTypes'; import { isString, isUndefined } from '../helpers/isTypes';
import { deepMerge } from '../helpers/deepMerge'; import { deepMerge } from '../helpers/deepMerge';
import { mergeConfig } from './mergeConfig'; import { mergeConfig } from './mergeConfig';
import { AxiosRequestConfig, AxiosRequestData, AxiosResponse } from './Axios'; import {
AxiosRequestConfig,
AxiosRequestData,
AxiosResponse,
AxiosResponseData,
} from './Axios';
export interface AxiosDomainRequest { export interface AxiosDomainRequest {
<TData = unknown>( <TData extends AxiosResponseData>(
/** /**
* *
*/ */
config: AxiosRequestConfig, config: AxiosRequestConfig,
): Promise<AxiosResponse<TData>>; ): Promise<AxiosResponse<TData>>;
<TData = unknown>( <TData extends AxiosResponseData>(
/** /**
* *
*/ */
@ -23,7 +28,7 @@ export interface AxiosDomainRequest {
} }
export interface AxiosDomainAsRequest { export interface AxiosDomainAsRequest {
<TData = unknown>( <TData extends AxiosResponseData>(
/** /**
* *
*/ */
@ -36,7 +41,7 @@ export interface AxiosDomainAsRequest {
} }
export interface AxiosDomainAsRequestWithParams { export interface AxiosDomainAsRequestWithParams {
<TData = unknown>( <TData extends AxiosResponseData>(
/** /**
* *
*/ */
@ -53,7 +58,7 @@ export interface AxiosDomainAsRequestWithParams {
} }
export interface AxiosDomainAsRequestWithData { export interface AxiosDomainAsRequestWithData {
<TData = unknown>( <TData extends AxiosResponseData>(
/** /**
* *
*/ */

View File

@ -2,17 +2,17 @@ export interface InterceptorResolved<T = unknown> {
(value: T): T | Promise<T>; (value: T): T | Promise<T>;
} }
export interface InterceptorRejected { export interface InterceptorRejected<T = unknown> {
(error: unknown): unknown | Promise<unknown>; (error: unknown): T | Promise<T>;
} }
export interface Interceptor<T = unknown> { export interface Interceptor<T = unknown> {
resolved: InterceptorResolved<T>; resolved: InterceptorResolved<T>;
rejected?: InterceptorRejected; rejected?: InterceptorRejected<T>;
} }
export interface InterceptorExecutor { export interface InterceptorExecutor<T = unknown> {
(interceptor: Interceptor): void; (interceptor: Interceptor<T>): void;
} }
export default class InterceptorManager<T = unknown> { export default class InterceptorManager<T = unknown> {
@ -22,7 +22,7 @@ export default class InterceptorManager<T = unknown> {
use( use(
resolved: InterceptorResolved<T>, resolved: InterceptorResolved<T>,
rejected?: InterceptorRejected, rejected?: InterceptorRejected<T>,
): number { ): number {
this.#interceptors[++this.#id] = { this.#interceptors[++this.#id] = {
resolved, resolved,
@ -36,8 +36,8 @@ export default class InterceptorManager<T = unknown> {
delete this.#interceptors[id]; delete this.#interceptors[id];
} }
forEach(executor: InterceptorExecutor, reverse?: boolean): void { forEach(executor: InterceptorExecutor<T>, reverse?: boolean): void {
let interceptors: Interceptor<any>[] = Object.values(this.#interceptors); let interceptors: Interceptor<T>[] = Object.values(this.#interceptors);
if (reverse) interceptors = interceptors.reverse(); if (reverse) interceptors = interceptors.reverse();
interceptors.forEach(executor); interceptors.forEach(executor);
} }

View File

@ -30,7 +30,7 @@ export function createError(
config: AxiosRequestConfig, config: AxiosRequestConfig,
response: AxiosErrorResponse, response: AxiosErrorResponse,
request: AxiosAdapterTask, request: AxiosAdapterTask,
): AxiosError { ) {
const axiosError = new AxiosError(message, config, response, request); const axiosError = new AxiosError(message, config, response, request);
cleanStack(axiosError); cleanStack(axiosError);
return axiosError; return axiosError;

View File

@ -14,29 +14,30 @@ function throwIfCancellationRequested(config: AxiosRequestConfig) {
} }
} }
export default function dispatchRequest<TData = unknown>( export function dispatchRequest(config: AxiosRequestConfig) {
config: AxiosRequestConfig,
): Promise<AxiosResponse> {
throwIfCancellationRequested(config); throwIfCancellationRequested(config);
const { transformRequest, transformResponse } = config; const { transformRequest, transformResponse } = config;
config.url = transformURL(config); config.url = transformURL(config);
config.method = config.method ?? 'get'; config.method = config.method ?? 'get';
config.headers = flattenHeaders(config); config.headers = flattenHeaders(config);
transform(config, transformRequest); transformer(config, transformRequest);
function onSuccess(response: AxiosResponse<TData>) { function onSuccess(response: AxiosResponse) {
throwIfCancellationRequested(config); throwIfCancellationRequested(config);
transform(response, transformResponse);
transformer(response, transformResponse);
return response; return response;
} }
function onError(reason: unknown) { function onError(reason: unknown) {
if (!isCancel(reason)) { if (!isCancel(reason)) {
throwIfCancellationRequested(config); throwIfCancellationRequested(config);
if (isAxiosError(reason)) { if (isAxiosError(reason)) {
transform(reason.response as AxiosResponse<TData>, transformResponse); transformer(reason.response as AxiosResponse, transformResponse);
} }
} }
@ -47,16 +48,16 @@ export default function dispatchRequest<TData = unknown>(
return Promise.reject(reason); return Promise.reject(reason);
} }
function transform<TData = unknown>( function transformer<TData = unknown>(
target: AxiosRequestConfig | AxiosResponse<TData>, targetObject: { data?: TData; headers?: AnyObject },
transformer?: AxiosTransformer | AxiosTransformer[], transformer?: AxiosTransformer<TData>,
) { ) {
target.data = transformData( targetObject.data = transformData(
target.data as AnyObject, targetObject.data,
target.headers, targetObject.headers,
transformer, transformer,
) as TData; );
} }
return request<TData>(config).then(onSuccess).catch(onError); return request(config).then(onSuccess).catch(onError);
} }

View File

@ -4,9 +4,7 @@ import { AxiosRequestConfig } from './Axios';
const postRE = /^POST$/i; const postRE = /^POST$/i;
const getRE = /^GET$/i; const getRE = /^GET$/i;
export function generateType( export function generateType(config: AxiosRequestConfig) {
config: AxiosRequestConfig,
): AxiosAdapterRequestType {
let requestType: AxiosAdapterRequestType = 'request'; let requestType: AxiosAdapterRequestType = 'request';
if (config.upload && postRE.test(config.method!)) { if (config.upload && postRE.test(config.method!)) {

View File

@ -17,7 +17,7 @@ const deepMergeConfigMap: Record<string, boolean> = {
export function mergeConfig( export function mergeConfig(
config1: AxiosRequestConfig = {}, config1: AxiosRequestConfig = {},
config2: AxiosRequestConfig = {}, config2: AxiosRequestConfig = {},
): AxiosRequestConfig { ) {
const config: AxiosRequestConfig = {}; const config: AxiosRequestConfig = {};
// 所有已知键名 // 所有已知键名

View File

@ -10,6 +10,7 @@ import {
AxiosProgressCallback, AxiosProgressCallback,
AxiosRequestConfig, AxiosRequestConfig,
AxiosResponse, AxiosResponse,
AxiosResponseData,
AxiosResponseError, AxiosResponseError,
} from './Axios'; } from './Axios';
import { isCancelToken } from './cancel'; import { isCancelToken } from './cancel';
@ -38,16 +39,14 @@ function tryToggleProgressUpdate(
} }
} }
export function request<TData = unknown>( export function request(config: AxiosRequestConfig) {
config: AxiosRequestConfig, return new Promise<AxiosResponse>((resolve, reject) => {
): Promise<AxiosResponse<TData>> {
return new Promise((resolve, reject) => {
assert(isFunction(config.adapter), 'adapter 不是一个 function'); assert(isFunction(config.adapter), 'adapter 不是一个 function');
assert(isString(config.url), 'url 不是一个 string'); assert(isString(config.url), 'url 不是一个 string');
const adapterConfig: AxiosAdapterRequestConfig = { const adapterConfig: AxiosAdapterRequestConfig = {
...config, ...config,
url: config.url, url: config.url!,
type: generateType(config), type: generateType(config),
method: config.method!.toUpperCase() as AxiosAdapterRequestMethod, method: config.method!.toUpperCase() as AxiosAdapterRequestMethod,
success, success,
@ -56,10 +55,11 @@ export function request<TData = unknown>(
const adapterTask = config.adapter!(adapterConfig); const adapterTask = config.adapter!(adapterConfig);
function success(_: AxiosAdapterResponse<TData>): void { function success(_: AxiosAdapterResponse): void {
const response = _ as AxiosResponse<TData>; const response = _ as AxiosResponse;
response.config = config; response.config = config;
response.request = adapterTask; response.request = adapterTask;
if (config.validateStatus?.(response.status) ?? true) { if (config.validateStatus?.(response.status) ?? true) {
resolve(response); resolve(response);
} else { } else {
@ -72,7 +72,7 @@ export function request<TData = unknown>(
responseError.isFail = true; responseError.isFail = true;
responseError.config = config; responseError.config = config;
responseError.request = adapterTask; responseError.request = adapterTask;
catchError(responseError.statusText, responseError); catchError('request fail', responseError);
} }
function catchError( function catchError(

View File

@ -1,35 +1,38 @@
import { isArray, isUndefined } from '../helpers/isTypes'; import { isArray, isFunction } from '../helpers/isTypes';
import { AxiosRequestData } from './Axios';
export interface AxiosTransformer { export interface AxiosTransformCallback<TData = unknown> {
( (
/** /**
* *
*/ */
data?: AxiosRequestData, data?: TData,
/** /**
* *
*/ */
headers?: AnyObject, headers?: AnyObject,
): AxiosRequestData; ): TData | undefined;
} }
export function transformData( export type AxiosTransformer<TData = unknown> =
data?: AxiosRequestData, | AxiosTransformCallback<TData>
| AxiosTransformCallback<TData>[];
export function transformData<TData = unknown>(
data?: TData,
headers?: AnyObject, headers?: AnyObject,
transforms?: AxiosTransformer | AxiosTransformer[], transforms?: AxiosTransformer<TData>,
): AxiosRequestData | undefined { ) {
if (isUndefined(transforms)) {
return data;
}
if (!isArray(transforms)) { if (!isArray(transforms)) {
if (isFunction(transforms)) {
transforms = [transforms]; transforms = [transforms];
} else {
transforms = [];
}
} }
transforms.forEach((transform: AxiosTransformer) => { transforms.forEach((transform) => {
data = transform(data, headers); data = transform(data, headers);
}); });
return data; return data as TData;
} }

View File

@ -4,7 +4,7 @@ import { dynamicURL } from '../helpers/dynamicURL';
import { isAbsoluteURL } from '../helpers/isAbsoluteURL'; import { isAbsoluteURL } from '../helpers/isAbsoluteURL';
import { AxiosRequestConfig } from './Axios'; import { AxiosRequestConfig } from './Axios';
export function transformURL(config: AxiosRequestConfig): string { export function transformURL(config: AxiosRequestConfig) {
let url = config.url ?? ''; let url = config.url ?? '';
if (!isAbsoluteURL(url)) url = combineURL(config.baseURL ?? '', url); if (!isAbsoluteURL(url)) url = combineURL(config.baseURL ?? '', url);

View File

@ -1,5 +1,5 @@
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest'; import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
import { getAdapterDefault } from 'src/adapter'; import { getAdapterDefault } from '@/adapter';
const platforms = [ const platforms = [
'uni', 'uni',

View File

@ -1,24 +1,29 @@
import { describe, test, expect, vi } from 'vitest'; import { describe, test, expect, vi } from 'vitest';
import { createAdapter } from 'src/adapter';
import { noop } from 'scripts/test.utils'; import { noop } from 'scripts/test.utils';
import { AxiosPlatform, createAdapter } from '@/adapter';
describe('src/adapter.ts', () => { describe('src/adapter.ts', () => {
test('应该抛出异常', () => { test('应该抛出异常', () => {
expect(() => expect(() =>
createAdapter(undefined as any), createAdapter(undefined as unknown as AxiosPlatform),
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
'"[axios-miniprogram]: platform 不是一个 object"', '"[axios-miniprogram]: platform 不是一个 object"',
); );
expect(() => createAdapter({} as any)).toThrowErrorMatchingInlineSnapshot( expect(() =>
createAdapter({} as unknown as AxiosPlatform),
).toThrowErrorMatchingInlineSnapshot(
'"[axios-miniprogram]: request 不是一个 function"', '"[axios-miniprogram]: request 不是一个 function"',
); );
expect(() => expect(() =>
createAdapter({ request: vi.fn() } as any), createAdapter({ request: vi.fn() } as unknown as AxiosPlatform),
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
'"[axios-miniprogram]: upload 不是一个 function"', '"[axios-miniprogram]: upload 不是一个 function"',
); );
expect(() => expect(() =>
createAdapter({ request: vi.fn(), upload: vi.fn() } as any), createAdapter({
request: vi.fn(),
upload: vi.fn(),
} as unknown as AxiosPlatform),
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
'"[axios-miniprogram]: download 不是一个 function"', '"[axios-miniprogram]: download 不是一个 function"',
); );

View File

@ -1,136 +1,17 @@
import { describe, test, expect, beforeAll, afterAll } from 'vitest'; import { describe, test, expect } from 'vitest';
import { mockAdapter } from 'scripts/test.utils'; import Axios from '@/core/Axios';
import Axios from '../src/core/Axios'; import { CancelToken, isCancel } from '@/core/cancel';
import { CancelToken, isCancel } from '../src/core/cancel'; import { isAxiosError } from '@/core/createError';
import { isAxiosError } from '../src/core/createError'; import { createAdapter } from '@/adapter';
import { createAdapter } from '../src/adapter'; import axios from '@/axios';
import defaults from '../src/defaults';
import axios from '../src/axios';
describe('src/axios.ts', () => { describe('src/axios.ts', () => {
const data = {
result: null,
};
beforeAll(() => {
axios.defaults.baseURL = 'http://api.com';
});
afterAll(() => {
axios.defaults.baseURL = undefined;
});
test('应该有这些静态属性', () => { test('应该有这些静态属性', () => {
expect(axios.defaults).toBe(defaults);
expect(axios.Axios).toBe(Axios); expect(axios.Axios).toBe(Axios);
expect(axios.CancelToken).toBe(CancelToken); expect(axios.CancelToken).toBe(CancelToken);
expect(axios.create).toBeTypeOf('function');
expect(axios.createAdapter).toBe(createAdapter); expect(axios.createAdapter).toBe(createAdapter);
expect(axios.isCancel).toBe(isCancel); expect(axios.isCancel).toBe(isCancel);
expect(axios.isAxiosError).toBe(isAxiosError); expect(axios.isAxiosError).toBe(isAxiosError);
expect(axios.interceptors).toBeTypeOf('object');
expect(axios.create).toBeTypeOf('function');
expect(axios.request).toBeTypeOf('function');
expect(axios.getUri).toBeTypeOf('function');
expect(axios.fork).toBeTypeOf('function');
[...Axios.as, ...Axios.asp, ...Axios.asd].forEach((k) => {
expect(axios[k]).toBeTypeOf('function');
});
});
test('应该可以发送普通别名请求', () => {
const c = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test');
},
data,
}),
};
Axios.as.forEach((a) => {
axios[a]('test', c).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以发送带参数的别名请求', () => {
const p = { id: 1 };
const c1 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test?id=1');
expect(config.params).toEqual(p);
},
data,
}),
};
const c2 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test/1?id=1');
expect(config.params).toEqual(p);
},
data,
}),
};
Axios.asp.forEach((a) => {
axios[a]('test', p, c1).then((res) => {
expect(res.data).toEqual(data);
});
axios[a]('test/:id', p, c2).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以发送带数据的别名请求', () => {
const d = { id: 1 };
const c1 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test');
expect(config.data).toEqual(d);
},
data,
}),
};
const c2 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test/1');
expect(config.data).toEqual(d);
},
data,
}),
};
Axios.asd.forEach((a) => {
axios[a]('test', d, c1).then((res) => {
expect(res.data).toEqual(data);
});
axios[a]('test/:id', d, c2).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以获取 URI', () => {
expect(
axios.getUri({
url: 'test',
}),
).toBe('test');
});
test('应该可以派生领域', () => {
const a = axios.fork({
baseURL: 'test',
});
expect(a.defaults.baseURL).toBe('http://api.com/test');
}); });
}); });

125
test/axios.instance.test.ts Normal file
View File

@ -0,0 +1,125 @@
import { describe, test, expect, beforeAll, afterAll } from 'vitest';
import { mockAdapter } from 'scripts/test.utils';
import Axios from '@/core/Axios';
import defaults from '@/defaults';
import axios from '@/axios';
describe('src/axios.ts', () => {
const data = {
result: null,
};
beforeAll(() => {
axios.defaults.baseURL = 'http://api.com';
});
afterAll(() => {
axios.defaults.baseURL = undefined;
});
test('应该有这些实例属性及方法', () => {
expect(axios.defaults).toBe(defaults);
expect(axios.interceptors).toBeTypeOf('object');
expect(axios.getUri).toBeTypeOf('function');
expect(axios.fork).toBeTypeOf('function');
expect(axios.request).toBeTypeOf('function');
[...Axios.as, ...Axios.asp, ...Axios.asd].forEach((k) => {
expect(axios[k]).toBeTypeOf('function');
});
});
test('应该可以发送普通别名请求', () => {
const c = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test');
},
data,
}),
};
Axios.as.forEach((a) => {
axios[a]('test', c).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以发送带参数的别名请求', () => {
const p = { id: 1 };
const c1 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test?id=1');
expect(config.params).toEqual(p);
},
data,
}),
};
const c2 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test/1?id=1');
expect(config.params).toEqual(p);
},
data,
}),
};
Axios.asp.forEach((a) => {
axios[a]('test', p, c1).then((res) => {
expect(res.data).toEqual(data);
});
axios[a]('test/:id', p, c2).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以发送带数据的别名请求', () => {
const d = { id: 1 };
const c1 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test');
expect(config.data).toEqual(d);
},
data,
}),
};
const c2 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test/1');
expect(config.data).toEqual(d);
},
data,
}),
};
Axios.asd.forEach((a) => {
axios[a]('test', d, c1).then((res) => {
expect(res.data).toEqual(data);
});
axios[a]('test/:id', d, c2).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以获取 URI', () => {
expect(
axios.getUri({
url: 'test',
}),
).toBe('test');
});
test('应该可以派生领域', () => {
const a = axios.fork({
baseURL: 'test',
});
expect(a.defaults.baseURL).toBe('http://api.com/test');
});
});

View File

@ -1,6 +1,6 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import axios from 'src/axios';
import { mockAdapter, mockAdapterError } from 'scripts/test.utils'; import { mockAdapter, mockAdapterError } from 'scripts/test.utils';
import axios from '@/axios';
describe('src/axios.ts', () => { describe('src/axios.ts', () => {
test('应该处理成功和失败', () => { test('应该处理成功和失败', () => {

View File

@ -1,27 +1,133 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import Axios from 'src/core/Axios'; import { mockAdapter } from 'scripts/test.utils';
import Axios from '@/core/Axios';
import AxiosDomain from '@/core/AxiosDomain';
describe('src/core/Axios.ts', () => { describe('src/core/Axios.ts', () => {
const data = {
result: null,
};
const axios = new Axios({
baseURL: 'http://api.com',
});
test('应该继承自 AxiosDomain', () => {
expect(new Axios() instanceof AxiosDomain).toBeTruthy();
});
test('应该有这些静态属性', () => { test('应该有这些静态属性', () => {
expect(Axios.as).toEqual(['options', 'trace', 'connect']); expect(Axios.as).toEqual(['options', 'trace', 'connect']);
expect(Axios.asp).toEqual(['head', 'get', 'delete']); expect(Axios.asp).toEqual(['head', 'get', 'delete']);
expect(Axios.asd).toEqual(['post', 'put']); expect(Axios.asd).toEqual(['post', 'put']);
}); });
test('应该有这些实例属性', () => { test('应该有这些实例属性及方法', () => {
const c = { const c = {
baseURL: 'http://api.com', baseURL: 'http://api.com',
}; };
const a = new Axios(c);
expect(a.defaults).toEqual(c); expect(axios.defaults).toEqual(c);
expect(a.interceptors).toBeTypeOf('object'); expect(axios.interceptors).toBeTypeOf('object');
expect(a.request).toBeTypeOf('function'); expect(axios.request).toBeTypeOf('function');
expect(a.getUri).toBeTypeOf('function'); expect(axios.getUri).toBeTypeOf('function');
expect(a.fork).toBeTypeOf('function'); expect(axios.fork).toBeTypeOf('function');
[...Axios.as, ...Axios.asp, ...Axios.asd].forEach((k) => { [...Axios.as, ...Axios.asp, ...Axios.asd].forEach((k) => {
expect(a[k]).toBeTypeOf('function'); expect(axios[k]).toBeTypeOf('function');
}); });
}); });
test('应该可以发送普通别名请求', () => {
const c = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test');
},
data,
}),
};
Axios.as.forEach((a) => {
axios[a]('test', c).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以发送带参数的别名请求', () => {
const p = { id: 1 };
const c1 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test?id=1');
expect(config.params).toEqual(p);
},
data,
}),
};
const c2 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test/1?id=1');
expect(config.params).toEqual(p);
},
data,
}),
};
Axios.asp.forEach((a) => {
axios[a]('test', p, c1).then((res) => {
expect(res.data).toEqual(data);
});
axios[a]('test/:id', p, c2).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以发送带数据的别名请求', () => {
const d = { id: 1 };
const c1 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test');
expect(config.data).toEqual(d);
},
data,
}),
};
const c2 = {
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test/1');
expect(config.data).toEqual(d);
},
data,
}),
};
Axios.asd.forEach((a) => {
axios[a]('test', d, c1).then((res) => {
expect(res.data).toEqual(data);
});
axios[a]('test/:id', d, c2).then((res) => {
expect(res.data).toEqual(data);
});
});
});
test('应该可以获取 URI', () => {
expect(
axios.getUri({
url: 'test',
}),
).toBe('test');
});
test('应该可以派生领域', () => {
const a = axios.fork({
baseURL: 'test',
});
expect(a.defaults.baseURL).toBe('http://api.com/test');
});
}); });

View File

@ -1,6 +1,7 @@
import { describe, test, expect, vi } from 'vitest'; import { describe, test, expect, vi } from 'vitest';
import AxiosDomain, { AxiosDomainRequest } from 'src/core/AxiosDomain'; import { ignore } from '@/helpers/ignore';
import { ignore } from 'src/helpers/ignore'; import AxiosDomain from '@/core/AxiosDomain';
import { AxiosResponse } from '@/core/Axios';
describe('src/core/AxiosDomain.ts', () => { describe('src/core/AxiosDomain.ts', () => {
test('应该有这些静态属性', () => { test('应该有这些静态属性', () => {
@ -48,6 +49,45 @@ describe('src/core/AxiosDomain.ts', () => {
}); });
test('应该可以调用这些方法', () => { test('应该可以调用这些方法', () => {
const cb = vi.fn();
const d = {
baseURL: 'http://api.com',
};
const u = 'test';
const c = {
params: {
id: 1,
},
data: {
id: 1,
},
};
const a = new AxiosDomain(d, async (config) => {
cb();
expect(config.baseURL).toBe(d.baseURL);
expect(config.url).toBe(u);
expect(config.params).toEqual(c.params);
expect(config.data).toEqual(c.data);
return {} as AxiosResponse;
});
a.request(u, c);
AxiosDomain.as.forEach((k) => a[k](u, c));
AxiosDomain.asp.forEach((k) => a[k](u, c.params, ignore(c, 'params')));
AxiosDomain.asd.forEach((k) => a[k](u, c.data, ignore(c, 'data')));
const l =
AxiosDomain.as.length +
AxiosDomain.asp.length +
AxiosDomain.asd.length +
1;
expect(cb.mock.calls.length).toBe(l);
});
test('应该可以直接传入 config 调用这些方法', () => {
const cb = vi.fn(); const cb = vi.fn();
const d = { const d = {
baseURL: 'http://api.com', baseURL: 'http://api.com',
@ -61,14 +101,16 @@ describe('src/core/AxiosDomain.ts', () => {
id: 1, id: 1,
}, },
}; };
const a = new AxiosDomain(d, ((config) => { const a = new AxiosDomain(d, async (config) => {
cb(); cb();
expect(config.baseURL).toBe(d.baseURL); expect(config.baseURL).toBe(d.baseURL);
expect(config.url).toBe(c.url); expect(config.url).toBe(c.url);
expect(config.params).toEqual(c.params); expect(config.params).toEqual(c.params);
expect(config.data).toEqual(c.data); expect(config.data).toEqual(c.data);
}) as AxiosDomainRequest);
return {} as AxiosResponse;
});
a.request(c); a.request(c);
@ -107,7 +149,7 @@ describe('src/core/AxiosDomain.ts', () => {
}, },
}; };
const a = new AxiosDomain(d, ((config) => { const a = new AxiosDomain(d, async (config) => {
expect(config.params).toEqual({ expect(config.params).toEqual({
v1: 1, v1: 1,
v2: { v2: {
@ -116,7 +158,9 @@ describe('src/core/AxiosDomain.ts', () => {
}, },
v3: 3, v3: 3,
}); });
}) as AxiosDomainRequest);
return {} as AxiosResponse;
});
AxiosDomain.asp.forEach((k) => a[k]('test', p, c)); AxiosDomain.asp.forEach((k) => a[k]('test', p, c));
}); });
@ -140,7 +184,7 @@ describe('src/core/AxiosDomain.ts', () => {
}, },
}; };
const a = new AxiosDomain(ds, ((config) => { const a = new AxiosDomain(ds, async (config) => {
expect(config.data).toEqual({ expect(config.data).toEqual({
v1: 1, v1: 1,
v2: { v2: {
@ -149,7 +193,9 @@ describe('src/core/AxiosDomain.ts', () => {
}, },
v3: 3, v3: 3,
}); });
}) as AxiosDomainRequest);
return {} as AxiosResponse;
});
AxiosDomain.asd.forEach((k) => a[k]('test', d, c)); AxiosDomain.asd.forEach((k) => a[k]('test', d, c));
}); });

View File

@ -1,5 +1,5 @@
import { describe, test, expect, vi } from 'vitest'; import { describe, test, expect, vi } from 'vitest';
import InterceptorManager from 'src/core/InterceptorManager'; import InterceptorManager from '@/core/InterceptorManager';
describe('src/core/InterceptorManager.ts', () => { describe('src/core/InterceptorManager.ts', () => {
test('应该有这些实例属性', () => { test('应该有这些实例属性', () => {

View File

@ -7,12 +7,7 @@ import {
asyncTimeout, asyncTimeout,
} from 'scripts/test.utils'; } from 'scripts/test.utils';
import axios from 'src/axios'; import axios from 'src/axios';
import { import { Cancel, isCancel, CancelToken, isCancelToken } from '@/core/cancel';
Cancel,
isCancel,
CancelToken,
isCancelToken,
} from '../../src/core/cancel';
describe('src/helpers/cancel.ts', () => { describe('src/helpers/cancel.ts', () => {
test('应该支持空参数', () => { test('应该支持空参数', () => {

View File

@ -1,6 +1,6 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { checkStack } from 'scripts/test.utils'; import { checkStack } from 'scripts/test.utils';
import { createError, isAxiosError } from 'src/core/createError'; import { createError, isAxiosError } from '@/core/createError';
describe('src/core/createError.ts', () => { describe('src/core/createError.ts', () => {
test('应该支持空参数', () => { test('应该支持空参数', () => {

View File

@ -0,0 +1,8 @@
import { describe, test, expect } from 'vitest';
import { dispatchRequest } from 'src/core/dispatchRequest';
describe('src/core/dispatchRequest.ts', () => {
test('应该有这些实例属性', () => {
expect(dispatchRequest).toBeTypeOf('function');
});
});

View File

@ -1,6 +1,6 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { flattenHeaders } from 'src/core/flattenHeaders'; import { flattenHeaders } from '@/core/flattenHeaders';
import Axios from 'src/core/Axios'; import Axios from '@/core/Axios';
describe('src/core/flattenHeaders.ts', () => { describe('src/core/flattenHeaders.ts', () => {
const keys = [...Axios.as, ...Axios.asp, ...Axios.asd]; const keys = [...Axios.as, ...Axios.asp, ...Axios.asd];

View File

@ -1,6 +1,6 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { generateType } from 'src/core/generateType'; import { generateType } from '@/core/generateType';
import Axios from 'src/core/Axios'; import Axios from '@/core/Axios';
describe('src/core/generateType.ts', () => { describe('src/core/generateType.ts', () => {
test('应该是一个 reuqest', () => { test('应该是一个 reuqest', () => {

View File

@ -1,7 +1,7 @@
import { describe, test, expect, vi } from 'vitest'; import { describe, test, expect, vi } from 'vitest';
import { ignore } from 'src/helpers/ignore'; import { ignore } from '@/helpers/ignore';
import { mergeConfig } from 'src/core/mergeConfig'; import { mergeConfig } from '@/core/mergeConfig';
import { CancelToken } from 'src/core/cancel'; import { CancelToken } from '@/core/cancel';
describe('src/core/mergeConfig.ts', () => { describe('src/core/mergeConfig.ts', () => {
test('应该支持空参数', () => { test('应该支持空参数', () => {

View File

@ -1,10 +1,10 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { request } from 'src/core/request';
import { import {
mockAdapter, mockAdapter,
mockAdapterError, mockAdapterError,
mockAdapterFail, mockAdapterFail,
} from 'scripts/test.utils'; } from 'scripts/test.utils';
import { request } from '@/core/request';
describe('src/core/request.ts', () => { describe('src/core/request.ts', () => {
test('应该抛出异常', () => { test('应该抛出异常', () => {
@ -18,14 +18,25 @@ describe('src/core/request.ts', () => {
); );
}); });
test('应该能够取到数据', async () => { test('应该正确的响应请求', async () => {
await expect( const s = request({
request({
adapter: mockAdapter(), adapter: mockAdapter(),
url: '/test', url: '/test',
method: 'get', method: 'get',
}), });
).resolves.toMatchInlineSnapshot(` const e = request({
adapter: mockAdapterError(),
url: '/test',
method: 'get',
validateStatus: () => false,
});
const f = request({
adapter: mockAdapterFail(),
url: '/test',
method: 'get',
});
await expect(s).resolves.toMatchInlineSnapshot(`
{ {
"config": { "config": {
"adapter": [Function], "adapter": [Function],
@ -34,19 +45,60 @@ describe('src/core/request.ts', () => {
}, },
"data": {}, "data": {},
"headers": {}, "headers": {},
"request": undefined, "request": {
"abort": [Function],
},
"status": 200, "status": 200,
"statusText": "OK", "statusText": "OK",
} }
`); `);
await expect(
request({ await expect(e).rejects.toMatchInlineSnapshot(
adapter: mockAdapterError(), '[Error: validate status fail]',
url: '/test', );
method: 'get', await expect(e.catch((e) => Object.assign({}, e))).resolves
}), .toMatchInlineSnapshot(`
).resolves.toMatchInlineSnapshot(`
{ {
"config": {
"adapter": [Function],
"method": "get",
"url": "/test",
"validateStatus": [Function],
},
"request": {
"abort": [Function],
},
"response": {
"config": {
"adapter": [Function],
"method": "get",
"url": "/test",
"validateStatus": [Function],
},
"data": {},
"headers": {},
"request": {
"abort": [Function],
},
"status": 500,
"statusText": "ERROR",
},
}
`);
await expect(f).rejects.toMatchInlineSnapshot('[Error: request fail]');
await expect(f.catch((e) => Object.assign({}, e))).resolves
.toMatchInlineSnapshot(`
{
"config": {
"adapter": [Function],
"method": "get",
"url": "/test",
},
"request": {
"abort": [Function],
},
"response": {
"config": { "config": {
"adapter": [Function], "adapter": [Function],
"method": "get", "method": "get",
@ -54,17 +106,14 @@ describe('src/core/request.ts', () => {
}, },
"data": {}, "data": {},
"headers": {}, "headers": {},
"request": undefined, "isFail": true,
"request": {
"abort": [Function],
},
"status": 400, "status": 400,
"statusText": "FAIL", "statusText": "FAIL",
},
} }
`); `);
await expect(
request({
adapter: mockAdapterFail(),
url: '/test',
method: 'get',
}),
).rejects.toThrowErrorMatchingInlineSnapshot('"FAIL"');
}); });
}); });

View File

@ -1,5 +1,5 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { transformData } from 'src/core/transformData'; import { transformData } from '@/core/transformData';
describe('src/core/transformData.ts', () => { describe('src/core/transformData.ts', () => {
test('应该支持空配置', () => { test('应该支持空配置', () => {

View File

@ -1,5 +1,5 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { transformURL } from 'src/core/transformURL'; import { transformURL } from '@/core/transformURL';
describe('src/core/transformURL.ts', () => { describe('src/core/transformURL.ts', () => {
test('应该支持空配置', () => { test('应该支持空配置', () => {

View File

@ -7,7 +7,10 @@
"module": "ESNext", "module": "ESNext",
"strict": true, "strict": true,
"noEmit": true, "noEmit": true,
"moduleResolution": "node" "moduleResolution": "node",
"paths": {
"@/*": ["src/*"]
}
}, },
"include": ["./src", "./test", "./global.d.ts", "./global.variables.d.ts"], "include": ["./src", "./test", "./global.d.ts", "./global.variables.d.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]

View File

@ -6,6 +6,9 @@ export default defineConfig({
test: { test: {
root: resolve(__dirname), root: resolve(__dirname),
globals: true, globals: true,
alias: {
'@': resolve(__dirname, 'src'),
},
include: ['./test/**/*.test.ts'], include: ['./test/**/*.test.ts'],
}, },
}); });