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(
status: number,
statusText: string,
headers: AnyObject,
data: AnyObject,
status = 200,
statusText = 'OK',
headers: AnyObject = {},
data: AnyObject = {},
) {
return {
status,

View File

@ -22,6 +22,7 @@ import {
WITH_PARAMS_METHODS,
} from '../constants/methods';
import MiddlewareManager, {
MiddlewareContext,
MiddlewareNext,
MiddlewareUse,
} from './MiddlewareManager';
@ -306,11 +307,6 @@ export interface AxiosResponseError extends AnyObject {
request?: AxiosAdapterPlatformTask;
}
export interface AxiosContext {
req: AxiosRequestConfig;
res: null | AxiosResponse;
}
export interface AxiosRequest {
<TData extends AxiosResponseData>(config: AxiosRequestConfig): Promise<
AxiosResponse<TData>
@ -382,7 +378,7 @@ export default class Axios {
/**
*
*/
#middleware = new MiddlewareManager<AxiosContext>();
#middleware = new MiddlewareManager();
/**
* 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.#parent = parent;
this.use = this.#middleware.use;
@ -504,24 +500,23 @@ export default class Axios {
}
#requestHandler = async (config: AxiosRequestConfig) => {
config.url = combineURL(config.baseURL, config.url);
const ctx: AxiosContext = {
const ctx: MiddlewareContext = {
req: config,
res: null,
};
await this.#flush(ctx, async () => {
await this.#wrap(ctx, async () => {
ctx.res = await dispatchRequest(ctx.req);
});
return ctx.res as AxiosResponse;
};
#flush(ctx: AxiosContext, finish: MiddlewareNext): Promise<void> {
#wrap(ctx: MiddlewareContext, flush: MiddlewareNext): Promise<void> {
if (this.#parent) {
return this.#parent.#flush(ctx, () => {
return this.#middleware.flush(ctx, finish);
return this.#parent.#wrap(ctx, () => {
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 { combineURL } from '../helpers/combineURL';
import { isFunction, isString } from '../helpers/isTypes';
import { AxiosRequestConfig, AxiosResponse } from './Axios';
export interface MiddlewareNext {
(): Promise<void>;
}
export interface MiddlewareCallback<Context extends AnyObject> {
(ctx: Context, next: MiddlewareNext): Promise<void>;
export interface MiddlewareContext {
req: AxiosRequestConfig;
res: null | AxiosResponse;
}
export interface MiddlewareUse<Context extends AnyObject> {
/**
*
*
* @param path
* @param callback
*/
(
path: string,
callback: MiddlewareCallback<Context>,
): MiddlewareManager<Context>;
export interface MiddlewareCallback {
(ctx: MiddlewareContext, next: MiddlewareNext): Promise<void>;
}
export interface MiddlewareUse {
/**
*
*
* @param callback
*/
(callback: MiddlewareCallback<Context>): MiddlewareManager<Context>;
(callback: MiddlewareCallback): MiddlewareManager;
}
export default class MiddlewareManager<Context extends AnyObject = AnyObject> {
#map = new Map<string, MiddlewareCallback<Context>[]>();
export default class MiddlewareManager {
/**
*
*/
#middlewares: MiddlewareCallback[] = [];
/**
*
*/
use: MiddlewareUse<Context> = (
path: string | MiddlewareCallback<Context>,
callback?: MiddlewareCallback<Context>,
) => {
if (isFunction(path)) {
callback = path;
path = '/';
}
assert(isString(path), 'path 不是一个 string');
assert(!!path, 'path 不是一个长度大于零的 string');
use: MiddlewareUse = (callback: MiddlewareCallback) => {
assert(isFunction(callback), 'callback 不是一个 function');
const middlewares = this.#map.get(path) ?? [];
middlewares.push(callback!);
this.#map.set(path, middlewares);
this.#middlewares.push(callback!);
return this;
};
flush(ctx: Context, finish: MiddlewareNext) {
const allMiddlewares: MiddlewareCallback<Context>[] = [];
for (const [path, middlewares] of this.#map.entries()) {
const url = combineURL(ctx.req.baseURL, path);
const checkRE = new RegExp(`^${url}([/?].*)?`);
if (path === '/') {
allMiddlewares.push(...middlewares);
} else if (checkRE.test(ctx.req.url!)) {
allMiddlewares.push(...middlewares);
}
}
const tasks = [...allMiddlewares, finish];
/**
*
*
* @param ctx
* @param flush
*/
wrap(ctx: MiddlewareContext, flush: MiddlewareNext) {
const runners = [...this.#middlewares, flush];
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 instance = context.request as AxiosInstance;
instance.getUri = function getUri(config: AxiosRequestConfig) {
instance.getUri = function getUri(config) {
const { url, params, paramsSerializer } = mergeConfig(
instance.defaults,
config,
@ -67,7 +67,7 @@ export function createInstance(config: AxiosRequestConfig, parent?: Axios) {
instance.create = function create(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);
return createInstance(mergeConfig(instance.defaults, config), context);
};

View File

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

View File

@ -11,7 +11,6 @@ describe('src/axios.ts', () => {
test('应该有这些静态属性', () => {
expect(axios.Axios).toBe(Axios);
expect(axios.CancelToken).toBe(CancelToken);
expect(axios.create).toBeTypeOf('function');
expect(axios.createAdapter).toBe(createAdapter);
expect(axios.isCancel).toBe(isCancel);
expect(axios.isAxiosError).toBe(isAxiosError);

View File

@ -7,6 +7,7 @@ import {
} from '@/constants/methods';
import defaults from '@/defaults';
import axios from '@/axios';
import { createInstance } from '@/core/createInstance';
describe('src/axios.ts', () => {
const data = { result: null };
@ -15,6 +16,9 @@ describe('src/axios.ts', () => {
expect(axios.defaults).toBe(defaults);
expect(axios.interceptors).toBeTypeOf('object');
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.request).toBeTypeOf('function');
});
@ -162,4 +166,21 @@ describe('src/axios.ts', () => {
'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.interceptors).toBeTypeOf('object');
expect(axiosObj.request).toBeTypeOf('function');
expect(axiosObj.use).toBeTypeOf('function');
});
testEachMethods('%s 应该是一个函数', (k) => {
@ -36,23 +37,34 @@ describe('src/core/Axios.ts', () => {
});
test('应该可以发送普通别名请求', () => {
const c = {
const axiosObj = new Axios({
adapter: mockAdapter({
before: (config) => {
expect(config.url).toBe('http://api.com/test');
},
data,
}),
});
const c = {
baseURL: 'https://api.com',
};
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) => {
expect(res.config.baseURL).toBe('https://api.com');
expect(res.data).toEqual(data);
});
});
});
test('应该可以发送带参数的别名请求', () => {
const axiosObj = new Axios({
adapter: mockAdapter({
data,
}),
baseURL: 'http://api.com',
});
const p = { id: 1 };
const c1 = {
adapter: mockAdapter({
@ -74,6 +86,12 @@ describe('src/core/Axios.ts', () => {
};
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) => {
expect(res.data).toEqual(data);
});
@ -84,6 +102,12 @@ describe('src/core/Axios.ts', () => {
});
test('应该可以发送带数据的别名请求', () => {
const axiosObj = new Axios({
adapter: mockAdapter({
data,
}),
baseURL: 'http://api.com',
});
const d = { id: 1 };
const c1 = {
adapter: mockAdapter({
@ -105,6 +129,12 @@ describe('src/core/Axios.ts', () => {
};
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) => {
expect(res.data).toEqual(data);
});
@ -207,7 +237,7 @@ describe('src/core/Axios.ts', () => {
});
test('添加多个请求拦截器时应该按添加顺序从后往前依次执行', () => {
const axiosObj = new Axios();
const axiosObj = new Axios({});
const cb1 = vi.fn((v) => {
expect(v.params.index).toBe(2);
@ -240,7 +270,7 @@ describe('src/core/Axios.ts', () => {
});
test('请求拦截器应该支持抛出异常', async () => {
const axiosObj = new Axios();
const axiosObj = new Axios({});
const c = { adapter: vi.fn(), url: 'test' };
const body = (v: any) => {
throw { ...v, throw: true };
@ -293,7 +323,7 @@ describe('src/core/Axios.ts', () => {
});
test('添加多个响应拦截器时应该按添加顺序从前往后依次执行', async () => {
const axiosObj = new Axios();
const axiosObj = new Axios({});
const cb1 = vi.fn((v) => {
expect(v.data.index).toBe(0);
@ -323,7 +353,7 @@ describe('src/core/Axios.ts', () => {
});
test('响应拦截器应该支持抛出异常', async () => {
const axiosObj = new Axios();
const axiosObj = new Axios({});
const c = { adapter: vi.fn(mockAdapter()), url: 'test' };
const body = () => {
throw { throw: true };
@ -347,4 +377,21 @@ describe('src/core/Axios.ts', () => {
expect(res2).not.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();
});
});