配了三年告警系统,今天发现根本不用写那个 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 解决的是「用户在手机锁屏状态」也能收通知。流程是这样的:

  1. 页面加载时,在 Service Worker 里订阅 Push:registration.pushManager.subscribe()
  2. 把订阅端点(一个 URL)存到你的服务端
  3. 服务端需要发通知时,POST 到这个端点
  4. 浏览器收到后唤醒 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() 的通过率会上升。


下一步可以做的

  1. 先接 SSE + Notification API:这个组合不需要 Service Worker,不需要 FCM,一个下午能跑通最小可用版本
  2. 加 Service Worker:只在用户明确需要「页面关了也能收」时才上,不要一开始就做全套
  3. 接 WebSocket 只在必要时:你的产品经理如果说要「双向实时聊天」,那才是 WebSocket 的场景

通知系统的复杂度应该和业务价值匹配。大多数场景下,你其实不需要那个 WebSocket。

评论区

0 条评论

登录后可评论。