52-useWebSocket:自动重连与消息分发
实时待办推送的前端侧------112行的composable管连接/重连/消息分发/生命周期。退避重连(3秒起步30秒封顶)、handler异常隔离、组件卸载自动断开。这是第43篇推送链路的浏览器终点。
文章目录
源码:
browise-vue/src/core/useWebSocket.ts(112行)消费方:MessageList.vue(消息中心)、HomeDemo.vue(全局挂载)
服务端:NotificationWebSocketHandler(第43篇)
一、112行的API面
typescript
const ws = useWebSocket(userId)
ws.connect() // 建连
ws.onMessage((data) => { ... }) // 订阅消息
ws.disconnect() // 主动断开(不重连)
// 响应式状态
ws.connected // ref<boolean>------连接状态(可绑定UI小圆点)
ws.lastMessage // ref<WsMessage|null>------最新消息
消息契约 ------{type:'workflow_message', userId, procInstId, msg, timestamp}------type字段区分消息类别(workflow_message/notice_published)------一个连接多类消息靠type路由。
二、URL构造的两个细节
typescript
function getUrl(): string {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:' // ①协议跟随
const uid = userId || JSON.parse(localStorage.getItem('browise_user_info')).psnId
return `${proto}//${location.hostname}:8080/ws/notification?userId=${uid}`
}
①wss/ws跟随页面协议 ------https页面里开ws://会被浏览器拦(mixed content)------协议必须自适应。
②userId的两级来源 ------参数显式传 > localStorage的用户信息------组件可以不传userId (挂载时自动从登录态取)。userId在query string不在header------WebSocket浏览器API不能自定义Header------后端Handler从query解析(这是WebSocket鉴权的通行妥协------query会被日志记录,生产环境该升级为连接后首条消息鉴权)。
硬编码的:8080 ------开发环境直连后端端口。生产(同域)会出问题 ------location.hostname:8080在生产可能不通------该用location.host(含端口自适应)或可配置的baseURL。E2E测试全过是因为测试环境就是8080------已知的部署适配点。
三、退避重连:3秒起步30秒封顶
typescript
let reconnectAttempts = 0
let shouldReconnect = true
function scheduleReconnect() {
if (reconnectTimer) clearTimeout(reconnectTimer)
if (!shouldReconnect) return // 主动断开不重连
reconnectAttempts++
const delay = Math.min(30000, 3000 * reconnectAttempts) // 3s,6s,9s...30s封顶
reconnectTimer = setTimeout(() => connect(), delay)
}
ws.onopen = () => { reconnectAttempts = 0 } // 连上重置计数
指数不是指数是线性 ------3s×次数的线性增长(不是2^n的指数)------网络抖动秒级恢复的场景3/6/9秒已经够用 ,指数退避(3/6/12/24)在服务端故障时更保守------政务内网的选择:线性够。
shouldReconnect标志位 ------disconnect()设false------用户主动退出登录后的onclose不触发重连 。没有这个标志:登出→连接关闭→3秒后自动重连→已登出用户又连上了(userId还在URL里)------安全事故。
30秒封顶的理由 ------重连风暴防护。网关重启时几千个客户端同时重试------无限增长的延迟只会让客户端长时间失联------封顶让重试节奏稳定,服务端恢复后最多30秒全员回来。
四、handler异常隔离
typescript
ws.onmessage = (event) => {
const data: WsMessage = JSON.parse(event.data)
lastMessage.value = data
handlers.forEach(h => {
try { h(data) } catch (e) { console.error('[WS] 消息处理异常', e) }
})
}
每个handler独立try-catch ------一个订阅者抛异常不影响其他订阅者、不断连接。消息中心组件的bug不连累待办推送------多订阅者场景的健壮性底线。
JSON.parse在消息层 ------协议定型为JSON文本帧------解析失败只console不重连(坏消息不该触发连接重置)。
五、生命周期:onUnmounted自动清理
typescript
onUnmounted(() => {
disconnect()
})
组件卸载自动断开 ------Vue3的composable里注册组件级钩子------useWebSocket在哪个组件setup里调用,组件销毁时就断。避免"路由切走连接还挂着"的僵尸连接累积。
全局推送的用法 ------HomeDemo.vue(主框架)调用一次------主框架不销毁连接一直在 ;MessageList.vue(消息中心页)也可以订阅------页面级订阅随页面销毁。一个连接,两级订阅生命周期。
六、与第43篇的完整闭环
后端completeTask
→ WorkflowNotifyService.sendMessage
→ 消息入库(WORKFLOW_MESSAGE)
→ afterCommit → publishEvent
→ WebSocketEventListener
→ notificationWebSocketHandler.sendToUser(userId, json)
→ userId的Session集合逐个send
↕ WebSocket连接
→ 浏览器ws.onmessage
→ handlers分发
→ MessageList加一条 / ElMessage弹通知
推送失败的用户 ------下次打开消息中心getUnreadMessages拉库------推+拉双通道的拉侧兜底(第43篇)。
✅ 亮点:wss/ws协议自适应、userId两级来源、线性退避30秒封顶的政务内网选择、shouldReconnect防登出自动重连事故、handler异常隔离、onUnmounted自动断连、硬编码8080的部署适配点。适合做实时推送前端的人。扩展方向:第43篇事务后推送、第42篇消息表兜底、第53篇dict缓存。