Vue3项目里怎么实现稳定不崩溃的长连接?踩过坑后的完整指南
很多开发者刚接触Vue3配WebSocket,要么是基础的心跳断了不重连,要么是切换路由组件就断开再连太耗资源,要么是页面重启、网络波动后数据全乱套,这段时间我帮朋友改了3个不同类型的Vue3长连接项目(实时弹幕、股票看板、在线协作白板),踩了不少常见坑,也攒了些通用的稳定方案,今天一次性聊透。
为什么Vue3要用自定义Hook封装WebSocket?不直接在组件里写?
新手最容易犯的第一个错,就是把WebSocket实例直接写在组件的setup或者生命周期里,举个例子,股票看板的行情页,你肯定不想用户切到个人中心再切回来,服务器又给你发几十条甚至上百条重复的历史补全数据吧?也不想实例被重复创建、内存越用越高对吧?
自定义Hook是解决这个问题的核心,Vue3的Hook有清晰的作用域控制,你可以把它做成“单实例挂载式”的——就是整个项目只维护一个WebSocket实例,不同组件通过订阅不同的消息通道(或者后端约定的topic)来接收自己要的数据;心跳检测、自动重连、断线缓存这些通用逻辑,写在Hook里复用性拉满,组件只要传个url、topicList、onMessage回调就行;用provide/inject或者pinia配合Hook存实例状态,切换路由后只要不是主动销毁,实例和订阅关系都能保留,体验丝滑。
新手必看的Vue3 WebSocket封装基础版,要包含哪些核心功能?
别急着搞高级版,先把基础的、不会出大问题的功能搭好,这几点绝对不能少:
第一步:核心状态定义
基础状态得有这些:ws(WebSocket实例本身,不能暴露给普通组件直接操作,容易乱)、isConnecting(是不是正在握手,防止重复点按钮重连)、isConnected(当前是不是连上了,UI上可以显示个小绿灯红灯)、reconnectCount(当前重连次数,用来做重连间隔的指数退避,别一断就每秒疯狂连)、heartTimer(心跳定时器)、reconnectTimer(重连定时器)。
这些状态建议用ref存,别用reactive包裹成对象——不是不行,是普通组件只需要读,直接解构暴露的ref更方便响应式更新UI。
第二步:实例初始化
初始化函数里,第一步要先判断ws有没有已经存在,或者isConnecting是不是true,如果是就直接return,避免重复创建,然后要创建new WebSocket(url),注意要加协议头,开发环境如果是本地后端可以用ws://,生产环境必须用wss://加密,不然浏览器会拦截不安全的请求。
接下来绑定WebSocket的四个原生事件:onopen、onmessage、onerror、onclose——每个事件都要写对应的处理逻辑,不能空着。
第三步:原生事件处理
这四个事件是整个长连接的骨架:
- onopen:握手成功后,先把
isConnected设为true,isConnecting设为false,reconnectCount清0,然后马上开始心跳检测,另外如果有断线期间缓存的待发送消息,这里要遍历发出去。 - onmessage:收到消息的第一件事,不是直接传给组件回调,而是要判断是不是后端返回的心跳应答——很多新手在这里踩过坑,把心跳消息也发给了组件,导致UI上乱跳数据,怎么判断?和后端提前约定好格式就行,比如心跳消息是
{"type":"heartbeat","data":"pong"},后端收到前端的ping必须马上回这个pong,如果是业务消息,再根据topic或者type分发给订阅了对应通道的组件回调。 - onerror:出错后不用马上重连,先打印个日志方便排查,然后触发
onclose的逻辑——因为浏览器有时候出错后会自动触发onclose,但不是所有情况都触发,手动统一处理更稳。 - onclose:不管是主动关的还是被动断的,先把
isConnected设为false,然后清空心跳定时器,如果是被动断开的(比如不是组件主动调用close函数触发的),就启动指数退避的重连逻辑。
第四步:心跳检测
为什么心跳检测这么重要?因为WebSocket的长连接本质上是TCP连接,TCP连接如果长时间没有数据传输,网络中间的路由器、防火墙可能会把这个连接“杀掉”,但浏览器和后端都不知道,还以为是连上的,这就是所谓的“假死”。
心跳检测的逻辑很简单:每隔一段时间(比如30秒,别太短也别太长,太短费流量费性能,太长假死时间久)给后端发一个{"type":"heartbeat","data":"ping"},然后设置一个超时定时器(比如10秒),如果在超时前收到了pong,就清空超时定时器,继续下一轮心跳;如果没收到,就认为连接假死了,手动调用ws.close(),触发重连。
第五步:指数退避重连
一断就每秒连一次的傻事千万别干,不仅后端烦,可能还会被防火墙或者后端限流拦截,指数退避的意思是,重连间隔每次翻倍,但要有个上限,比如第一次隔2秒,第二次4秒,第三次8秒,最多到60秒就不再涨了,这样既能给后端恢复的时间,又不会让用户等太久。 还有,重连次数要不要有上限?我之前踩过坑,有的朋友设置了重连10次就不再连了,结果用户网络断了5分钟再恢复,永远连不上了——建议不要设置上限,最多在UI上显示个“已重连X次,仍未连接,请检查网络”,然后让用户可以手动点个“立即重连”按钮。
第六步:消息订阅与取消订阅
前面说了,最好是单实例多topic的模式,这样就需要订阅和取消订阅的功能,比如弹幕组件挂载时订阅topic:danmaku_room_123,弹幕组件卸载时取消订阅这个topic,白板组件同时订阅topic:whiteboard_sync_456和topic:whiteboard_undo_redo_456——这样消息分发才不会乱。
怎么实现订阅?可以在Hook里定义一个subscribers对象,key是topic,value是一个Set,存所有订阅了这个topic的组件回调;组件调用subscribe(topic, callback)时,就把callback加到对应topic的Set里;调用unsubscribe(topic, callback)时,就把callback从Set里删掉,注意要用Set,不能用数组,因为Set自动去重,而且删除的时间复杂度是O(1),比数组高效。
进阶版优化,解决实际开发中的3个大坑
刚才的基础版能解决80%的问题,但剩下的20%才是决定长连接稳不稳定的关键——我帮朋友改项目时遇到的这3个坑,几乎每个Vue3 WebSocket项目都会碰到:
坑1:页面刷新、关闭标签页后再打开,订阅关系全没了,心跳也断了?
哦不对,页面刷新后所有JavaScript变量都会重置,基础版的单实例肯定也没了,这个是必然的,但我们可以优化:
- 页面刷新前,把当前的
topicList(就是所有组件正在订阅的topic)存到sessionStorage里,sessionStorage在标签页关闭后清空,刷新后保留; - 页面刷新后,初始化Hook的时候,先从
sessionStorage里读取topicList,如果有,就自动订阅这些topic,不用等组件重新挂载再订阅——不过这里要注意,组件重新挂载时可能会再次调用subscribe,所以刚才说的用Set存callback是对的,自动去重; - 还有,页面关闭或者刷新前,要给后端发一个“临时断开,保留订阅状态”的消息吗?这个要看后端的实现,如果后端支持保留用户的session或者订阅状态一段时间(比如5分钟),那可以发;如果不支持,就不用发,刷新后重新握手订阅就行。
坑2:切换路由组件后,待发送的消息丢了?
比如股票看板用户在个人中心页面,看到自己关注的股票有个新公告,想切回行情页发个弹幕吐槽,但切换的时候断网了,弹幕没发出去——等网好了,用户已经忘了这件事,体验很差。
解决方案是:在Hook里定义一个messageQueue数组,用来存断线期间待发送的消息;组件调用sendMessage(msg)时,先判断isConnected是不是true,如果是,就直接发送;如果不是,就把msg加到messageQueue里;等onopen触发时,再遍历messageQueue,把所有消息按顺序发出去,发完清空数组。
注意,如果是在线协作白板这种对消息顺序要求极高的场景,还要给每条消息加个唯一的ID和时间戳,后端也要做消息顺序的校验和重排,防止因为网络延迟导致消息乱序。
坑3:Pinia配合Hook存状态,会不会有响应式问题?
刚才说可以用provide/inject或者pinia配合Hook存状态,这里推荐用Pinia,因为Pinia是Vue3官方推荐的状态管理工具,支持devtools调试,而且响应式更新更稳定。
响应式问题主要出在两个地方:
- 第一个是
ws实例本身,不能直接存到Pinia的state里,因为WebSocket实例是个复杂的原生对象,不是响应式数据,存进去没用;可以把Hook放在Pinia的store里,这样store里就能直接用Hook的状态和方法,其他组件通过useStore来调用。 - 第二个是订阅回调的响应式,如果组件的回调里用到了组件的响应式数据(比如弹幕组件的
userId),直接把回调传给Hook的subscribe,等组件卸载后,回调里的响应式数据可能会变成undefined,或者导致内存泄漏——解决方案是,组件里用watchEffect或者onMounted配合onUnmounted,在subscribe时用Vue的markRaw包裹回调吗?不对,markRaw是让对象不变成响应式,和这个没关系;应该用Vue的effectScope吗?或者直接在组件的onUnmounted里手动调用unsubscribe,同时确保回调里用的是组件的ref的.value,而不是直接解构出来的普通变量——对,后者更简单,新手也能掌握。
在线协作白板项目里的实际应用示例
光说不练假把式,我给大家写个在线协作白板项目里的Hook和组件的简化版示例,方便大家直接套用:
首先是useWebSocket.js的Hook,然后是Whiteboard.vue的组件,最后是Pinia的wsStore.js——注意这里我把Hook放在了Pinia里,这样所有组件都能通过useStore共享同一个WebSocket实例。
哦对了,生产环境还要加个鉴权,就是在WebSocket的url后面加个token参数,比如wss://api.example.com/ws?token=xxx,后端在握手的时候验证token,如果token无效,就直接关闭连接;还有,消息要做加密吗?如果是敏感数据(比如在线协作的合同内容),建议用TLS加密(就是刚才说的wss协议),再在业务层加个简单的AES加密,双重保险。
Vue3实现稳定长连接的5个核心原则
今天聊了这么多,最后总结一下5个核心原则,大家记住这5个原则,不管做什么类型的Vue3 WebSocket项目,都不会出大问题:
- 单实例多topic:整个项目只维护一个WebSocket实例,不同组件通过订阅不同的topic来接收数据,减少服务器压力和资源消耗;
- 必加心跳检测:防止TCP连接假死,及时发现断线;
- 指数退避重连:不要疯狂重连,给后端恢复的时间,还要有手动重连按钮;
- 断线缓存消息:断线期间待发送的消息要存起来,等连上了再发;
- 合理使用状态管理:推荐用Pinia配合自定义Hook存状态,支持devtools调试,响应式更新更稳定。
如果大家还有其他Vue3 WebSocket的问题,可以在评论区留言,我会尽量一一解答。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网



