配了三年告警系统,今天发现根本不用写那个 WebSocket 服务端——浏览器自己会「收通知」,原生桌面提醒 + SSE 把这件事彻底变了
配了三年告警系统,今天发现根本不用写那个 WebSocket 服务端——浏览器自己会「收通知」,原生桌面提醒 + SSE 把这件事彻底变了。
那个 WebSocket 真的有必要吗
2019 年你要做个工单变更通知,第一反应是搭 WebSocket。建连接池、处理心跳、重连逻辑,光服务端就能写半天。更要命的是,有些项目根本用不到双向通信——用户只是被动接收通知,不需要回消息。
这种场景下,WebSocket 是用大炮打蚊子。
Server-Sent Events(SSE) 就是这个问题的原生平替。它是 HTTP 协议的一个扩展,服务器通过一个长期打开的 HTTP 连接向浏览器「流」数据。浏览器只需要一行原生 API:
const source = new EventSource("/events/stream");
source.addEventListener("notification", (e) => {
const data = JSON.parse(e.data);
new Notification(data.title, { body: data.body });
});
没了。自动重连、事件 ID 追踪、浏览器原生支持,三个最烦的点全不用自己写。
场景拆解:什么时候该用哪个
通知分两种场景,这两种场景需要完全不同的技术方案:
用户在页面内 → SSE 直接推过来,用 Notification API 弹一个系统通知框,同时更新页面内的铃铛数字。这套组合覆盖了 80% 的业务通知需求。
用户不在页面内(页面关了、电脑休眠了) → 需要 Push API + Service Worker。服务器通过 FCM(Firebase Cloud Messaging)把消息推给浏览器,浏览器唤醒 Service Worker,Service Worker 调用 Notification API 把通知弹出来。整套流程用户不需要开着页面。
对比一下:
| 场景 | 技术方案 | 复杂度 |
|---|---|---|
| 页面打开着,实时收通知 | SSE + Notification API | 极低 |
| 页面关了,还要推 | Push API + Service Worker + FCM | 中 |
| 需要双向通信(聊天) | WebSocket | 高 |
| 分钟级延迟可接受 | 轮询 | 极低 |
结论很清晰:大多数管理后台、运营系统的告警场景,SSE 就够了。
最小可用实现:Node.js + 原生浏览器 API
服务端只需要一个 /events/stream 端点:
// Node.js + Express
const clients = new Map(); // userId -> Set of Response
app.get("/events/stream", (req, res) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
});
res.write(": connectednn");
const userId = req.user.id;
if (!clients.has(userId)) clients.set(userId, new Set());
clients.get(userId).add(res);
req.on("close", () => {
clients.get(userId)?.delete(res);
});
});
function notify(userId, payload) {
const payloadLine = `event: notificationndata: ${JSON.stringify(payload)}nn`;
clients.get(userId)?.forEach(res => res.write(payloadLine));
}
客户端订阅:
const source = new EventSource("/events/stream");
source.addEventListener("notification", (e) => {
const data = JSON.parse(e.data);
// 页面开着就更新 UI
updateNotificationUI(data);
// 同时弹系统通知
if (Notification.permission === "granted") {
new Notification(data.title, { body: data.body, icon: "/icon.png" });
}
});
source.addEventListener("error", () => {
console.warn("SSE 连接断开,浏览器会自动重连");
});
申请通知权限只需要一次用户交互:
if (Notification.permission === "default") {
Notification.requestPermission();
}
Service Worker 让「页面关了」也能收到
Push API 解决的是「用户在手机锁屏状态」也能收通知。流程是这样的:
- 页面加载时,在 Service Worker 里订阅 Push:
registration.pushManager.subscribe() - 把订阅端点(一个 URL)存到你的服务端
- 服务端需要发通知时,POST 到这个端点
- 浏览器收到后唤醒 Service Worker,Service Worker 调用
showNotification()弹出系统通知
Service Worker 里的核心代码:
// service-worker.js
self.addEventListener("push", (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: "/icon.png",
tag: data.tag, // 同 tag 的通知不会叠加
data: { url: data.url },
})
);
});
self.addEventListener("notificationclick", (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data.url));
});
这里有个关键认知:Service Worker 是浏览器在后台运行的脚本,它和你的页面是独立的。这意味着即便用户把页面关了、浏览器最小化了,只要 Service Worker 还在,你的通知就能触达。
真实项目里容易掉的三个坑
坑一:代理超时把连接掐了
有些 nginx/负载均衡默认 60 秒无活动就断掉连接。SSE 需要心跳保活:
const heartbeat = setInterval(() => {
res.write(": heartbeatnn"); // 注释行,客户端不触发事件
}, 25000);
req.on("close", () => clearInterval(heartbeat));
坑二:多实例部署后消息丢了你不知道为什么
单实例 Node.js 的 Map 只管理本进程的连接。如果上了多实例(比如 Kubernetes 多副本),消息只发到了实例 A,用户 B 连的是实例 B,就收不到。
解法:所有实例订阅同一个 Redis pub/sub 频道,消息进来时统一广播:
const sub = createClient();
await sub.subscribe("notifications", (msg) => {
const { userId, data } = JSON.parse(msg);
broadcastToUser(userId, data); // 每个实例都尝试发给自己的连接
});
坑三:Notification API 权限被用户拒绝后就不问了
权限只有 grant、deny、default 三种状态。deny 之后无法通过代码再次请求,用户必须手动到浏览器设置里改。实践中建议在 UI 上给一个入口,而不是盲目调用 requestPermission()。
2026 年通知系统的新变量
今年有个值得关注的动向:Chrome 开始系统性整治滥用浏览器通知。2026 年 8 月 Google 公布的数字是,每分钟超过 1000 条推送的域名会被强制限流到 1000 条,超出后返回 HTTP 429。这对正常业务没影响,但那些靠高频推送骚扰用户的域名会被自动识别和封禁。
对于正经做通知系统的团队来说,这反而是好事——滥用少了,用户对通知权限的信任度会更高,requestPermission() 的通过率会上升。
下一步可以做的
- 先接 SSE + Notification API:这个组合不需要 Service Worker,不需要 FCM,一个下午能跑通最小可用版本
- 加 Service Worker:只在用户明确需要「页面关了也能收」时才上,不要一开始就做全套
- 接 WebSocket 只在必要时:你的产品经理如果说要「双向实时聊天」,那才是 WebSocket 的场景
通知系统的复杂度应该和业务价值匹配。大多数场景下,你其实不需要那个 WebSocket。
评论区
登录后可评论。