放弃Promise用回调整合REST与Websocket的方案是否合理?
问题分析与实操建议
用回调替代Promise封装逻辑的核心问题
- 直接废掉React Query的所有优势:React Query自带的缓存、自动重试、后台刷新、数据过期这些省心功能,全是基于Promise的请求函数实现的。换成回调模式后,这些功能直接失效,你得自己手写缓存、拦截重复请求、同步数据,还要兼顾React和React Native的不同环境,工作量直接翻倍,还容易出bug。
- 状态管理彻底变乱:回调模式下,实时数据更新得手动传递到组件层,要么依赖全局状态管理,要么每个组件自己维护订阅,很容易遗漏订阅销毁逻辑导致内存泄漏,双客户端还要统一处理订阅生命周期,调试难度陡增。
- 代码复用性极差:Services层的回调逻辑会和组件强绑定,不同页面的历史/实时场景要写不同的回调处理,没法像React Query那样用
enabled参数灵活控制开关,双客户端的代码差异也会越来越大。 - 类型安全容易崩盘:原本OpenAPI生成的DTO转换是强类型的Promise链,换成回调后,数据更新的类型检查很容易遗漏,尤其是WebSocket推送格式变化时,很难统一处理错误。
推荐方案:保留React Query,拆分WebSocket与Services层逻辑
方案1:useQuery + 独立WebSocket订阅钩子
- 核心思路:Services层继续返回Promise(负责拉取历史数据),单独编写自定义Hook处理WebSocket订阅,根据页面需求决定是否启用。
- 具体代码示例:
- Services层的
getR保持Promise返回,专注API调用与DTO转换:class RService { async getR(startTimestamp: number): Promise<R[]> { const apiResponse = await openApiClient.getR({ timestampGte: startTimestamp }); return apiResponse.data.map(dto => convertDtoToR(dto)); } } - 自定义Hook管理WebSocket订阅,收到新数据后更新React Query缓存:
function useSubscribeR(enabled: boolean) { const queryClient = useQueryClient(); useEffect(() => { if (!enabled) return; const socket = new WebSocket('ws://你的服务器地址/ws/r'); socket.onmessage = (event) => { const newR = convertWsMessageToR(JSON.parse(event.data)); // 将新数据加入缓存并去重 queryClient.setQueryData(['r'], (oldData: R[] | undefined) => { return oldData ? [newR, ...oldData.filter(item => item.timestamp !== newR.timestamp)] : [newR]; }); }; // 组件卸载时关闭连接 return () => socket.close(); }, [enabled, queryClient]); } - 组件按需组合使用:
- 实时页面:
function RealTimeRPage() { const now = Date.now(); const { data: rList } = useQuery(['r'], () => rService.getR(now - 10 * 60 * 60 * 1000)); useSubscribeR(true); // 启用订阅 return <RList data={rList} />; } - 历史页面:
function HistoricalRPage() { const now = Date.now(); const { data: rList } = useQuery(['r', 'historical'], () => rService.getR(now - 10 * 60 * 60 * 1000)); useSubscribeR(false); // 禁用订阅 return <RList data={rList} />; }
- 实时页面:
- Services层的
方案2:WebSocket消息触发缓存刷新
如果不想直接修改缓存,也可以在收到新数据时触发React Query缓存失效,让它自动重新拉取最新数据(适合更新频率不高的场景):
socket.onmessage = () => { queryClient.invalidateQueries(['r']); };
方案3:封装跨平台实时工具类
针对React和React Native的WebSocket差异(比如RN可能使用第三方库),可以在Services层之上封装跨平台工具类,保证双客户端订阅逻辑一致:
class CrossPlatformWebSocket { static subscribeR(onMessage: (data: R) => void, enabled: boolean) { // 根据平台初始化WebSocket,处理订阅、重连逻辑 // 返回取消订阅的函数 } }
在自定义Hook中调用该类即可实现跨平台兼容。
额外提醒
- DTO转换逻辑单独抽离:把REST和WebSocket消息的DTO转业务对象逻辑写成独立工具函数,避免分散在多处,保证数据格式统一。
- 添加WebSocket重连逻辑:在自定义Hook中加入断开自动重试逻辑,避免实时数据断更。
- 用缓存键区分场景:历史和实时页面使用不同的缓存键(比如
['r', 'historical']和['r']),防止数据互相干扰。
内容的提问来源于stack exchange,提问作者empjon
相关产品推荐
相关产品推荐

