WebSocket 重连退避与页面卸载的收尾
来源:17golang原创
时间:2026-09-29 06:10:25 381浏览 收藏
实时看板、在线协作和消息页常见一个细小但顽固的故障:网络断开后 WebSocket 能重连,用户离开页面后却还在继续排队;或者页面从前进后退缓存恢复时,旧连接和新连接同时存在。稳定的做法是把“异常断开”和“页面收尾”拆开处理:前者允许退避重连,后者设置主动关闭标记、清掉定时器,并在需要时由 pageshow 恢复。
重连只由非主动关闭触发;页面离开先清理状态,再关闭 socket。这样退避策略不会把页面卸载重新变成无限重连。
- 连接对象、重试次数、定时器和收尾标记必须共享一份状态。
- 延迟使用指数退避加少量随机抖动,并在 open、close 和主动收尾时清理。
pagehide处理离开,pageshow处理 bfcache 恢复;不要把unload当作唯一保证。
先把重连状态和页面生命周期拆开
WebSocket 的 close 事件可能来自服务端、网络错误或页面代码主动调用 close()。如果所有 close 都直接进入重连函数,页面离开时就会出现“刚关闭又创建”的竞态。先定义一份小状态:当前 socket、重试次数、重连定时器,以及表示页面是否正在收尾的 closing。

| 状态/事件 | 应做的事 | 不能做的事 |
|---|---|---|
| open | 重置重试次数,确认连接属于当前实例 | 保留旧的重连定时器 |
| close | 清理引用,再按主动标记决定是否重连 | 无条件创建新 socket |
| pagehide | 设置 closing,取消定时器并 close(1000) | 等待 unload 才清理 |
| pageshow.persisted | 解除 closing,重新建立一条连接 | 复用已经关闭的 socket 对象 |
退避函数只接受一次“允许重连”的判断。下面的代码把延迟上限设为 30 秒,抖动只用于错开同时重连的客户端;重试次数在新连接成功后归零。
const state = {
socket: null,
retryTimer: 0,
retryCount: 0,
closing: false,
};
function scheduleReconnect() {
// 页面正在收尾或已经排队时,不再创建第二个定时器。
if (state.closing || state.retryTimer) return;
const base = Math.min(30000, 500 * 2 ** state.retryCount);
const delay = base + Math.floor(Math.random() * 300);
state.retryCount += 1;
state.retryTimer = window.setTimeout(() => {
// 回调执行前再次判断,覆盖 pagehide 与定时器竞态。
state.retryTimer = 0;
if (!state.closing) connect();
}, delay);
}
退避只绑定异常关闭,正常收尾不再拉起
每次连接都要绑定自己的 close 监听器,监听器里先确认它仍是当前 socket。否则旧连接的延迟 close 事件可能覆盖新连接状态。close() 会启动关闭握手,不能把它当成“立即丢弃所有已发送数据”的硬切断;主动收尾的目标是停止后续工作,而不是伪造网络故障。

function connect() {
if (state.closing || state.socket) return;
const socket = new WebSocket("wss://example.test/realtime");
state.socket = socket;
socket.addEventListener("open", () => {
// 成功连接后清零退避计数,避免短暂故障后长期等待。
if (state.socket === socket) state.retryCount = 0;
});
socket.addEventListener("close", (event) => {
// 旧 socket 的延迟事件不能清掉新 socket 的引用。
if (state.socket === socket) state.socket = null;
if (!state.closing && !event.wasClean) scheduleReconnect();
});
}
function closeForNavigation() {
// 收尾顺序固定:先阻断重连,再取消定时器,最后关闭连接。
state.closing = true;
if (state.retryTimer) {
window.clearTimeout(state.retryTimer);
state.retryTimer = 0;
}
if (state.socket) {
state.socket.close(1000, "page navigation");
state.socket = null;
}
}
页面卸载、隐藏和 bfcache 的边界
对于“用户离开当前页面”这个动作,pagehide 比 unload 更适合,它与 bfcache 兼容;页面从 bfcache 恢复时,pageshow 的 persisted 可以提示重新建立连接。只是移动端的页面生命周期并不保证每个事件都发生,所以不能把一次 close 当作服务端会话已经完成的证明。
window.addEventListener("pagehide", () => {
// pagehide 只负责页面离开场景,不把普通后台隐藏误判为卸载。
closeForNavigation();
});
window.addEventListener("pageshow", (event) => {
// bfcache 恢复后,旧的 WebSocket 实例不能复用。
if (event.persisted) {
state.closing = false;
connect();
}
});
document.addEventListener("visibilitychange", () => {
// 隐藏页暂停非必要 UI 工作;是否断开实时连接应由业务策略决定。
if (document.visibilityState === "hidden") pauseRendering();
});
如果业务要求切到后台就断开,应把策略写成独立的 closeForBackground,并在恢复时走同一套 connect,不要直接复用页面卸载函数后忘记解除 closing。若只是减少渲染和心跳成本,保留 socket 通常更符合实时页面的预期。
常见问题
为什么加了指数退避仍然会重复连接?
通常是定时器没有清零,或旧 socket 的 close 事件没有做实例比较。把“当前 socket 是谁”和“是否已有 retryTimer”作为两个独立条件检查。
主动调用 close() 后还需要监听 close 吗?
需要。close 事件仍是统一回收引用和心跳资源的入口,但监听器必须看到 closing 后跳过重连。
pagehide 能保证所有手机都触发吗?
不能。它适合监听页面离开,但移动端切换应用或系统回收页面时可能没有可靠的最后事件;重要数据要提前保存,不能押在 unload 上。
收尾检查清单
- 异常 close 会重连,主动 close 不会重连。
- 每个重连定时器在执行、成功连接或页面收尾时都能被清理。
- pageshow 的 bfcache 恢复不会复用旧 socket,也不会留下 closing 标记。
- 隐藏页策略和卸载策略分开,移动端不可靠事件不会被当成业务确认。
-
130 收藏
-
311 收藏
-
447 收藏
-
493 收藏
-
156 收藏
-
291 收藏
-
294 收藏
-
142 收藏
-
288 收藏
-
392 收藏
-
110 收藏
-
251 收藏
-
380 收藏
-
269 收藏
-
249 收藏
-
167 收藏
-
149 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习