feat: 添加中间件

pull/49/head
zjx0905 2023-04-23 23:05:30 +08:00
parent 7bfeba832c
commit 6263759ba9
14 changed files with 227 additions and 23 deletions

View File

@ -84,7 +84,9 @@ function createInstance(config: AxiosRequestConfig) {
const axios = createInstance(defaults) as AxiosStatic; const axios = createInstance(defaults) as AxiosStatic;
axios.create = function create(config) { axios.create = function create(config) {
return createInstance(mergeConfig(axios.defaults, config)); const instance = createInstance(mergeConfig(axios.defaults, config));
instance.flush = axios.middleware.wrap(instance.flush);
return instance;
}; };
axios.version = version; axios.version = version;

View File

@ -16,4 +16,7 @@ export const WITH_DATA_METHODS = ['post', 'put', 'patch'] as const;
/** /**
* data * data
*/ */
export const WITH_DATA_RE = new RegExp(`^${WITH_DATA_METHODS.join('|')}`, 'i'); export const WITH_DATA_RE = new RegExp(
`^(${WITH_DATA_METHODS.join('|')})`,
'i',
);

View File

@ -1,9 +1,6 @@
import { buildURL } from '../helpers/buildURL'; import { buildURL } from '../helpers/buildURL';
import { isAbsoluteURL } from '../helpers/isAbsoluteURL';
import { combineURL } from '../helpers/combineURL'; import { combineURL } from '../helpers/combineURL';
import { isString } from '../helpers/isTypes';
import { CancelToken } from '../request/cancel'; import { CancelToken } from '../request/cancel';
import { dispatchRequest } from '../request/dispatchRequest';
import { AxiosTransformer } from '../request/transformData'; import { AxiosTransformer } from '../request/transformData';
import { import {
AxiosAdapter, AxiosAdapter,
@ -14,7 +11,7 @@ import {
} from '../adpater/createAdapter'; } from '../adpater/createAdapter';
import InterceptorManager, { Interceptor } from './InterceptorManager'; import InterceptorManager, { Interceptor } from './InterceptorManager';
import { mergeConfig } from './mergeConfig'; import { mergeConfig } from './mergeConfig';
import AxiosDomain from './AxiosDomain'; import AxiosDomain, { AxiosDomainRequestHandler } from './AxiosDomain';
/** /**
* *
@ -319,7 +316,7 @@ export default class Axios extends AxiosDomain {
}; };
constructor(defaults: AxiosRequestConfig = {}) { constructor(defaults: AxiosRequestConfig = {}) {
super(defaults, (config) => this.#processRequest(config)); super(defaults, (...args) => this.#processRequest(...args));
} }
getUri(config: AxiosRequestConfig): string { getUri(config: AxiosRequestConfig): string {
@ -334,17 +331,21 @@ export default class Axios extends AxiosDomain {
* *
*/ */
fork = (config: AxiosRequestConfig = {}) => { fork = (config: AxiosRequestConfig = {}) => {
if (isString(config.baseURL) && !isAbsoluteURL(config.baseURL)) { config.baseURL = combineURL(this.defaults.baseURL, config.baseURL);
config.baseURL = combineURL(this.defaults.baseURL, config.baseURL); const domain = new AxiosDomain(
} mergeConfig(this.defaults, config),
return new AxiosDomain(mergeConfig(this.defaults, config), (config) => (...args) => this.#processRequest(...args),
this.#processRequest(config),
); );
domain.flush = this.middleware.wrap(domain.flush);
return domain;
}; };
#processRequest(config: AxiosRequestConfig) { #processRequest(
config: AxiosRequestConfig,
requestHandlerFn: AxiosDomainRequestHandler,
) {
const requestHandler = { const requestHandler = {
resolved: dispatchRequest, resolved: requestHandlerFn,
}; };
const errorHandler = { const errorHandler = {
rejected: config.errorHandler, rejected: config.errorHandler,

View File

@ -5,6 +5,8 @@ import {
} from '../constants/methods'; } from '../constants/methods';
import { isString, isUndefined } from '../helpers/isTypes'; import { isString, isUndefined } from '../helpers/isTypes';
import { deepMerge } from '../helpers/deepMerge'; import { deepMerge } from '../helpers/deepMerge';
import { combineURL } from '../helpers/combineURL';
import { dispatchRequest } from '../request/dispatchRequest';
import { mergeConfig } from './mergeConfig'; import { mergeConfig } from './mergeConfig';
import { import {
AxiosRequestConfig, AxiosRequestConfig,
@ -12,6 +14,10 @@ import {
AxiosResponse, AxiosResponse,
AxiosResponseData, AxiosResponseData,
} from './Axios'; } from './Axios';
import MiddlewareManager, {
MiddlewareContext,
MiddlewareFlush,
} from './MiddlewareManager';
/** /**
* *
@ -56,12 +62,18 @@ export type AxiosDomainRequestMethodWithData = <
config?: AxiosRequestConfig, config?: AxiosRequestConfig,
) => Promise<AxiosResponse<TData>>; ) => Promise<AxiosResponse<TData>>;
export interface AxiosDomainRequestHandler {
(config: AxiosRequestConfig): Promise<AxiosResponse>;
}
export default class AxiosDomain { export default class AxiosDomain {
/** /**
* *
*/ */
defaults: AxiosRequestConfig; defaults: AxiosRequestConfig;
middleware = new MiddlewareManager();
/** /**
* *
*/ */
@ -112,9 +124,14 @@ export default class AxiosDomain {
*/ */
connect!: AxiosDomainRequestMethod; connect!: AxiosDomainRequestMethod;
flush: MiddlewareFlush;
constructor( constructor(
defaults: AxiosRequestConfig, defaults: AxiosRequestConfig,
processRequest: (config: AxiosRequestConfig) => Promise<AxiosResponse>, processRequest: (
config: AxiosRequestConfig,
requestHandler: AxiosDomainRequestHandler,
) => Promise<AxiosResponse>,
) { ) {
this.defaults = defaults; this.defaults = defaults;
@ -132,9 +149,25 @@ export default class AxiosDomain {
config.method = 'get'; config.method = 'get';
} }
return processRequest(mergeConfig(this.defaults, config)); return processRequest(
mergeConfig(this.defaults, config),
this.#requestHandler,
);
}; };
this.flush = this.middleware.wrap(async (ctx) => {
ctx.res = await dispatchRequest(ctx.req);
});
} }
#requestHandler: AxiosDomainRequestHandler = async (config) => {
config.url = combineURL(config.baseURL, config.url);
const ctx: MiddlewareContext = {
req: config,
res: null,
};
await this.flush(ctx);
return ctx.res as AxiosResponse;
};
} }
for (const method of PLAIN_METHODS) { for (const method of PLAIN_METHODS) {

View File

@ -0,0 +1,70 @@
import { assert } from '../helpers/error';
import { combineURL } from '../helpers/combineURL';
import { isFunction } from '../helpers/isTypes';
import { AxiosRequestConfig, AxiosResponse } from './Axios';
export interface MiddlewareContext {
req: AxiosRequestConfig;
res: null | AxiosResponse;
}
export interface MiddlewareNext {
(): Promise<void>;
}
export interface MiddlewareCallback {
(ctx: MiddlewareContext, next: MiddlewareNext): Promise<void>;
}
export interface MiddlewareFlush {
(ctx: MiddlewareContext): Promise<void>;
}
export default class MiddlewareManager {
#map = new Map<string, MiddlewareCallback[]>();
use(callback: MiddlewareCallback): MiddlewareManager;
use(path: string, callback: MiddlewareCallback): MiddlewareManager;
use(path: string | MiddlewareCallback, callback?: MiddlewareCallback) {
if (isFunction(path)) {
callback = path;
path = '/';
}
assert(!!path, 'path 不是一个非空的 string');
const middlewares = this.#map.get(path) ?? [];
middlewares.push(callback!);
this.#map.set(path, middlewares);
return this;
}
wrap(flush: MiddlewareFlush): MiddlewareFlush {
return (ctx) => this.#performer(ctx, flush);
}
#performer(ctx: MiddlewareContext, flush: MiddlewareFlush) {
const middlewares = [...this.#getAllMiddlewares(ctx), flush];
function next(): Promise<void> {
return middlewares.shift()!(ctx, next);
}
return next();
}
#getAllMiddlewares(ctx: MiddlewareContext) {
const allMiddlewares: MiddlewareCallback[] = [];
for (const [path, middlewares] of this.#map.entries()) {
const url = combineURL(ctx.req.baseURL, path);
const checkRE = new RegExp(`^${url}([/?].*)?`);
if (checkRE.test(ctx.req.url!)) {
allMiddlewares.push(...middlewares);
}
}
return allMiddlewares;
}
}

View File

@ -1,5 +1,10 @@
import { isAbsoluteURL } from './isAbsoluteURL';
const combineRE = /(^|[^:])\/{2,}/g; const combineRE = /(^|[^:])\/{2,}/g;
const removeRE = /\/$/; const removeRE = /\/$/;
export function combineURL(baseURL = '', url = ''): string { export function combineURL(baseURL = '', url = ''): string {
if (isAbsoluteURL(url)) {
return url;
}
return `${baseURL}/${url}`.replace(combineRE, '$1/').replace(removeRE, ''); return `${baseURL}/${url}`.replace(combineRE, '$1/').replace(removeRE, '');
} }

View File

@ -14,6 +14,11 @@ export type {
AxiosUploadProgressEvent, AxiosUploadProgressEvent,
AxiosUploadProgressCallback, AxiosUploadProgressCallback,
} from './core/Axios'; } from './core/Axios';
export type {
MiddlewareContext,
MiddlewareCallback,
MiddlewareNext,
} from './core/MiddlewareManager';
export type { export type {
AxiosAdapter, AxiosAdapter,
AxiosAdapterRequestConfig, AxiosAdapterRequestConfig,

View File

@ -27,7 +27,6 @@ export function dispatchRequest(config: AxiosRequestConfig) {
assert(isString(config.url), 'url 不是一个 string'); assert(isString(config.url), 'url 不是一个 string');
assert(isString(config.method), 'method 不是一个 string'); assert(isString(config.method), 'method 不是一个 string');
config.url = transformURL(config);
config.method = config.method!.toUpperCase() as AxiosRequestMethod; config.method = config.method!.toUpperCase() as AxiosRequestMethod;
config.headers = flattenHeaders(config); config.headers = flattenHeaders(config);
@ -39,6 +38,8 @@ export function dispatchRequest(config: AxiosRequestConfig) {
delete config.data; delete config.data;
} }
config.url = transformURL(config);
function onSuccess(response: AxiosResponse) { function onSuccess(response: AxiosResponse) {
throwIfCancellationRequested(config); throwIfCancellationRequested(config);
dataTransformer(response, config.transformResponse); dataTransformer(response, config.transformResponse);

View File

@ -6,7 +6,6 @@ import {
} from '../core/Axios'; } from '../core/Axios';
import { import {
AxiosAdapterRequestConfig, AxiosAdapterRequestConfig,
AxiosAdapterRequestMethod,
AxiosAdapterResponse, AxiosAdapterResponse,
AxiosAdapterResponseError, AxiosAdapterResponseError,
AxiosAdapterPlatformTask, AxiosAdapterPlatformTask,

View File

@ -2,15 +2,14 @@ import { isPlainObject } from '../helpers/isTypes';
import { buildURL } from '../helpers/buildURL'; import { buildURL } from '../helpers/buildURL';
import { combineURL } from '../helpers/combineURL'; import { combineURL } from '../helpers/combineURL';
import { dynamicURL } from '../helpers/dynamicURL'; import { dynamicURL } from '../helpers/dynamicURL';
import { isAbsoluteURL } from '../helpers/isAbsoluteURL';
import { AxiosRequestConfig } from '../core/Axios'; import { AxiosRequestConfig } from '../core/Axios';
export function transformURL(config: AxiosRequestConfig) { export function transformURL(config: AxiosRequestConfig) {
let url = config.url ?? '';
if (!isAbsoluteURL(url)) url = combineURL(config.baseURL ?? '', url);
const data = isPlainObject(config.data) ? config.data : {}; const data = isPlainObject(config.data) ? config.data : {};
let url = config.url ?? '/';
url = combineURL(config.baseURL ?? '', url);
url = dynamicURL(url, config.params, data); url = dynamicURL(url, config.params, data);
url = buildURL(url, config.params, config.paramsSerializer); url = buildURL(url, config.params, config.paramsSerializer);

View File

@ -37,6 +37,7 @@ describe('src/axios.ts', () => {
}, },
}), }),
baseURL: 'http://api.com', baseURL: 'http://api.com',
method: 'post',
data: { data: {
id: 1, id: 1,
}, },

View File

@ -0,0 +1,78 @@
import { describe, test, expect, vi } from 'vitest';
import MiddlewareManager from '@/core/MiddlewareManager';
describe('src/core/MiddlewareManager.ts', () => {
test('应该有这些实例属性', () => {
const m = new MiddlewareManager();
expect(m.use).toBeTypeOf('function');
expect(m.wrap).toBeTypeOf('function');
});
test('应该可以添加中间件回调', async () => {
const m = new MiddlewareManager();
const ctx = {
req: { url: 'https://api.com' },
res: null,
};
const res = {
'src/core/MiddlewareManager.ts': true,
};
const midde = vi.fn(async (ctx, next) => {
expect(ctx).toBe(ctx);
ctx.req.url = 'test';
await next();
expect(ctx.res).toBe(res);
});
const flush = vi.fn(async (ctx) => {
expect(ctx.req.url).toBe('test');
ctx.res = res;
});
m.use(midde);
await m.wrap(flush)(ctx);
expect(ctx.res).toBe(res);
expect(midde).toBeCalled();
});
test('应该可以给路径添加中间件回调', async () => {
const m = new MiddlewareManager();
const ctx1 = {
req: {
baseURL: 'https://api.com',
url: 'https://api.com',
},
res: null,
};
const ctx2 = {
req: {
baseURL: 'https://api.com',
url: 'https://api.com/test',
},
res: null,
};
const res = {
'src/core/MiddlewareManager.ts': true,
};
const midde = vi.fn(async (ctx, next) => {
expect(ctx).toBe(ctx);
await next();
expect(ctx.res).toBe(res);
});
const flush = vi.fn(async (ctx) => {
ctx.res = res;
});
m.use('/test', midde);
await m.wrap(flush)(ctx1);
expect(ctx1.res).toBe(res);
expect(midde).not.toBeCalled();
m.use('/test', midde);
await m.wrap(flush)(ctx2);
expect(midde).toBeCalled();
});
});

View File

@ -14,6 +14,12 @@ describe('src/helpers/combineURL.ts', () => {
expect(combineURL('unknow://api.com', '')).toBe('unknow://api.com'); expect(combineURL('unknow://api.com', '')).toBe('unknow://api.com');
}); });
test('应该直接返回第二个参数', () => {
expect(combineURL('', 'http://api.com')).toBe('http://api.com');
expect(combineURL('', 'file://api.com')).toBe('file://api.com');
expect(combineURL('', 'unknow://api.com')).toBe('unknow://api.com');
});
test('应该得到拼接后的结果', () => { test('应该得到拼接后的结果', () => {
expect(combineURL('http://api.com', 'test')).toBe('http://api.com/test'); expect(combineURL('http://api.com', 'test')).toBe('http://api.com/test');
expect(combineURL('file://api.com', '/test')).toBe('file://api.com/test'); expect(combineURL('file://api.com', '/test')).toBe('file://api.com/test');

View File

@ -100,6 +100,7 @@ describe('src/request/dispatchRequest.ts', () => {
}; };
const c3 = { const c3 = {
...defaults, ...defaults,
method: 'post' as const,
url: 'test/:id', url: 'test/:id',
data: { data: {
id: 1, id: 1,