← 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 是前后端唯一真相源,字段改名必须双端同步

六、复盘

长连接管理的本质,是把"不确定性"(断线 / 重连 / 弱网)收敛到可预期行为;契约翻译层把"前后端不一致"收敛到一处,避免污染业务组件。这两件事做好了,前端才不会在联调期反复"灵异"。