Skip to content

Commit

Permalink
feat: add onComplete in global response interceptor (#233)
Browse files Browse the repository at this point in the history
* first

* feat: add onComplete in global response interceptor

* fix: onComplete was not called when hit response cache
  • Loading branch information
panghujiajia authored Nov 24, 2023
1 parent 052bd2d commit 1e7f9d5
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 24 deletions.
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img width="200px" src="https://alova.js.org/img/logo-text-vertical.svg" />
</p>

<p align="center"><b>轻量级的请求策略库,它针对不同请求场景分别提供了具有针对性的请求策略,来提升应用可用性、流畅性,降低服务端压力,让应用如智者一般具备卓越的策略思维</b></p>
<p align="center"><b>轻量级的请求策略库,它针对不同请求场景分别提供了具有针对性的请求策略,来提升应用可用性、流畅性,降低服务端压力,让应用如智者一般具备卓越的策略思维</b></p>

<p align="center">中文 | <a href="./README.md">📑English</a></p>

Expand Down
54 changes: 33 additions & 21 deletions src/functions/sendRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import { matchSnapshotMethod, saveMethodSnapshot } from '@/storage/methodSnapSho
import { getResponseCache, setResponseCache } from '@/storage/responseCache';
import { persistResponse } from '@/storage/responseStorage';
import cloneMethod from '@/utils/cloneMethod';
import { AlovaRequestAdapter, Arg, ProgressUpdater, ResponsedHandler, ResponseErrorHandler } from '~/typings';
import {
AlovaRequestAdapter,
Arg,
ProgressUpdater,
ResponsedHandler,
ResponseErrorHandler,
ResponseCompleteHandler
} from '~/typings';
import {
getConfig,
getContext,
Expand Down Expand Up @@ -124,16 +131,6 @@ export default function sendRequest<S, E, R, T, RC, RE, RH>(
}
})
.then(cachedResponse => {
// 如果没有缓存则发起请求
const { e: expireTimestamp, s: toStorage, t: tag, m: cacheMode } = getLocalCacheConfigParam(clonedMethod);
if (cachedResponse !== undefinedValue) {
requestAdapterCtrlsPromiseResolveFn(); // 遇到缓存将不传入ctrls

// 打印缓存日志
sloughFunction(cacheLogger, defaultCacheLogger)(cachedResponse, clonedMethod, cacheMode, tag);
return cachedResponse;
}
fromCache = falseValue;
const { baseURL, url: newUrl, type, data } = clonedMethod,
{ id, storage } = getContext(clonedMethod),
{
Expand All @@ -150,7 +147,27 @@ export default function sendRequest<S, E, R, T, RC, RE, RH>(
responseUnified = responded || responsed;
let requestAdapterCtrls = namespacedAdapterReturnMap[methodKey],
responseHandler: ResponsedHandler<any, any, RC, RE, RH> = _self,
responseErrorHandler: ResponseErrorHandler<any, any, RC, RE, RH> | undefined = undefinedValue;
responseErrorHandler: ResponseErrorHandler<any, any, RC, RE, RH> | undefined = undefinedValue,
responseCompleteHandler: ResponseCompleteHandler<any, any, RC, RE, RH> = _self;
if (isFn(responseUnified)) {
responseHandler = responseUnified;
} else if (isPlainObject(responseUnified)) {
const { onSuccess: successHandler, onError: errorHandler, onComplete: completeHandler } = responseUnified;
responseHandler = isFn(successHandler) ? successHandler : responseHandler;
responseErrorHandler = isFn(errorHandler) ? errorHandler : responseErrorHandler;
responseCompleteHandler = isFn(completeHandler) ? completeHandler : responseCompleteHandler;
}
// 如果没有缓存则发起请求
const { e: expireTimestamp, s: toStorage, t: tag, m: cacheMode } = getLocalCacheConfigParam(clonedMethod);
if (cachedResponse !== undefinedValue) {
requestAdapterCtrlsPromiseResolveFn(); // 遇到缓存将不传入ctrls

// 打印缓存日志
sloughFunction(cacheLogger, defaultCacheLogger)(cachedResponse, clonedMethod, cacheMode, tag);
responseCompleteHandler(clonedMethod);
return cachedResponse;
}
fromCache = falseValue;

if (!shareRequest || !requestAdapterCtrls) {
// 请求数据
Expand All @@ -167,14 +184,6 @@ export default function sendRequest<S, E, R, T, RC, RE, RH>(
}
// 将requestAdapterCtrls传到promise中供onDownload、onUpload及abort中使用
requestAdapterCtrlsPromiseResolveFn(requestAdapterCtrls);

if (isFn(responseUnified)) {
responseHandler = responseUnified;
} else if (isPlainObject(responseUnified)) {
const { onSuccess: successHandler, onError: errorHandler } = responseUnified;
responseHandler = isFn(successHandler) ? successHandler : responseHandler;
responseErrorHandler = isFn(errorHandler) ? errorHandler : responseErrorHandler;
}
return (
PromiseCls.all([requestAdapterCtrls.response(), requestAdapterCtrls.headers()])
.then(
Expand Down Expand Up @@ -226,7 +235,10 @@ export default function sendRequest<S, E, R, T, RC, RE, RH>(
}
)
// 请求成功、失败,以及在成功后处理报错,都需要移除共享的请求
.finally(() => deleteAttr(namespacedAdapterReturnMap, methodKey))
.finally(() => {
deleteAttr(namespacedAdapterReturnMap, methodKey);
return responseCompleteHandler(clonedMethod);
})
);
});
};
Expand Down
94 changes: 94 additions & 0 deletions test/browser/global/createAlova.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,100 @@ describe('createAlova', function () {
expect(errorConsoleMockFn).toBeCalledTimes(3);
});

test('`responded-onComplete` hook will receive the method param', async () => {
const mockFn = jest.fn();
const alova = getAlovaInstance(VueHook, {
beforeRequestExpect: method => {
method.meta = {
a: 1,
b: 2
};
},
resCompleteExpect: method => {
expect(method.meta).toEqual({ a: 1, b: 2 });
mockFn();
}
});
const Get = alova.Get('/unit-test', {
params: { a: 'a', b: 'str' },
timeout: 10000,
headers: {
'Content-Type': 'application/json'
},
localCache: 100 * 1000
});
await Get.send();
});

test('should emit onComplete when `beforeRequest` hook throws a error', async () => {
const alova = createAlova({
baseURL: 'http://localhost:3000',
statesHook: VueHook,
requestAdapter: GlobalFetch(),
beforeRequest: method => {
if (method.config.params.async) {
return Promise.reject(new Error('reject in beforeRequest'));
}
throw new Error('error in beforeRequest');
},
errorLogger: false,
responded: r => r.json()
});
const Get = (async = true) =>
alova.Get<Result>('/unit-test', {
params: { async }
});

// beforeRequest异步函数测试
const { onComplete } = useRequest(Get);
let completeEvent = await untilCbCalled(onComplete);
expect(completeEvent.error.message).toBe('reject in beforeRequest');
await expect(Get().send()).rejects.toThrow('reject in beforeRequest');

// beforeRequest同步函数测试
const { onComplete: onComplete2 } = useRequest(Get(false));
completeEvent = await untilCbCalled(onComplete2);
expect(completeEvent.error.message).toBe('error in beforeRequest');
await expect(Get(false).send()).rejects.toThrow('error in beforeRequest');
});

test('should emit onComplete when hit response cache', async () => {
const MockFn = jest.fn();
const alova = createAlova({
baseURL,
statesHook: VueHook,
requestAdapter: GlobalFetch(),
responded: {
onComplete: method => {
expect(method).toBeInstanceOf(Method);
MockFn();
}
}
});
const Get = alova.Get('/unit-test', {
localCache: 1000 * 100
});

await Get.send();
await Get.send();
await Get.send();
expect(MockFn).toBeCalledTimes(3);
});

test('should throws a async error in `responded-onComplete` hook', async () => {
const alova = getAlovaInstance(VueHook, {
resErrorExpect: async () => {},
resCompleteExpect: async () => {
await new Promise(resolve => {
setTimeout(resolve, 200);
});
throw new Error('async error');
}
});
const Get = alova.Get('/unit-test-error');
await expect(Get.send()).rejects.toThrow('async error');
});

test("shouldn't print error message when set errorLogger to false", async () => {
const errorConsoleMockFn = jest.fn();
console.error = errorConsoleMockFn;
Expand Down
7 changes: 5 additions & 2 deletions test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,15 @@ export const getAlovaInstance = <S, E>(
localCache,
beforeRequestExpect,
responseExpect,
resErrorExpect
resErrorExpect,
resCompleteExpect
}: {
endWithSlash?: boolean;
localCache?: GlobalLocalCacheConfig;
beforeRequestExpect?: (methodInstance: FetchMethod) => void;
responseExpect?: (response: Response, method: FetchMethod) => void;
resErrorExpect?: (err: Error, method: FetchMethod) => void;
resCompleteExpect?: (method: FetchMethod) => void;
} = {}
) => {
const alovaInst = createAlova({
Expand All @@ -71,7 +73,8 @@ export const getAlovaInstance = <S, E>(
: resErrorExpect
? {
onSuccess: responseExpect,
onError: resErrorExpect
onError: resErrorExpect,
onComplete: resCompleteExpect
}
: undefined,
errorLogger: false,
Expand Down
7 changes: 7 additions & 0 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ type ResponseErrorHandler<R, T, RC, RE, RH> = (
error: any,
methodInstance: Method<any, any, R, T, RC, RE, RH>
) => void | Promise<void>;
type ResponseCompleteHandler<R, T, RC, RE, RH> = (methodInstance: Method<any, any, R, T, RC, RE, RH>) => any;
type ResponsedHandlerRecord<R, T, RC, RE, RH> = {
/**
* 全局的请求成功钩子函数
Expand All @@ -164,6 +165,12 @@ type ResponsedHandlerRecord<R, T, RC, RE, RH> = {
* 当指定了全局onError捕获错误时,如果没有抛出错误则会触发请求位置的onSuccess
*/
onError?: ResponseErrorHandler<R, T, RC, RE, RH>;

/**
* 请求完成钩子函数
* 请求成功、缓存匹配成功、请求失败都将触发此钩子函数
*/
onComplete?: ResponseCompleteHandler<R, T, RC, RE, RH>;
};

type HookType = 1 | 2 | 3;
Expand Down

0 comments on commit 1e7f9d5

Please sign in to comment.