← Back to Lab
// Engineering2026.07.174 min read
前端长连接管理实战:指数退避重连、心跳与跨路由保活
前端原生 WebSocket 的健壮性实践:指数退避重连、心跳、看门狗、跨路由保活与前后端契约翻译层。
前端长连接管理实战:指数退避重连、心跳与跨路由保活
适用场景:用原生 WebSocket(非 socket.io)做 AI 流式/实时通信的前端。
一、需求
AI 写作 IDE 用原生 WebSocket 接收逐字生成。难点不是"连上",而是"长时间稳定不断、切页面不丢、弱网能恢复"。
二、useAIStream hook 的健壮性设计
- 指数退避重连:
[1000, 2000, 4000]最多 3 次; - 30s 心跳
ping/pong防网关空闲断连; - 60s 看门狗:超时无服务端消息强制关闭、触发重连;
- 发送队列
pendingQueue:未连时缓存任务,连上 flush; - 干净断开
disconnect()置retryCount = MAX防重连。
const RECONNECT_DELAYS = [1000, 2000, 4000];
socket.onopen = () => {
heartbeat = setInterval(() => {
socket.send(JSON.stringify({ type: "ping" }));
if (Date.now() - lastMessageAt > 60000) socket.close(); // 看门狗
}, 30000);
// 连上后 flush 排队任务
pendingQueue.forEach((t) => socket.send(JSON.stringify(t)));
pendingQueue.length = 0;
};
socket.onclose = (e) => {
if (e.code === 1000 || retryCount >= MAX) return; // 正常关闭 / 达上限
const delay = RECONNECT_DELAYS[Math.min(retryCount, 2)];
retryCount += 1;
setTimeout(connect, delay);
};
三、跨路由保活(容易被忽略)
项目级 layout 用 <AIStreamProvider> 挂载一次 WS,跨"章节 → 大纲 → 世界观"子路由切换时连接不断开、token 持续写入全局 store。否则"边切页面边看生成"就会断流重连,体验断层。
// projects/[slug]/layout.tsx
<AIStreamProvider>
{children} {/* 子路由切换,WS 不卸载 */}
</AIStreamProvider>
四、前后端契约翻译层
后端字段名 / 枚举常和前端不一致。我们把所有差异收敛到 shims.ts 一个文件,组件层永远消费 src/types,不直接碰后端裸结构:
const WS_TASK_TYPE_MAP: Record<string, string> = {
check_consistency: "check", // 前端长名 → 后端短名
continue: "continue",
};
export function toWsTaskType(t: string): string {
return WS_TASK_TYPE_MAP[t] ?? t;
}
五、两个联调坑(真实踩过)
- nginx 前缀红线:
location /api/转发后端、location /兜底前端 SPA。缺/api/v1前缀的请求被甩给前端,返回整页 HTML 而非 JSON。所有接口地址务必带前缀。 - schema 字段分叉:后端把时间约束字段叫
character/max_utterance_length,前端旧代码用characters/max_length,GET 直接 500。教训:schema 是前后端唯一真相源,字段改名必须双端同步。
六、复盘
长连接管理的本质,是把"不确定性"(断线 / 重连 / 弱网)收敛到可预期行为;契约翻译层把"前后端不一致"收敛到一处,避免污染业务组件。这两件事做好了,前端才不会在联调期反复"灵异"。