我刚接手的项目里有个实时告警看板,线上跑了一个多月,时不时就有用户反馈“画面不动了”、“状态不刷新了”,大部分时候刷新页面能恢复,但总有几个顽固分子怎么刷都不行。查了一圈最后发现,根子全在WebSocket的异常处理上——不是连接没建立,而是建立之后的那些“半死状态”和异常分支没有做处理。WebSocket这东西,建立连接只是万里长征第一步,真正考验功底的是连接建立之后那堆乱七八糟的异常怎么接得住、怎么恢复。
这篇文章我想把实际项目中踩过的坑和最终沉淀下来的异常处理方案完整拆一遍。内容覆盖面比较广,包括WebSocket生命周期的各个异常节点怎么抓、连接中断后怎么自救、后端该如何设计异常消息体配合前端联动,以及“谷歌浏览器高版本无法启用WebSocket”这类高频问题的真实原因与排查方法。不管是纯前端、全栈还是做桌面客户端(比如WPF里嵌WebSocket)的同学,这套方案都值得抄作业。
1. 先搞清楚WebSocket的异常到底长什么样
很多前端同学接WebSocket,第一版代码都长这样:
javascript复制const ws = new WebSocket('ws://api.example.com/ws');
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
render(data);
};
这段代码在Demo里跑得欢天喜地,一上生产就原形毕露。被用户骂了三天之后我才意识到一个问题:onmessage只是整个WebSocket生命周期里的一个节点,而异常根本不只在消息推送阶段出现。连接压根没建立成功、连接中途断开、服务端主动关闭、网络切换导致连接假死,每一种情况表现出来的现象和应对策略都完全不同。
1.1 生命周期视角下的异常节点划分
WebSocket从创建到销毁,大概会经历这么几个阶段:
- 连接建立阶段(connecting → open)
- 消息通信阶段(open → 持续收发消息)
- 主动关闭阶段(客户端或服务端发起close)
- 异常断开阶段(网络中断、服务崩溃、代理超时等)
我后来在项目里画了一张表,把关闭事件里的code字段和常见场景严格对应起来,这成为后面所有异常处理逻辑的依据:
| Close Code | 含义 | 常见触发场景 |
|---|---|---|
| 1000 | 正常关闭 | 页面unload、主动调用close() |
| 1001 | 端点正在离开(如服务器重启) | 服务端发版、主动踢人 |
| 1006 | 异常关闭(无close frame) | 网络断、代理断开、进程崩溃 |
| 1009 | 消息过大 | 单条消息超过服务端限制 |
| 1011 | 服务端内部错误 | 服务端未捕获的业务异常 |
| 4000-4999 | 私有业务码(自定义) | 鉴权过期、token失效、单点登录被顶 |
这里最坑的是1006,因为它是“非正常关闭”,而且浏览器在触发onclose时通常拿不到具体的错误信息。网络抓包能看到是TCP层面直接断开,但前端能感知到的就是一个孤零零的1006。所有“画面不动了”的线上事故,基本都和1006有关。
1.2 按发生阶段分类比按错误类型分类更实用
最初我也习惯性地按“网络错误”、“服务端错误”、“数据错误”来写异常处理分支,但在实际操作中非常别扭——因为一个异常可能是多因素叠加导致的。比如用户在电梯里,手机信号切到4G,TCP中断触发onclose(1006),紧接着断网恢复触发onerror事件,这两个事件里你能拿到的信息完全不同。
所以后面我换了一套思路:按“阶段”划分异常处理的关注点,每个阶段做独立策略。连接阶段的异常想的是“要不要重试”——比如网络抖动导致的连接失败,重试是合理的;但如果是401鉴权失败,重试多少次都没用。通信阶段的异常想的是“当前这条消息还要不要继续处理”——比如消息格式坏了,直接丢弃并且上报,而不是让它阻断后续正常消息。关闭阶段的异常想的是“该怎么通知用户”——是静默重连,还是弹提示,取决于异常码和业务容忍度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 前端异常捕获:每个阶段都要有兜底
这一节是全文的核心,也是我在生产环境里反复验证过的方案。WebSocket的前端异常处理,本质上要做四件事:连接建立失败要能感知、消息解析要容错、连接断开要能区分“主动”和“被动”、所有异常都要有对应的处置策略。
2.1 连接阶段的onerror与onclose联动
连接阶段最容易踩的坑是只监听onerror,不监听onclose。要知道,WebSocket一旦onerror触发,后面必然还会跟一个onclose——这俩是“先报错、再关闭”的关系。如果只处理onerror,你其实漏掉了关闭事件里最有价值的code信息和后续重连时机;如果只处理onclose,你又抓不到错误事件的详情。
我现在的做法是把两个事件联动起来:
javascript复制ws.addEventListener('error', (event) => {
// 注意:error事件里几乎拿不到有效信息
console.error('WebSocket error:', event);
ws.__lastError = event;
});
ws.addEventListener('close', (event) => {
if (event.code === 1006) {
// 1006 说明没有收到关闭帧,属于异常断开
reportError('ws_abnormal_close', event.reason);
scheduleReconnect(calculateBackoff(retryCount));
} else if (ev
