revert: 取消支持为路径添加中间件

pull/49/head
zjx0905 2023-04-25 22:18:08 +08:00
parent db787a2b5f
commit 1e5809aee3
10 changed files with 315 additions and 81 deletions

View File

@ -35,10 +35,10 @@ export function noop() {
} }
export function mockResponse( export function mockResponse(
status: number, status = 200,
statusText: string, statusText = 'OK',
headers: AnyObject, headers: AnyObject = {},
data: AnyObject, data: AnyObject = {},
) { ) {
return { return {
status, status,

View File

@ -22,6 +22,7 @@ import {
WITH_PARAMS_METHODS, WITH_PARAMS_METHODS,
} from '../constants/methods'; } from '../constants/methods';
import MiddlewareManager, { import MiddlewareManager, {
MiddlewareContext,
MiddlewareNext, MiddlewareNext,
MiddlewareUse, MiddlewareUse,
} from './MiddlewareManager'; } from './MiddlewareManager';
@ -306,11 +307,6 @@ export interface AxiosResponseError extends AnyObject {
request?: AxiosAdapterPlatformTask; request?: AxiosAdapterPlatformTask;
} }
export interface AxiosContext {
req: AxiosRequestConfig;
res: null | AxiosResponse;
}
export interface AxiosRequest { export interface AxiosRequest {
<TData extends AxiosResponseData>(config: AxiosRequestConfig): Promise< <TData extends AxiosResponseData>(config: AxiosRequestConfig): Promise<
AxiosResponse<TData> AxiosResponse<TData>
@ -382,7 +378,7 @@ export default class Axios {
/** /**
* *
*/ */
#middleware = new MiddlewareManager<AxiosContext>(); #middleware = new MiddlewareManager();
/** /**
* options * options
@ -432,9 +428,9 @@ export default class Axios {
/** /**
* *
*/ */
use: MiddlewareUse<AxiosContext>; use: MiddlewareUse;
constructor(defaults: AxiosRequestConfig = {}, parent?: Axios) { constructor(defaults: AxiosRequestConfig, parent?: Axios) {
this.defaults = defaults; this.defaults = defaults;
this.#parent = parent; this.#parent = parent;
this.use = this.#middleware.use; this.use = this.#middleware.use;
@ -504,24 +500,23 @@ export default class Axios {
} }
#requestHandler = async (config: AxiosRequestConfig) => { #requestHandler = async (config: AxiosRequestConfig) => {
config.url = combineURL(config.baseURL, config.url); const ctx: MiddlewareContext = {
const ctx: AxiosContext = {
req: config, req: config,
res: null, res: null,
}; };
await this.#flush(ctx, async () => { await this.#wrap(ctx, async () => {
ctx.res = await dispatchRequest(ctx.req); ctx.res = await dispatchRequest(ctx.req);
}); });
return ctx.res as AxiosResponse; return ctx.res as AxiosResponse;
}; };
#flush(ctx: AxiosContext, finish: MiddlewareNext): Promise<void> { #wrap(ctx: MiddlewareContext, flush: MiddlewareNext): Promise<void> {
if (this.#parent) { if (this.#parent) {
return this.#parent.#flush(ctx, () => { return this.#parent.#wrap(ctx, () => {
return this.#middleware.flush(ctx, finish); return this.#middleware.wrap(ctx, flush);
}); });
} }
return this.#middleware.flush(ctx, finish); return this.#middleware.wrap(ctx, flush);
} }
} }

View File

@ -1,76 +1,55 @@
import { assert } from '../helpers/error'; import { assert } from '../helpers/error';
import { combineURL } from '../helpers/combineURL'; import { combineURL } from '../helpers/combineURL';
import { isFunction, isString } from '../helpers/isTypes'; import { isFunction, isString } from '../helpers/isTypes';
import { AxiosRequestConfig, AxiosResponse } from './Axios';
export interface MiddlewareNext { export interface MiddlewareNext {
(): Promise<void>; (): Promise<void>;
} }
export interface MiddlewareCallback<Context extends AnyObject> { export interface MiddlewareContext {
(ctx: Context, next: MiddlewareNext): Promise<void>; req: AxiosRequestConfig;
res: null | AxiosResponse;
} }
export interface MiddlewareUse<Context extends AnyObject> { export interface MiddlewareCallback {
/** (ctx: MiddlewareContext, next: MiddlewareNext): Promise<void>;
* }
*
* @param path export interface MiddlewareUse {
* @param callback
*/
(
path: string,
callback: MiddlewareCallback<Context>,
): MiddlewareManager<Context>;
/** /**
* *
* *
* @param callback * @param callback
*/ */
(callback: MiddlewareCallback<Context>): MiddlewareManager<Context>; (callback: MiddlewareCallback): MiddlewareManager;
} }
export default class MiddlewareManager<Context extends AnyObject = AnyObject> { export default class MiddlewareManager {
#map = new Map<string, MiddlewareCallback<Context>[]>(); /**
*
*/
#middlewares: MiddlewareCallback[] = [];
/** /**
* *
*/ */
use: MiddlewareUse<Context> = ( use: MiddlewareUse = (callback: MiddlewareCallback) => {
path: string | MiddlewareCallback<Context>,
callback?: MiddlewareCallback<Context>,
) => {
if (isFunction(path)) {
callback = path;
path = '/';
}
assert(isString(path), 'path 不是一个 string');
assert(!!path, 'path 不是一个长度大于零的 string');
assert(isFunction(callback), 'callback 不是一个 function'); assert(isFunction(callback), 'callback 不是一个 function');
this.#middlewares.push(callback!);
const middlewares = this.#map.get(path) ?? [];
middlewares.push(callback!);
this.#map.set(path, middlewares);
return this; return this;
}; };
flush(ctx: Context, finish: MiddlewareNext) { /**
const allMiddlewares: MiddlewareCallback<Context>[] = []; *
*
for (const [path, middlewares] of this.#map.entries()) { * @param ctx
const url = combineURL(ctx.req.baseURL, path); * @param flush
const checkRE = new RegExp(`^${url}([/?].*)?`); */
wrap(ctx: MiddlewareContext, flush: MiddlewareNext) {
if (path === '/') { const runners = [...this.#middlewares, flush];
allMiddlewares.push(...middlewares);
} else if (checkRE.test(ctx.req.url!)) {
allMiddlewares.push(...middlewares);
}
}
const tasks = [...allMiddlewares, finish];
return (function next(): Promise<void> { return (function next(): Promise<void> {
return tasks.shift()!(ctx, next); return runners.shift()!(ctx, next);
})(); })();
} }
} }

View File

@ -57,7 +57,7 @@ export function createInstance(config: AxiosRequestConfig, parent?: Axios) {
const context = new Axios(config, parent); const context = new Axios(config, parent);
const instance = context.request as AxiosInstance; const instance = context.request as AxiosInstance;
instance.getUri = function getUri(config: AxiosRequestConfig) { instance.getUri = function getUri(config) {
const { url, params, paramsSerializer } = mergeConfig( const { url, params, paramsSerializer } = mergeConfig(
instance.defaults, instance.defaults,
config, config,
@ -67,7 +67,7 @@ export function createInstance(config: AxiosRequestConfig, parent?: Axios) {
instance.create = function create(config) { instance.create = function create(config) {
return createInstance(mergeConfig(instance.defaults, config)); return createInstance(mergeConfig(instance.defaults, config));
}; };
instance.extend = function extend(config: AxiosRequestConfig = {}) { instance.extend = function extend(config) {
config.baseURL = combineURL(instance.defaults.baseURL, config.baseURL); config.baseURL = combineURL(instance.defaults.baseURL, config.baseURL);
return createInstance(mergeConfig(instance.defaults, config), context); return createInstance(mergeConfig(instance.defaults, config), context);
}; };

View File

@ -1,5 +1,10 @@
import axios from './axios'; import axios from './axios';
export type {
MiddlewareContext,
MiddlewareCallback,
MiddlewareNext,
} from './core/MiddlewareManager';
export type { export type {
AxiosRequestConfig, AxiosRequestConfig,
AxiosRequestData, AxiosRequestData,
@ -14,10 +19,6 @@ export type {
AxiosUploadProgressEvent, AxiosUploadProgressEvent,
AxiosUploadProgressCallback, AxiosUploadProgressCallback,
} from './core/Axios'; } from './core/Axios';
export type {
MiddlewareCallback,
MiddlewareNext,
} from './core/MiddlewareManager';
export type { export type {
AxiosInstanceDefaults, AxiosInstanceDefaults,
AxiosInstance, AxiosInstance,

View File

@ -11,7 +11,6 @@ describe('src/axios.ts', () => {
test('应该有这些静态属性', () => { test('应该有这些静态属性', () => {
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);

View File

@ -7,6 +7,7 @@ import {
} from '@/constants/methods'; } from '@/constants/methods';
import defaults from '@/defaults'; import defaults from '@/defaults';
import axios from '@/axios'; import axios from '@/axios';
import { createInstance } from '@/core/createInstance';
describe('src/axios.ts', () => { describe('src/axios.ts', () => {
const data = { result: null }; const data = { result: null };
@ -15,6 +16,9 @@ describe('src/axios.ts', () => {
expect(axios.defaults).toBe(defaults); expect(axios.defaults).toBe(defaults);
expect(axios.interceptors).toBeTypeOf('object'); expect(axios.interceptors).toBeTypeOf('object');
expect(axios.getUri).toBeTypeOf('function'); expect(axios.getUri).toBeTypeOf('function');
expect(axios.create).toBeTypeOf('function');
expect(axios.extend).toBeTypeOf('function');
expect(axios.use).toBeTypeOf('function');
expect(axios.fork).toBeTypeOf('function'); expect(axios.fork).toBeTypeOf('function');
expect(axios.request).toBeTypeOf('function'); expect(axios.request).toBeTypeOf('function');
}); });
@ -162,4 +166,21 @@ describe('src/axios.ts', () => {
'test?id=1', 'test?id=1',
); );
}); });
test('应该支持中间件', async () => {
const axios = createInstance(defaults);
axios.defaults.adapter = mockAdapter();
const cb = vi.fn(async (ctx, next) => {
expect(ctx.res).toBeNull();
await next();
expect(ctx.res).toBeTypeOf('object');
});
axios.use(cb);
await axios.get('test');
expect(cb).toBeCalled();
});
}); });

View File

@ -29,6 +29,7 @@ describe('src/core/Axios.ts', () => {
expect(axiosObj.defaults).toEqual(c); expect(axiosObj.defaults).toEqual(c);
expect(axiosObj.interceptors).toBeTypeOf('object'); expect(axiosObj.interceptors).toBeTypeOf('object');
expect(axiosObj.request).toBeTypeOf('function'); expect(axiosObj.request).toBeTypeOf('function');
expect(axiosObj.use).toBeTypeOf('function');
}); });
testEachMethods('%s 应该是一个函数', (k) => { testEachMethods('%s 应该是一个函数', (k) => {
@ -36,23 +37,34 @@ describe('src/core/Axios.ts', () => {
}); });
test('应该可以发送普通别名请求', () => { test('应该可以发送普通别名请求', () => {
const c = { const axiosObj = new Axios({
adapter: mockAdapter({ adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test');
},
data, data,
}), }),
});
const c = {
baseURL: 'https://api.com',
}; };
PLAIN_METHODS.forEach((a) => { PLAIN_METHODS.forEach((a) => {
axiosObj[a]('test').then((res) => {
expect(res.config.baseURL).toBeUndefined();
expect(res.data).toEqual(data);
});
axiosObj[a]('test', c).then((res) => { axiosObj[a]('test', c).then((res) => {
expect(res.config.baseURL).toBe('https://api.com');
expect(res.data).toEqual(data); expect(res.data).toEqual(data);
}); });
}); });
}); });
test('应该可以发送带参数的别名请求', () => { test('应该可以发送带参数的别名请求', () => {
const axiosObj = new Axios({
adapter: mockAdapter({
data,
}),
baseURL: 'http://api.com',
});
const p = { id: 1 }; const p = { id: 1 };
const c1 = { const c1 = {
adapter: mockAdapter({ adapter: mockAdapter({
@ -74,6 +86,12 @@ describe('src/core/Axios.ts', () => {
}; };
WITH_PARAMS_METHODS.forEach((a) => { WITH_PARAMS_METHODS.forEach((a) => {
axiosObj[a]('test').then((res) => {
expect(res.data).toEqual(data);
});
axiosObj[a]('test', p).then((res) => {
expect(res.data).toEqual(data);
});
axiosObj[a]('test', p, c1).then((res) => { axiosObj[a]('test', p, c1).then((res) => {
expect(res.data).toEqual(data); expect(res.data).toEqual(data);
}); });
@ -84,6 +102,12 @@ describe('src/core/Axios.ts', () => {
}); });
test('应该可以发送带数据的别名请求', () => { test('应该可以发送带数据的别名请求', () => {
const axiosObj = new Axios({
adapter: mockAdapter({
data,
}),
baseURL: 'http://api.com',
});
const d = { id: 1 }; const d = { id: 1 };
const c1 = { const c1 = {
adapter: mockAdapter({ adapter: mockAdapter({
@ -105,6 +129,12 @@ describe('src/core/Axios.ts', () => {
}; };
WITH_DATA_METHODS.forEach((a) => { WITH_DATA_METHODS.forEach((a) => {
axiosObj[a]('test').then((res) => {
expect(res.data).toEqual(data);
});
axiosObj[a]('test', d).then((res) => {
expect(res.data).toEqual(data);
});
axiosObj[a]('test', d, c1).then((res) => { axiosObj[a]('test', d, c1).then((res) => {
expect(res.data).toEqual(data); expect(res.data).toEqual(data);
}); });
@ -207,7 +237,7 @@ describe('src/core/Axios.ts', () => {
}); });
test('添加多个请求拦截器时应该按添加顺序从后往前依次执行', () => { test('添加多个请求拦截器时应该按添加顺序从后往前依次执行', () => {
const axiosObj = new Axios(); const axiosObj = new Axios({});
const cb1 = vi.fn((v) => { const cb1 = vi.fn((v) => {
expect(v.params.index).toBe(2); expect(v.params.index).toBe(2);
@ -240,7 +270,7 @@ describe('src/core/Axios.ts', () => {
}); });
test('请求拦截器应该支持抛出异常', async () => { test('请求拦截器应该支持抛出异常', async () => {
const axiosObj = new Axios(); const axiosObj = new Axios({});
const c = { adapter: vi.fn(), url: 'test' }; const c = { adapter: vi.fn(), url: 'test' };
const body = (v: any) => { const body = (v: any) => {
throw { ...v, throw: true }; throw { ...v, throw: true };
@ -293,7 +323,7 @@ describe('src/core/Axios.ts', () => {
}); });
test('添加多个响应拦截器时应该按添加顺序从前往后依次执行', async () => { test('添加多个响应拦截器时应该按添加顺序从前往后依次执行', async () => {
const axiosObj = new Axios(); const axiosObj = new Axios({});
const cb1 = vi.fn((v) => { const cb1 = vi.fn((v) => {
expect(v.data.index).toBe(0); expect(v.data.index).toBe(0);
@ -323,7 +353,7 @@ describe('src/core/Axios.ts', () => {
}); });
test('响应拦截器应该支持抛出异常', async () => { test('响应拦截器应该支持抛出异常', async () => {
const axiosObj = new Axios(); const axiosObj = new Axios({});
const c = { adapter: vi.fn(mockAdapter()), url: 'test' }; const c = { adapter: vi.fn(mockAdapter()), url: 'test' };
const body = () => { const body = () => {
throw { throw: true }; throw { throw: true };
@ -347,4 +377,21 @@ describe('src/core/Axios.ts', () => {
expect(res2).not.toBeCalled(); expect(res2).not.toBeCalled();
expect(rej2).toBeCalled(); expect(rej2).toBeCalled();
}); });
test('应该支持中间件', async () => {
const axiosObj = new Axios({
adapter: mockAdapter(),
});
const cb = vi.fn(async (ctx, next) => {
expect(ctx.res).toBeNull();
await next();
expect(ctx.res).toBeTypeOf('object');
});
axiosObj.use(cb);
await axiosObj.get('test');
expect(cb).toBeCalled();
});
}); });

View File

@ -0,0 +1,101 @@
import { describe, test, expect, vi } from 'vitest';
import MiddlewareManager, { MiddlewareContext } from '@/core/MiddlewareManager';
import { AxiosResponse } from '@/index';
describe('src/core/MiddlewareManager.ts', () => {
test('应该有这些实例属性', () => {
const m = new MiddlewareManager();
expect(m.use).toBeTypeOf('function');
expect(m.wrap).toBeTypeOf('function');
});
test('应该抛出异常', () => {
const m = new MiddlewareManager();
expect(() => m.use(undefined as any)).toThrowError(
'[axios-miniprogram]: callback 不是一个 function',
);
});
test('应该支持添加中间件', () => {
const m = new MiddlewareManager();
const c: MiddlewareContext = {
req: {},
res: null,
};
const res = {} as AxiosResponse;
const cb = vi.fn(async (ctx, next) => {
await next();
});
const flush = vi.fn(async () => {
c.res = res;
});
m.use(cb);
m.wrap(c, flush);
expect(cb).toBeCalled();
expect(flush).toBeCalled();
});
test('应该支持洋葱模型', async () => {
const m = new MiddlewareManager();
const c: MiddlewareContext = {
req: {},
res: null,
};
const res = {} as AxiosResponse;
const cb1 = vi.fn(async (ctx, next) => {
// 1
expect(ctx.req.step).toBeUndefined();
ctx.req.step = 'cb1 start';
await next();
// 8
expect(ctx.res.step).toBe('cb2 end');
ctx.res.step = 'cb1 end';
});
const cb2 = vi.fn((ctx, next) => {
// 2
expect(ctx.req.step).toBe('cb1 start');
ctx.req.step = 'cb2 start';
return next().then(() => {
// 7
expect(ctx.res.step).toBe('cb3 end');
ctx.res.step = 'cb2 end';
});
});
const cb3 = vi.fn(async (ctx, next) => {
// 3
expect(ctx.req.step).toBe('cb2 start');
ctx.req.step = 'cb3 start';
await next();
// 6
expect(ctx.res.step).toBe('flush end');
ctx.res.step = 'cb3 end';
});
const flush = vi.fn(async () => {
// 4
expect(c.req.step).toBe('cb3 start');
c.req.step = 'flush start';
c.res = res;
// 5
expect(c.res.step).toBeUndefined();
c.res.step = 'flush end';
});
m.use(cb1);
m.use(cb2);
m.use(cb3);
await m.wrap(c, flush);
expect(c.req.step).toBe('flush start');
expect(c.res!.step).toBe('cb1 end');
expect(cb1).toBeCalled();
expect(cb2).toBeCalled();
expect(cb3).toBeCalled();
expect(flush).toBeCalled();
});
});

View File

@ -0,0 +1,91 @@
import { describe, test, expect, vi } from 'vitest';
import { eachMethods, mockAdapter } from 'scripts/test.utils';
import { createInstance } from '@/core/createInstance';
describe('src/core/createInstance.ts', () => {
test('应该可以创建实例', () => {
const i = createInstance({});
expect(i).toBeTypeOf('function');
expect(i.defaults).toBeTypeOf('object');
expect(i.interceptors).toBeTypeOf('object');
expect(i.getUri).toBeTypeOf('function');
expect(i.create).toBeTypeOf('function');
expect(i.extend).toBeTypeOf('function');
expect(i.fork).toBeTypeOf('function');
expect(i.use).toBeTypeOf('function');
expect(i.request).toBeTypeOf('function');
eachMethods((k) => {
expect(i[k]).toBeTypeOf('function');
});
});
test('应该支持合并配置', () => {
const parent = createInstance({});
const child = parent.create({
baseURL: 'https://api.com',
});
expect(parent.defaults.baseURL).toBeUndefined();
expect(child.defaults.baseURL).toBe('https://api.com');
});
test('应该支持复用拦截器', async () => {
const parent = createInstance({
baseURL: 'https://api.com',
});
const child = parent.extend({
adapter: mockAdapter(),
});
const cb1 = vi.fn((config) => config);
const cb2 = vi.fn((response) => response);
const cb3 = vi.fn((config) => config);
const cb4 = vi.fn((response) => response);
parent.interceptors.request.use(cb1);
parent.interceptors.response.use(cb2);
child.interceptors.request.use(cb3);
child.interceptors.response.use(cb4);
await child('test');
expect(cb1).toBeCalled();
expect(cb2).toBeCalled();
expect(cb3).toBeCalled();
expect(cb4).toBeCalled();
});
test('应该支持复用中间件', async () => {
const parent = createInstance({
baseURL: 'https://api.com',
});
const child = parent.extend({
adapter: mockAdapter(),
});
const cb1 = vi.fn(async (ctx, next) => {
await next();
});
const cb2 = vi.fn(async (ctx, next) => {
await next();
});
const cb3 = vi.fn(async (ctx, next) => {
await next();
});
const cb4 = vi.fn(async (ctx, next) => {
await next();
});
parent.use(cb1);
parent.use(cb2);
child.use(cb3);
child.use(cb4);
await child('test');
expect(cb1).toBeCalled();
expect(cb2).toBeCalled();
expect(cb3).toBeCalled();
expect(cb4).toBeCalled();
});
});