多人系统的"刷新":为什么选 SSE 而不是 WebSocket
先分清需求
| 需求 | 合适的技术 |
|---|---|
| 单向通知"数据变了" | SSE |
| 双向实时协作(画板、协同编辑) | WebSocket |
| 秒级都嫌慢的行情推送 | WebSocket / WebTransport |
内部系统里 90% 的"实时"需求其实是第一行:服务端有事要说,客户端没什么要回的。这种场景用 WebSocket,等于为了发一条通知维护一套双向连接协议。
最小实现
const clients = new Map(); // clientId -> res
app.get("/api/events", (req, res) => {
const id = req.query.cid;
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
});
res.write(": ok\n\n");
clients.set(id, res);
req.on("close", () => clients.delete(id));
});
function broadcast(evt, from) {
for (const [id, res] of clients) {
if (id !== from) res.write(`data: ${JSON.stringify(evt)}\n\n`);
}
}
浏览器侧只有三行:
const es = new EventSource("/api/events?cid=" + CID);
es.onmessage = (e) => handle(JSON.parse(e.data)); // 断线会自动重连
四个必须避开的坑
1. 反代默认会缓冲。 Caddy 要显式关掉,否则事件被攒着不发,看起来像"推送失败":
reverse_proxy localhost:3000 {
flush_interval -1
}
2. 别把自己的操作也推给自己。 广播时排除发起的 clientId,否则一保存页面自己跳一下,像出 bug。
3. 粒度要细。 广播 project.updated:42 而不是"有数据变了",否则整页反复闪。
4. 正在编辑的表单不能静默覆盖。 这条最重要:检测到该区域有未保存改动时,只在角落提示"某某刚改了这条,点此加载最新",绝不自动重拉。丢别人十分钟的输入,比晚刷新十秒严重得多。
兜底
SSE 在代理后面偶尔会被悄悄掐断而前端不知道。叠一个 60 秒的低频轮询(只取 max(updated_at),几十字节),对不上就整块重拉。这个成本极低,但能兜住"只在别人电脑上出现"的连接假死。
本文为个人学习记录,可能存在错误或已过时,仅供参考。
← 返回笔记列表