From 388cf75633e0f163dc651d697e851c3a4d434a71 Mon Sep 17 00:00:00 2001 From: jmal Date: Fri, 13 Jun 2025 11:26:25 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=20SSEClient=20?= =?UTF-8?q?=E4=BB=A5=E5=AE=9E=E7=8E=B0=E8=87=AA=E5=8A=A8=E9=87=8D=E8=BF=9E?= =?UTF-8?q?=E5=92=8C=E6=9B=B4=E5=A5=BD=E7=9A=84=E9=94=99=E8=AF=AF=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/sse/SSEClient.js | 164 ++++++++++++++++++++++++++------------ src/store/modules/user.js | 2 + 2 files changed, 116 insertions(+), 50 deletions(-) diff --git a/src/sse/SSEClient.js b/src/sse/SSEClient.js index 23f22c9..eb2cf08 100644 --- a/src/sse/SSEClient.js +++ b/src/sse/SSEClient.js @@ -1,70 +1,138 @@ import Bus from "@/assets/js/bus"; -import _ from "lodash"; import store from "@/store"; import { generateUUID } from "ant-design-vue/lib/vc-select/util"; let eventSource = null; -let timer = null; -let lastHeartbeat = new Date().getTime(); +let reconnectTimer = null; // 新增一个我们自己控制的重连计时器 +const RECONNECT_INTERVAL = 5000; // 每 5 秒尝试一次 const uuid = generateUUID(); -const throttledConnectSSE = _.throttle((username) => { - if (typeof EventSource !== 'undefined') { - // 判断cookie中是否有token - if (store.getters.name) { - connectToSSE(username); - } - } else { - console.error('EventSource is not supported by the browser'); +/** + * 处理从服务器接收到的业务消息 + * @param {MessageEvent} event - SSE 消息事件 + */ +function handleBusinessMessage(event) { + try { + const msg = JSON.parse(event.data); + onMessage(msg); + } catch (e) { + console.error('Failed to parse message data:', event.data, e); } -}, 3000); +} +/** + * EventSource 错误处理器 + * 当连接丢失或无法建立时触发 + */ +function handleError() { + // 当 readyState 变为 CLOSED 时,意味着原生重连已放弃 + // 这通常是因为服务器返回了非 200 状态码 + if (eventSource && eventSource.readyState === EventSource.CLOSED) { + console.warn('EventSource connection closed permanently. Attempting to restart after a delay.'); + // 启动我们自己的重连逻辑 + startManualReconnect(); + } +} + +/** + * 启动手动重连计时器 + */ +function startManualReconnect() { + // 防止重复启动计时器 + if (reconnectTimer) return; + + // 先关闭可能存在的旧的、已死掉的 eventSource 实例 + if (eventSource) { + eventSource.close(); + eventSource = null; + } + + reconnectTimer = setTimeout(() => { + console.log('Attempting to manually reconnect SSE...'); + // 从 store 或其他地方获取当前用户名 + const username = store.getters.name; + if (username) { + connectToSSE(username); + } else { + console.error("Cannot reconnect: username not found."); + } + }, RECONNECT_INTERVAL); +} + +/** + * EventSource 连接成功打开时的处理器 + */ +function handleOpen() { + //console.log('EventSource connection established successfully.'); + // 连接成功,可以更新 UI 状态 + //store.dispatch('updateConnectionStatus', 'connected'); +} + +/** + * 关闭当前的 SSE 连接 + */ +function closeConnection() { + if (eventSource) { + eventSource.close(); // 这会阻止 EventSource 自动重连 + eventSource = null; + } +} + +/** + * 连接到 SSE 服务 + * @param {string} username - 当前登录的用户名 + */ function connectToSSE(username) { - if (eventSource) { - // 移除之前的事件监听器 - eventSource.removeEventListener('message', handleMessage); - eventSource.removeEventListener('error', handleError.bind(null, username)); + // 如果已存在连接,先断开旧的,以防用户切换等场景 + terminateSSE() + + if (typeof EventSource === 'undefined') { + console.error('EventSource is not supported by your browser.'); + return; } const url = `/api/events?username=${username}&uuid=${uuid}`; - eventSource = new EventSource(url); - eventSource.addEventListener('message', handleMessage); - eventSource.addEventListener('error', handleError.bind(null, username)); - startHeartbeatCheck(username); + try { + eventSource = new EventSource(url); + + // 1. 监听连接打开 + eventSource.onopen = handleOpen; + + // 2. 监听业务消息(这是默认事件) + // 服务器发送的不带 "event:" 字段的数据会触发此监听器 + // 服务器的心跳(无论是注释行还是自定义事件名)都不会触发它 + eventSource.onmessage = handleBusinessMessage; + + // 3. 监听错误(这是自动重连的关键) + eventSource.onerror = handleError; + + } catch (e) { + console.error('Failed to create EventSource:', e); + } } -function handleMessage(event) { - if (event.data === 'h') { - lastHeartbeat = new Date().getTime(); +/** + * 初始化 SSE 连接的入口函数 + * @param {string} username - 当前登录的用户名 + */ +export function initiateSSE(username) { + // 确保有用户名(即用户已登录) + if (username) { + connectToSSE(username); } else { - const msg = JSON.parse(event.data); - onMessage(msg); + console.warn('Cannot initiate SSE connection without a username.'); } } -function handleError(username) { - eventSource.close(); - throttledConnectSSE(username); -} - -function clearExistingTimer() { - if (timer) { - clearInterval(timer); - } -} - -function startHeartbeatCheck(username) { - clearExistingTimer(); - timer = setInterval(() => { - if (eventSource.readyState === 1 && new Date().getTime() - lastHeartbeat > 5000) { - eventSource.close(); - } - if (eventSource.readyState === 2) { - throttledConnectSSE(username); - } - }, 3000); +/** + * 组件卸载或用户登出时,应调用此函数清理连接 + */ +export function terminateSSE() { + clearTimeout(reconnectTimer); + reconnectTimer = null; + closeConnection(); } function onMessage(msg) { @@ -125,7 +193,3 @@ function handleFileChangeMessage(msg, url, body) { } -export function initiateSSE(username) { - throttledConnectSSE(username); -} - diff --git a/src/store/modules/user.js b/src/store/modules/user.js index 0d33ebb..49eedb0 100644 --- a/src/store/modules/user.js +++ b/src/store/modules/user.js @@ -6,6 +6,7 @@ import { resetRouter } from '@/router' import router from '@/router' import Layout from '@/layout' import ParentView from '@/components/ParentView' +import { terminateSSE } from '@/sse/SSEClient' const setState = (state) => { localStorage.setItem('store', JSON.stringify(state)) @@ -197,6 +198,7 @@ const actions = { localStorage.removeItem('app') resetRouter() commit('RESET_STATE') + terminateSSE() resolve() }).catch(error => { reject(error)