Skip to content

Commit 5a59550

Browse files
committed
feature: useSSE() basic implement
1 parent f471b97 commit 5a59550

File tree

5 files changed

+259
-1
lines changed

5 files changed

+259
-1
lines changed

src/functions/sendRequest.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { filterItem, map, objectKeys } from '@/helper';
2+
import { undefinedValue } from '@/helper/variables';
3+
import { Arg } from 'alova';
4+
5+
/**
6+
* 构建完整的url
7+
* @param base baseURL
8+
* @param url 路径
9+
* @param params url参数
10+
* @returns 完整的url
11+
*/
12+
export const buildCompletedURL = (baseURL: string, url: string, params: Arg) => {
13+
// baseURL如果以/结尾,则去掉/
14+
baseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
15+
// 如果不是/或http协议开头的,则需要添加/
16+
url = url.match(/^(\/|https?:\/\/)/) ? url : `/${url}`;
17+
18+
const completeURL = baseURL + url;
19+
20+
// 将params对象转换为get字符串
21+
// 过滤掉值为undefined的
22+
const paramsStr = map(
23+
filterItem(objectKeys(params), key => params[key] !== undefinedValue),
24+
key => `${key}=${params[key]}`
25+
).join('&');
26+
// 将get参数拼接到url后面,注意url可能已存在参数
27+
return paramsStr
28+
? +completeURL.includes('?')
29+
? `${completeURL}&${paramsStr}`
30+
: `${completeURL}?${paramsStr}`
31+
: completeURL;
32+
};

src/helper/index.ts

+47-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CacheExpire, LocalCacheConfig, Method } from 'alova';
1+
import { AlovaMethodHandler, CacheExpire, LocalCacheConfig, Method } from 'alova';
22
import { BackoffPolicy } from '~/typings/general';
33
import { ObjectCls, PromiseCls, StringCls, falseValue, nullValue, trueValue, undefinedValue } from './variables';
44

@@ -320,3 +320,49 @@ export const delayWithBackoff = (backoff: BackoffPolicy, retryTimes: number) =>
320320
}
321321
return retryDelayFinally;
322322
};
323+
/**
324+
* 获取请求方法对象
325+
* @param methodHandler 请求方法句柄
326+
* @param args 方法调用参数
327+
* @returns 请求方法对象
328+
*/
329+
export const getHandlerMethod = <S, E, R, T, RC, RE, RH>(
330+
methodHandler: Method<S, E, R, T, RC, RE, RH> | AlovaMethodHandler<S, E, R, T, RC, RE, RH>,
331+
args: any[] = []
332+
) => {
333+
const methodInstance = isFn(methodHandler) ? methodHandler(...args) : methodHandler;
334+
createAssert('scene')(
335+
instanceOf(methodInstance, Method),
336+
'hook handler must be a method instance or a function that returns method instance'
337+
);
338+
return methodInstance;
339+
};
340+
341+
type AnyFn = (...args: any[]) => any;
342+
export function useCallback<Fn extends AnyFn = AnyFn>(onCallbackChange?: (callbacks: Fn[]) => void) {
343+
let callbacks: Fn[] = [];
344+
345+
const setCallback = (fn: Fn) => {
346+
if (!callbacks.includes(fn)) {
347+
callbacks.push(fn);
348+
onCallbackChange && onCallbackChange(callbacks);
349+
}
350+
// 返回取消注册函数
351+
return () => {
352+
callbacks = filterItem(callbacks, e => e !== fn);
353+
onCallbackChange && onCallbackChange(callbacks);
354+
};
355+
};
356+
357+
const triggerCallback = (...args: any[]) => {
358+
if (callbacks) {
359+
return forEach(callbacks, fn => fn(args));
360+
}
361+
};
362+
363+
const removeAllCallback = () => {
364+
callbacks = [];
365+
};
366+
367+
return [setCallback, triggerCallback as Fn, removeAllCallback] as const;
368+
}

src/hooks/useSSE.ts

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { T$, T$$, TonMounted$, Tupd$, Twatch$, T_$, T_exp$, TonUnmounted$ } from '@/framework/type';
2+
import { AlovaMethodHandler, Method, useRequest } from 'alova';
3+
import { getConfig, getHandlerMethod, useCallback } from '@/helper';
4+
import { buildCompletedURL } from '@/functions/sendRequest';
5+
import {
6+
AlovaSSEErrorEvent,
7+
AlovaSSEEvent,
8+
AlovaSSEMessageEvent,
9+
SSEHookConfig,
10+
SSEHookReadyState,
11+
SSEOn,
12+
SSEOnErrorTrigger,
13+
SSEOnMessageTrigger,
14+
SSEOnOpenTrigger
15+
} from '~/typings/general';
16+
17+
// !! interceptByGlobalResponded 参数 尚未实现
18+
19+
export default <S, E, R, T, RC, RE, RH>(
20+
handler: Method<S, E, R, T, RC, RE, RH> | AlovaMethodHandler<S, E, R, T, RC, RE, RH>,
21+
config: SSEHookConfig = {},
22+
$: T$,
23+
$$: T$$,
24+
_$: T_$,
25+
_exp$: T_exp$,
26+
upd$: Tupd$,
27+
watch$: Twatch$,
28+
onMounted$: TonMounted$,
29+
onUnmounted$: TonUnmounted$
30+
// useFlag$: TuseFlag$,
31+
// useMemorizedCallback$: TuseMemorizedCallback$
32+
) => {
33+
// SSE 不需要传参(吧?)
34+
const methodInstance = getHandlerMethod(handler);
35+
const { baseURL, url } = methodInstance;
36+
const { params, transformData, headers } = getConfig(methodInstance);
37+
38+
const fullURL = buildCompletedURL(baseURL, url, params);
39+
const eventSource = new EventSource(fullURL, { withCredentials: config.withCredentials });
40+
41+
const { data, update } = useRequest(handler, config);
42+
const readyState = $<SSEHookReadyState>(SSEHookReadyState.CONNECTING);
43+
44+
// type: eventname & useCallback()
45+
const eventMap: Map<string, ReturnType<typeof useCallback>> = new Map();
46+
const [onOpen, triggerOnOpen, offOpen] = useCallback<SSEOnOpenTrigger>();
47+
const [onMessage, triggerOnMessage, offMessage] = useCallback<SSEOnMessageTrigger<any>>();
48+
const [onError, triggerOnError, offError] = useCallback<SSEOnErrorTrigger>();
49+
50+
const dataHandler = (data: any) => {
51+
const transformedData = transformData ? transformData(data, (headers || {}) as RH) : data;
52+
update({ data: transformedData });
53+
return data;
54+
};
55+
56+
// 将 SourceEvent 产生的事件变为 AlovaSSEHook 的事件
57+
const createSSEEvent = (eventName: string, event: MessageEvent<any> | Event) => {
58+
if (eventName === 'open') {
59+
return {
60+
method: methodInstance,
61+
eventSource
62+
} as AlovaSSEEvent;
63+
}
64+
if (eventName === 'error') {
65+
return {
66+
method: methodInstance,
67+
eventSource,
68+
error: new Error('sse error')
69+
} as AlovaSSEErrorEvent;
70+
}
71+
72+
// 其余名称的事件都是(类)message 的事件,data 交给 dataHandler 处理
73+
return {
74+
method: methodInstance,
75+
eventSource,
76+
data: dataHandler((event as MessageEvent).data)
77+
} as AlovaSSEMessageEvent<any>;
78+
};
79+
80+
const on: SSEOn = (eventName, handler) => {
81+
if (!eventMap.has(eventName)) {
82+
const useCallbackObject = useCallback<(event: AlovaSSEEvent) => void>(callbacks => {
83+
if (callbacks.length === 0) {
84+
eventSource.removeEventListener(eventName, useCallbackObject[1] as any);
85+
eventMap.delete(eventName);
86+
}
87+
});
88+
89+
const trigger = useCallbackObject[1];
90+
eventMap.set(eventName, useCallbackObject);
91+
eventSource.addEventListener(eventName, event => {
92+
trigger(createSSEEvent(eventName, event));
93+
});
94+
}
95+
96+
const [onEvent] = eventMap.get(eventName)!;
97+
98+
return onEvent(handler);
99+
};
100+
101+
eventSource.addEventListener('open', event => {
102+
upd$(readyState, SSEHookReadyState.OPEN);
103+
triggerOnOpen(createSSEEvent('open', event));
104+
});
105+
eventSource.addEventListener('error', event => {
106+
upd$(readyState, SSEHookReadyState.CLOSED);
107+
triggerOnError(createSSEEvent('error', event) as AlovaSSEErrorEvent);
108+
});
109+
eventSource.addEventListener('message', event => {
110+
triggerOnMessage(createSSEEvent('message', event) as AlovaSSEMessageEvent<any>);
111+
});
112+
113+
onUnmounted$(() => {
114+
offOpen();
115+
offMessage();
116+
offError();
117+
});
118+
119+
return {
120+
readyState: _exp$(readyState),
121+
data,
122+
eventSource,
123+
onMessage,
124+
onError,
125+
onOpen,
126+
on
127+
};
128+
};

src/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import useAutoRequest_unified from '@/hooks/useAutoRequest';
2020
import useCaptcha_unified from '@/hooks/useCaptcha';
2121
import useForm_unified from '@/hooks/useForm';
2222
import useRetriableRequest_unified from '@/hooks/useRetriableRequest';
23+
import useSSE_unified from '@/hooks/useSSE';
2324
import { actionDelegationMiddleware as actionDelegationMiddleware_unified } from '@/middlewares/actionDelegation';
2425

2526
export const usePagination = (handler, config = {}) =>
@@ -93,3 +94,7 @@ forEach(objectKeys(useAutoRequest_unified), key => {
9394
trueValue
9495
);
9596
});
97+
98+
// 导出useSSE
99+
export const useSSE = (handler, config = {}) =>
100+
useSSE_unified(handler, config, $, $$, _$, _exp$, upd$, watch$, onMounted$, useFlag$, useMemorizedCallback$);

typings/general.d.ts

+47
Original file line numberDiff line numberDiff line change
@@ -836,5 +836,52 @@ type AutoRequestHookConfig<S, E, R, T, RC, RE, RH> = {
836836
*/
837837
throttle?: number;
838838
} & RequestHookConfig<S, E, R, T, RC, RE, RH>;
839+
840+
/**
841+
* SSERequest配置
842+
*/
843+
type SSEHookConfig = {
844+
/** 会传给new EventSource */
845+
withCredentials?: boolean;
846+
847+
/** 是否经过alova实例的responded拦截,默认为true */
848+
interceptByGlobalResponded?: boolean;
849+
850+
/** 初始数据 */
851+
initialData?: any;
852+
};
853+
854+
type SSEReturnType<S> = {
855+
readyState: ExportedType<boolean, S>;
856+
data: R;
857+
eventSource: EventSource;
858+
859+
onOpen(callback: (event: AlovaSSEEvent) => void): void;
860+
onMessage<T>(callback: (event: AlovaSSEMessageEvent<T>) => void): void;
861+
onError(callback: (event: AlovaSSEErrorEvent) => void): void;
862+
on(eventName: 'open' | 'message' | 'error', handler: (event: AlovaSSEEvent) => void): () => void;
863+
};
864+
865+
const enum SSEHookReadyState {
866+
CONNECTING = 0,
867+
OPEN = 1,
868+
CLOSED = 2
869+
}
870+
871+
interface AlovaSSEEvent {
872+
method: Method; // alova的method实例
873+
eventSource: EventSource; // eventSource实例
874+
}
875+
interface AlovaSSEErrorEvent extends AlovaSSEEvent {
876+
error: Error; // 错误对象
877+
}
878+
interface AlovaSSEMessageEvent<T> extends AlovaSSEEvent {
879+
data: T; // 每次响应的,经过拦截器转换后的数据
880+
}
881+
type SSEOnOpenTrigger = (event: AlovaSSEEvent) => void;
882+
type SSEOnMessageTrigger<T> = (event: AlovaSSEMessageEvent<T>) => void;
883+
type SSEOnErrorTrigger = (event: AlovaSSEErrorEvent) => void;
884+
type SSEOn = (eventName: 'open' | 'message' | 'error', handler: (event: AlovaSSEEvent) => void) => () => void;
885+
839886
type NotifyHandler = () => void;
840887
type UnbindHandler = () => void;

0 commit comments

Comments
 (0)