diff --git a/src/contexts/NotificationContext.js b/src/contexts/NotificationContext.js index dd6ea424..0058c104 100644 --- a/src/contexts/NotificationContext.js +++ b/src/contexts/NotificationContext.js @@ -649,183 +649,213 @@ export const NotificationProvider = ({ children }) => { }, [adaptEventToNotification]); - // ========== 连接到 Socket 服务(⚡ 方案2: 只执行一次) ========== + // ========== 连接到 Socket 服务(⚡ 异步初始化,不阻塞首屏) ========== useEffect(() => { - logger.info('NotificationContext', '初始化 Socket 连接(方案2:只注册一次)'); + let cleanupCalled = false; + let idleCallbackId; + let timeoutId; - // ========== 监听连接成功(首次连接 + 重连) ========== - socket.on('connect', () => { - setIsConnected(true); - setReconnectAttempt(0); + // ⚡ Socket 初始化函数(将在浏览器空闲时执行) + const initSocketConnection = () => { + if (cleanupCalled) return; // 防止组件卸载后执行 - // 判断是首次连接还是重连 - if (isFirstConnect.current) { - logger.info('NotificationContext', '首次连接成功', { - socketId: socket.getSocketId?.() - }); - setConnectionStatus(CONNECTION_STATUS.CONNECTED); - isFirstConnect.current = false; - } else { - logger.info('NotificationContext', '重连成功'); - setConnectionStatus(CONNECTION_STATUS.RECONNECTED); + logger.info('NotificationContext', '初始化 Socket 连接(异步执行,不阻塞首屏)'); - // 清除之前的定时器 - if (reconnectedTimerRef.current) { - clearTimeout(reconnectedTimerRef.current); + // ========== 监听连接成功(首次连接 + 重连) ========== + socket.on('connect', () => { + setIsConnected(true); + setReconnectAttempt(0); + + // 判断是首次连接还是重连 + if (isFirstConnect.current) { + logger.info('NotificationContext', '首次连接成功', { + socketId: socket.getSocketId?.() + }); + setConnectionStatus(CONNECTION_STATUS.CONNECTED); + isFirstConnect.current = false; + } else { + logger.info('NotificationContext', '重连成功'); + setConnectionStatus(CONNECTION_STATUS.RECONNECTED); + + // 清除之前的定时器 + if (reconnectedTimerRef.current) { + clearTimeout(reconnectedTimerRef.current); + } + + // 2秒后自动变回 CONNECTED + reconnectedTimerRef.current = setTimeout(() => { + setConnectionStatus(CONNECTION_STATUS.CONNECTED); + logger.info('NotificationContext', 'Auto-dismissed RECONNECTED status'); + }, 2000); } - // 2秒后自动变回 CONNECTED - reconnectedTimerRef.current = setTimeout(() => { - setConnectionStatus(CONNECTION_STATUS.CONNECTED); - logger.info('NotificationContext', 'Auto-dismissed RECONNECTED status'); - }, 2000); - } + // ⚡ 重连后只需重新订阅,不需要重新注册监听器 + logger.info('NotificationContext', '重新订阅事件推送'); - // ⚡ 重连后只需重新订阅,不需要重新注册监听器 - logger.info('NotificationContext', '重新订阅事件推送'); - - if (socket.subscribeToEvents) { - socket.subscribeToEvents({ - eventType: 'all', - importance: 'all', - onSubscribed: (data) => { - logger.info('NotificationContext', '订阅成功', data); - }, - }); - } else { - logger.error('NotificationContext', 'socket.subscribeToEvents 方法不可用'); - } - }); - - // ========== 监听断开连接 ========== - socket.on('disconnect', (reason) => { - setIsConnected(false); - setConnectionStatus(CONNECTION_STATUS.DISCONNECTED); - logger.warn('NotificationContext', 'Socket 已断开', { reason }); - }); - - // ========== 监听连接错误 ========== - socket.on('connect_error', (error) => { - logger.error('NotificationContext', 'Socket connect_error', error); - setConnectionStatus(CONNECTION_STATUS.RECONNECTING); - - const attempts = socket.getReconnectAttempts?.() || 0; - setReconnectAttempt(attempts); - logger.info('NotificationContext', `重连中... (第 ${attempts} 次尝试)`); - }); - - // ========== 监听重连失败 ========== - socket.on('reconnect_failed', () => { - logger.error('NotificationContext', '重连失败'); - setConnectionStatus(CONNECTION_STATUS.FAILED); - - toast({ - title: '连接失败', - description: '无法连接到服务器,请检查网络连接', - status: 'error', - duration: null, - isClosable: true, + if (socket.subscribeToEvents) { + socket.subscribeToEvents({ + eventType: 'all', + importance: 'all', + onSubscribed: (data) => { + logger.info('NotificationContext', '订阅成功', data); + }, + }); + } else { + logger.error('NotificationContext', 'socket.subscribeToEvents 方法不可用'); + } }); - }); - // ========== 监听新事件推送(⚡ 只注册一次,使用 ref 访问最新函数) ========== - socket.on('new_event', (data) => { - logger.info('NotificationContext', '收到 new_event 事件', { - id: data?.id, - title: data?.title, - eventType: data?.event_type || data?.type, - importance: data?.importance + // ========== 监听断开连接 ========== + socket.on('disconnect', (reason) => { + setIsConnected(false); + setConnectionStatus(CONNECTION_STATUS.DISCONNECTED); + logger.warn('NotificationContext', 'Socket 已断开', { reason }); }); - logger.debug('NotificationContext', '原始事件数据', data); - // ⚠️ 防御性检查:确保 ref 已初始化 - if (!addNotificationRef.current || !adaptEventToNotificationRef.current) { - logger.error('NotificationContext', 'Ref 未初始化,跳过处理', { - addNotificationRef: !!addNotificationRef.current, - adaptEventToNotificationRef: !!adaptEventToNotificationRef.current, + // ========== 监听连接错误 ========== + socket.on('connect_error', (error) => { + logger.error('NotificationContext', 'Socket connect_error', error); + setConnectionStatus(CONNECTION_STATUS.RECONNECTING); + + const attempts = socket.getReconnectAttempts?.() || 0; + setReconnectAttempt(attempts); + logger.info('NotificationContext', `重连中... (第 ${attempts} 次尝试)`); + }); + + // ========== 监听重连失败 ========== + socket.on('reconnect_failed', () => { + logger.error('NotificationContext', '重连失败'); + setConnectionStatus(CONNECTION_STATUS.FAILED); + + toast({ + title: '连接失败', + description: '无法连接到服务器,请检查网络连接', + status: 'error', + duration: null, + isClosable: true, }); - return; - } + }); - // ========== Socket层去重检查 ========== - const eventId = data.id || `${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - - if (!data.id) { - logger.warn('NotificationContext', 'Event missing ID, generated fallback', { - eventId, - eventType: data.type, - title: data.title, + // ========== 监听新事件推送(⚡ 只注册一次,使用 ref 访问最新函数) ========== + socket.on('new_event', (data) => { + logger.info('NotificationContext', '收到 new_event 事件', { + id: data?.id, + title: data?.title, + eventType: data?.event_type || data?.type, + importance: data?.importance }); - } + logger.debug('NotificationContext', '原始事件数据', data); - if (processedEventIds.current.has(eventId)) { - logger.warn('NotificationContext', '重复事件已忽略', { eventId }); - return; - } + // ⚠️ 防御性检查:确保 ref 已初始化 + if (!addNotificationRef.current || !adaptEventToNotificationRef.current) { + logger.error('NotificationContext', 'Ref 未初始化,跳过处理', { + addNotificationRef: !!addNotificationRef.current, + adaptEventToNotificationRef: !!adaptEventToNotificationRef.current, + }); + return; + } - processedEventIds.current.add(eventId); - logger.debug('NotificationContext', '事件已记录,防止重复处理', { eventId }); + // ========== Socket层去重检查 ========== + const eventId = data.id || `${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - // 限制 Set 大小,避免内存泄漏 - if (processedEventIds.current.size > MAX_PROCESSED_IDS) { - const idsArray = Array.from(processedEventIds.current); - processedEventIds.current = new Set(idsArray.slice(-MAX_PROCESSED_IDS)); - logger.debug('NotificationContext', 'Cleaned up old processed event IDs', { - kept: MAX_PROCESSED_IDS, - }); - } - // ========== Socket层去重检查结束 ========== + if (!data.id) { + logger.warn('NotificationContext', 'Event missing ID, generated fallback', { + eventId, + eventType: data.type, + title: data.title, + }); + } - // ✅ 使用 ref.current 访问最新的适配器函数(避免闭包陷阱) - logger.debug('NotificationContext', '正在转换事件格式'); - const notification = adaptEventToNotificationRef.current(data); - logger.debug('NotificationContext', '转换后的通知对象', notification); + if (processedEventIds.current.has(eventId)) { + logger.warn('NotificationContext', '重复事件已忽略', { eventId }); + return; + } - // ✅ 使用 ref.current 访问最新的 addNotification 函数 - logger.debug('NotificationContext', '准备添加通知到队列'); - addNotificationRef.current(notification); - logger.info('NotificationContext', '通知已添加到队列'); + processedEventIds.current.add(eventId); + logger.debug('NotificationContext', '事件已记录,防止重复处理', { eventId }); - // ⚡ 调用所有注册的事件更新回调(用于通知其他组件刷新数据) - if (eventUpdateCallbacks.current.size > 0) { - logger.debug('NotificationContext', `触发 ${eventUpdateCallbacks.current.size} 个事件更新回调`); - eventUpdateCallbacks.current.forEach(callback => { - try { - callback(data); - } catch (error) { - logger.error('NotificationContext', '事件更新回调执行失败', error); - } - }); - logger.debug('NotificationContext', '所有事件更新回调已触发'); - } - }); + // 限制 Set 大小,避免内存泄漏 + if (processedEventIds.current.size > MAX_PROCESSED_IDS) { + const idsArray = Array.from(processedEventIds.current); + processedEventIds.current = new Set(idsArray.slice(-MAX_PROCESSED_IDS)); + logger.debug('NotificationContext', 'Cleaned up old processed event IDs', { + kept: MAX_PROCESSED_IDS, + }); + } + // ========== Socket层去重检查结束 ========== - // ========== 监听系统通知(兼容性) ========== - socket.on('system_notification', (data) => { - logger.info('NotificationContext', '收到系统通知', data); + // ✅ 使用 ref.current 访问最新的适配器函数(避免闭包陷阱) + logger.debug('NotificationContext', '正在转换事件格式'); + const notification = adaptEventToNotificationRef.current(data); + logger.debug('NotificationContext', '转换后的通知对象', notification); - if (addNotificationRef.current) { - addNotificationRef.current(data); - } else { - logger.error('NotificationContext', 'addNotificationRef 未初始化'); - } - }); + // ✅ 使用 ref.current 访问最新的 addNotification 函数 + logger.debug('NotificationContext', '准备添加通知到队列'); + addNotificationRef.current(notification); + logger.info('NotificationContext', '通知已添加到队列'); - logger.info('NotificationContext', '所有监听器已注册(只注册一次)'); + // ⚡ 调用所有注册的事件更新回调(用于通知其他组件刷新数据) + if (eventUpdateCallbacks.current.size > 0) { + logger.debug('NotificationContext', `触发 ${eventUpdateCallbacks.current.size} 个事件更新回调`); + eventUpdateCallbacks.current.forEach(callback => { + try { + callback(data); + } catch (error) { + logger.error('NotificationContext', '事件更新回调执行失败', error); + } + }); + logger.debug('NotificationContext', '所有事件更新回调已触发'); + } + }); - // ========== 获取最大重连次数 ========== - const maxAttempts = socket.getMaxReconnectAttempts?.() || Infinity; - setMaxReconnectAttempts(maxAttempts); - logger.info('NotificationContext', 'Max reconnect attempts', { maxAttempts }); + // ========== 监听系统通知(兼容性) ========== + socket.on('system_notification', (data) => { + logger.info('NotificationContext', '收到系统通知', data); - // ========== 启动连接 ========== - logger.info('NotificationContext', '调用 socket.connect()'); - socket.connect(); + if (addNotificationRef.current) { + addNotificationRef.current(data); + } else { + logger.error('NotificationContext', 'addNotificationRef 未初始化'); + } + }); + + logger.info('NotificationContext', '所有监听器已注册'); + + // ========== 获取最大重连次数 ========== + const maxAttempts = socket.getMaxReconnectAttempts?.() || Infinity; + setMaxReconnectAttempts(maxAttempts); + logger.info('NotificationContext', 'Max reconnect attempts', { maxAttempts }); + + // ========== 启动连接 ========== + logger.info('NotificationContext', '调用 socket.connect()'); + socket.connect(); + }; + + // ⚡ 使用 requestIdleCallback 在浏览器空闲时初始化 Socket + // 降级到 setTimeout(0) 以兼容不支持的浏览器(如 Safari) + if ('requestIdleCallback' in window) { + idleCallbackId = window.requestIdleCallback(initSocketConnection, { + timeout: 3000 // 最多等待 3 秒,确保连接不会延迟太久 + }); + logger.debug('NotificationContext', 'Socket 初始化已排入 requestIdleCallback'); + } else { + timeoutId = setTimeout(initSocketConnection, 0); + logger.debug('NotificationContext', 'Socket 初始化已排入 setTimeout(0)(降级模式)'); + } // ========== 清理函数(组件卸载时) ========== return () => { + cleanupCalled = true; logger.info('NotificationContext', '清理 Socket 连接'); + // 取消待执行的初始化 + if (idleCallbackId && 'cancelIdleCallback' in window) { + window.cancelIdleCallback(idleCallbackId); + } + if (timeoutId) { + clearTimeout(timeoutId); + } + // 清理 reconnected 状态定时器 if (reconnectedTimerRef.current) { clearTimeout(reconnectedTimerRef.current); diff --git a/src/services/socket/index.js b/src/services/socket/index.js index 11586e78..37310632 100644 --- a/src/services/socket/index.js +++ b/src/services/socket/index.js @@ -10,25 +10,12 @@ import { socketService } from '../socketService'; export const socket = socketService; export { socketService }; -// ⚡ 新增:暴露 Socket 实例到 window(用于调试和验证) +// ⚡ 暴露 Socket 实例到 window(用于调试和验证) +// 注意:移除首屏加载时的日志,避免阻塞感知 if (typeof window !== 'undefined') { window.socket = socketService; window.socketService = socketService; - - console.log( - '%c[Socket Service] ✅ Socket instance exposed to window', - 'color: #4CAF50; font-weight: bold; font-size: 14px;' - ); - console.log(' 📍 window.socket:', window.socket); - console.log(' 📍 window.socketService:', window.socketService); - console.log(' 📍 Socket.IO instance:', window.socket?.socket); - console.log(' 📍 Connection status:', window.socket?.connected ? '✅ Connected' : '❌ Disconnected'); + // 日志已移除,如需调试可在控制台执行: console.log(window.socket) } -// 打印当前使用的服务类型 -console.log( - '%c[Socket Service] Using REAL Socket Service', - 'color: #4CAF50; font-weight: bold; font-size: 12px;' -); - export default socket;