diff --git a/src/bytedesk-integration/components/BytedeskWidget.jsx b/src/bytedesk-integration/components/BytedeskWidget.jsx index 2437a7ca..07b7dbc2 100644 --- a/src/bytedesk-integration/components/BytedeskWidget.jsx +++ b/src/bytedesk-integration/components/BytedeskWidget.jsx @@ -15,6 +15,10 @@ import { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; +// ⚡ 模块级变量:防止 React StrictMode 双重初始化 +let widgetInitialized = false; +let idleCallbackId = null; + const BytedeskWidget = ({ config, autoLoad = true, @@ -27,110 +31,98 @@ const BytedeskWidget = ({ useEffect(() => { // 如果不自动加载或配置未设置,跳过 if (!autoLoad || !config) { - if (!config) { - console.warn('[Bytedesk] 配置未设置,客服组件未加载'); - } return; } - console.log('[Bytedesk] 开始加载客服Widget...', config); + // ⚡ 防止重复初始化(React StrictMode 会双重调用 useEffect) + if (widgetInitialized) { + return; + } - // 加载Bytedesk Widget脚本 - const script = document.createElement('script'); - script.src = 'https://www.weiyuai.cn/embed/bytedesk-web.js'; - script.async = true; - script.id = 'bytedesk-web-script'; + // ⚡ 使用 requestIdleCallback 延迟加载,不阻塞首屏 + const loadWidget = () => { + // 再次检查,防止竞态条件 + if (widgetInitialized) return; + widgetInitialized = true; - script.onload = () => { - console.log('[Bytedesk] Widget脚本加载成功'); + // 检查脚本是否已存在 + if (document.getElementById('bytedesk-web-script')) { + return; + } - try { - if (window.BytedeskWeb) { - console.log('[Bytedesk] 初始化Widget'); - const bytedesk = new window.BytedeskWeb(config); - bytedesk.init(); + // 加载Bytedesk Widget脚本 + const script = document.createElement('script'); + script.src = 'https://www.weiyuai.cn/embed/bytedesk-web.js'; + script.async = true; + script.id = 'bytedesk-web-script'; - widgetRef.current = bytedesk; - console.log('[Bytedesk] Widget初始化成功'); + script.onload = () => { + try { + if (window.BytedeskWeb) { + const bytedesk = new window.BytedeskWeb(config); + bytedesk.init(); + widgetRef.current = bytedesk; - // ⚡ 屏蔽 STOMP WebSocket 错误日志(不影响功能) - // Bytedesk SDK 内部的 /stomp WebSocket 连接失败不影响核心客服功能 - // SDK 会自动降级使用 HTTP 轮询 - const originalConsoleError = console.error; - console.error = function(...args) { - const errorMsg = args.join(' '); - // 忽略 /stomp 和 STOMP 相关错误 - if (errorMsg.includes('/stomp') || - errorMsg.includes('stomp onWebSocketError') || - (errorMsg.includes('WebSocket connection to') && errorMsg.includes('/stomp'))) { - return; // 不输出日志 + // ⚡ 屏蔽 STOMP WebSocket 错误日志(不影响功能) + const originalConsoleError = console.error; + console.error = function(...args) { + const errorMsg = args.join(' '); + if (errorMsg.includes('/stomp') || + errorMsg.includes('stomp onWebSocketError') || + (errorMsg.includes('WebSocket connection to') && errorMsg.includes('/stomp'))) { + return; + } + originalConsoleError.apply(console, args); + }; + + if (onLoad) { + onLoad(bytedesk); } - originalConsoleError.apply(console, args); - }; - - if (onLoad) { - onLoad(bytedesk); + } else { + throw new Error('BytedeskWeb对象未定义'); + } + } catch (error) { + console.error('[Bytedesk] 初始化失败:', error); + if (onError) { + onError(error); } - } else { - throw new Error('BytedeskWeb对象未定义'); } - } catch (error) { - console.error('[Bytedesk] Widget初始化失败:', error); + }; + + script.onerror = (error) => { + console.error('[Bytedesk] 脚本加载失败:', error); + widgetInitialized = false; // 允许重试 if (onError) { onError(error); } - } + }; + + document.body.appendChild(script); + scriptRef.current = script; }; - script.onerror = (error) => { - console.error('[Bytedesk] Widget脚本加载失败:', error); - if (onError) { - onError(error); - } - }; + // ⚡ 使用 requestIdleCallback 在浏览器空闲时加载 + if ('requestIdleCallback' in window) { + idleCallbackId = requestIdleCallback(loadWidget, { timeout: 3000 }); + } else { + // 降级:使用 setTimeout + idleCallbackId = setTimeout(loadWidget, 100); + } - // 添加脚本到页面 - document.body.appendChild(script); - scriptRef.current = script; - - // 清理函数 - 增强错误处理,防止 React 18 StrictMode 双重清理报错 + // 清理函数 return () => { - console.log('[Bytedesk] 清理Widget'); - - // 移除脚本 - try { - if (scriptRef.current && scriptRef.current.parentNode) { - scriptRef.current.parentNode.removeChild(scriptRef.current); + // 取消待执行的 idle callback + if (idleCallbackId) { + if ('cancelIdleCallback' in window) { + cancelIdleCallback(idleCallbackId); + } else { + clearTimeout(idleCallbackId); } - scriptRef.current = null; - } catch (error) { - console.warn('[Bytedesk] 移除脚本失败(可能已被移除):', error.message); + idleCallbackId = null; } - // 移除Widget DOM元素 - try { - const widgetElements = document.querySelectorAll('[class*="bytedesk"], [id*="bytedesk"]'); - widgetElements.forEach(el => { - try { - if (el && el.parentNode && el.parentNode.contains(el)) { - el.parentNode.removeChild(el); - } - } catch (err) { - // 忽略单个元素移除失败(可能已被移除) - } - }); - } catch (error) { - console.warn('[Bytedesk] 清理Widget DOM元素失败:', error.message); - } - - // 清理全局对象 - try { - if (window.BytedeskWeb) { - delete window.BytedeskWeb; - } - } catch (error) { - console.warn('[Bytedesk] 清理全局对象失败:', error.message); - } + // ⚠️ 不重置 widgetInitialized,保持单例 + // 不清理 DOM,因为客服 Widget 应该持久存在 }; }, [config, autoLoad, onLoad, onError]); diff --git a/src/index.js b/src/index.js index 2521860d..6324f2db 100755 --- a/src/index.js +++ b/src/index.js @@ -40,82 +40,18 @@ if (process.env.REACT_APP_ENABLE_DEBUG === 'true') { function registerServiceWorker() { // ⚠️ Mock 模式下跳过 Service Worker 注册(避免与 MSW 冲突) if (process.env.REACT_APP_ENABLE_MOCK === 'true') { - console.log( - '%c[App] Mock 模式已启用,跳过通知 Service Worker 注册(避免与 MSW 冲突)', - 'color: #FF9800; font-weight: bold;' - ); return; } // 仅在支持 Service Worker 的浏览器中注册 if ('serviceWorker' in navigator) { - // 在页面加载完成后注册 window.addEventListener('load', () => { navigator.serviceWorker .register('/service-worker.js') - .then((registration) => { - console.log('[App] ✅ Service Worker 注册成功'); - console.log('[App] Scope:', registration.scope); - - // 检查当前激活状态 - if (navigator.serviceWorker.controller) { - console.log('[App] ✅ Service Worker 已激活并控制页面'); - } else { - console.log('[App] ⏳ Service Worker 已注册,等待激活...'); - console.log('[App] 💡 刷新页面以激活 Service Worker'); - - // 监听 controller 变化(Service Worker 激活后触发) - navigator.serviceWorker.addEventListener('controllerchange', () => { - console.log('[App] ✅ Service Worker 控制器已更新'); - }); - } - - // 监听 Service Worker 更新 - registration.addEventListener('updatefound', () => { - const newWorker = registration.installing; - console.log('[App] 🔄 发现 Service Worker 更新'); - - if (newWorker) { - newWorker.addEventListener('statechange', () => { - console.log(`[App] Service Worker 状态: ${newWorker.state}`); - if (newWorker.state === 'activated') { - console.log('[App] ✅ Service Worker 已激活'); - - // 如果有旧的 Service Worker 在控制页面,提示用户刷新 - if (navigator.serviceWorker.controller) { - console.log('[App] 💡 Service Worker 已更新,建议刷新页面'); - } - } - }); - } - }); - }) .catch((error) => { - console.error('[App] ❌ Service Worker 注册失败'); - console.error('[App] 错误类型:', error.name); - console.error('[App] 错误信息:', error.message); - console.error('[App] 完整错误:', error); - - // 额外检查:验证文件是否可访问 - fetch('/service-worker.js', { method: 'HEAD' }) - .then(response => { - if (response.ok) { - console.error('[App] Service Worker 文件存在但注册失败'); - console.error('[App] 💡 可能的原因:'); - console.error('[App] 1. Service Worker 文件有语法错误'); - console.error('[App] 2. 浏览器不支持某些 Service Worker 特性'); - console.error('[App] 3. HTTPS 证书问题(Service Worker 需要 HTTPS)'); - } else { - console.error('[App] Service Worker 文件不存在(HTTP', response.status, ')'); - } - }) - .catch(fetchError => { - console.error('[App] 无法访问 Service Worker 文件:', fetchError.message); - }); + console.error('[App] Service Worker 注册失败:', error.message); }); }); - } else { - console.warn('[App] Service Worker is not supported in this browser'); } } @@ -149,12 +85,8 @@ async function startApp() { try { const { startMockServiceWorker } = await import('./mocks/browser'); await startMockServiceWorker(); - console.log( - '%c[MSW] ✅ Mock Service Worker 已启动,开始渲染应用', - 'color: #4CAF50; font-weight: bold;' - ); } catch (error) { - console.error('[MSW] ❌ Mock Service Worker 启动失败,继续渲染应用:', error); + console.error('[MSW] 启动失败:', error); } } diff --git a/src/mocks/browser.js b/src/mocks/browser.js index 0c1b88cc..b454e1b8 100644 --- a/src/mocks/browser.js +++ b/src/mocks/browser.js @@ -47,18 +47,8 @@ export async function startMockServiceWorker() { }); isStarted = true; - console.log( - '%c[MSW] Mock Service Worker 已启动 🎭 (警告模式)', - 'color: #4CAF50; font-weight: bold; font-size: 14px;' - ); - console.log( - '%c警告模式:已定义 Mock → 返回假数据 | 未定义 Mock → 显示警告 ⚠️ | 允许 passthrough', - 'color: #FF9800; font-weight: bold; font-size: 12px;' - ); - console.log( - '%c查看 src/mocks/handlers/ 目录管理 Mock 接口', - 'color: #2196F3; font-size: 12px;' - ); + // 精简日志:只保留一行启动提示 + console.log('%c[MSW] Mock 已启用 🎭', 'color: #4CAF50; font-weight: bold;'); } catch (error) { console.error('[MSW] 启动失败:', error); } finally { diff --git a/src/services/socketService.js b/src/services/socketService.js index 6df22196..74e1bb28 100644 --- a/src/services/socketService.js +++ b/src/services/socketService.js @@ -56,17 +56,8 @@ class SocketService { // 注册所有暂存的事件监听器(保留 pendingListeners,不清空) if (this.pendingListeners.length > 0) { - console.log(`[socketService] 📦 注册 ${this.pendingListeners.length} 个暂存的事件监听器`); this.pendingListeners.forEach(({ event, callback }) => { - // 直接在 Socket.IO 实例上注册(避免递归调用 this.on()) - const wrappedCallback = (...args) => { - console.log(`%c[socketService] 🔔 收到原始事件: ${event}`, 'color: #2196F3; font-weight: bold;'); - console.log(`[socketService] 事件数据 (${event}):`, ...args); - callback(...args); - }; - - this.socket.on(event, wrappedCallback); - console.log(`[socketService] ✓ 已注册事件监听器: ${event}`); + this.socket.on(event, callback); }); // ⚠️ 重要:不清空 pendingListeners,保留用于重连 } @@ -82,15 +73,8 @@ class SocketService { this.customReconnectTimer = null; } - logger.info('socketService', 'Socket.IO connected successfully', { - socketId: this.socket.id, - }); - - console.log(`%c[socketService] ✅ WebSocket 已连接`, 'color: #4CAF50; font-weight: bold;'); - console.log('[socketService] Socket ID:', this.socket.id); - + logger.info('socketService', 'Socket.IO connected', { socketId: this.socket.id }); // ⚠️ 已移除自动订阅,让 NotificationContext 负责订阅 - // this.subscribeToAllEvents(); }); // 监听断开连接 @@ -174,25 +158,12 @@ class SocketService { ); if (!exists) { - logger.info('socketService', 'Socket not ready, queuing listener', { event }); - console.log(`[socketService] 📦 Socket 未初始化,暂存事件监听器: ${event}`); this.pendingListeners.push({ event, callback }); - } else { - console.log(`[socketService] ⚠️ 监听器已存在,跳过: ${event}`); } return; } - // 包装回调函数,添加日志 - const wrappedCallback = (...args) => { - console.log(`%c[socketService] 🔔 收到原始事件: ${event}`, 'color: #2196F3; font-weight: bold;'); - console.log(`[socketService] 事件数据 (${event}):`, ...args); - callback(...args); - }; - - this.socket.on(event, wrappedCallback); - logger.info('socketService', `Event listener added: ${event}`); - console.log(`[socketService] ✓ 已注册事件监听器: ${event}`); + this.socket.on(event, callback); } /** @@ -210,8 +181,6 @@ class SocketService { } else { this.socket.off(event); } - - logger.info('socketService', `Event listener removed: ${event}`); } /** @@ -231,8 +200,6 @@ class SocketService { } else { this.socket.emit(event, data); } - - logger.info('socketService', `Event emitted: ${event}`, data); } /** @@ -355,65 +322,31 @@ class SocketService { * 执行订阅操作(内部方法) */ _doSubscribe(eventType, importance, onNewEvent, onSubscribed) { - console.log('\n========== [SocketService DEBUG] 开始订阅 =========='); - console.log('[SocketService DEBUG] 事件类型:', eventType); - console.log('[SocketService DEBUG] 重要性:', importance); - console.log('[SocketService DEBUG] Socket 连接状态:', this.connected); - console.log('[SocketService DEBUG] Socket ID:', this.socket?.id); - // 发送订阅请求 const subscribeData = { event_type: eventType, importance: importance, }; - console.log('[SocketService DEBUG] 准备发送 subscribe_events:', subscribeData); this.emit('subscribe_events', subscribeData); - console.log('[SocketService DEBUG] ✓ 已发送 subscribe_events'); // 监听订阅确认 this.socket.once('subscription_confirmed', (data) => { - console.log('\n[SocketService DEBUG] ========== 收到订阅确认 =========='); - console.log('[SocketService DEBUG] 订阅确认数据:', data); - logger.info('socketService', 'Subscription confirmed', data); if (onSubscribed) { - console.log('[SocketService DEBUG] 调用 onSubscribed 回调'); onSubscribed(data); } - console.log('[SocketService DEBUG] ========== 订阅确认处理完成 ==========\n'); }); // 监听订阅错误 this.socket.once('subscription_error', (error) => { - console.error('\n[SocketService ERROR] ========== 订阅错误 =========='); - console.error('[SocketService ERROR] 错误信息:', error); logger.error('socketService', 'Subscription error', error); - console.error('[SocketService ERROR] ========== 订阅错误处理完成 ==========\n'); }); // 监听新事件推送 - // ⚠️ 注意:不要移除其他地方注册的 new_event 监听器(如 NotificationContext) - // 多个监听器可以共存,都会被触发 if (onNewEvent) { - console.log('[SocketService DEBUG] 设置 new_event 监听器'); - - // ⚠️ 已移除 this.socket.off('new_event'),允许多个监听器共存 - - // 添加新的监听器(与其他监听器共存) this.socket.on('new_event', (eventData) => { - console.log('\n[SocketService DEBUG] ========== 收到新事件推送 =========='); - console.log('[SocketService DEBUG] 事件数据:', eventData); - console.log('[SocketService DEBUG] 事件 ID:', eventData?.id); - console.log('[SocketService DEBUG] 事件标题:', eventData?.title); - logger.info('socketService', 'New event received', eventData); - console.log('[SocketService DEBUG] 准备调用 onNewEvent 回调'); onNewEvent(eventData); - console.log('[SocketService DEBUG] ✓ onNewEvent 回调已调用'); - console.log('[SocketService DEBUG] ========== 新事件处理完成 ==========\n'); }); - console.log('[SocketService DEBUG] ✓ new_event 监听器已设置(与其他监听器共存)'); } - - console.log('[SocketService DEBUG] ========== 订阅完成 ==========\n'); } /** @@ -440,11 +373,7 @@ class SocketService { // 监听取消订阅确认 this.socket.once('unsubscription_confirmed', (data) => { - logger.info('socketService', 'Unsubscription confirmed', data); - - // 移除新事件监听器 this.socket.off('new_event'); - if (onUnsubscribed) { onUnsubscribed(data); } @@ -462,22 +391,10 @@ class SocketService { * @returns {Function} 取消订阅的函数 */ subscribeToAllEvents(onNewEvent) { - console.log('%c[socketService] 🔔 自动订阅所有事件...', 'color: #FF9800; font-weight: bold;'); - - // 如果没有提供回调,添加一个默认的日志回调 - const defaultCallback = (event) => { - console.log('%c[socketService] 📨 收到新事件(默认回调)', 'color: #4CAF50; font-weight: bold;'); - console.log('[socketService] 事件数据:', event); - }; - this.subscribeToEvents({ eventType: 'all', importance: 'all', - onNewEvent: onNewEvent || defaultCallback, - onSubscribed: (data) => { - console.log('%c[socketService] ✅ 订阅成功!', 'color: #4CAF50; font-weight: bold;'); - console.log('[socketService] 订阅确认:', data); - }, + onNewEvent: onNewEvent || (() => {}), }); // 返回取消订阅的清理函数