用 Notification API 实现网页端消息推送的完整实践指南

晓曼 交互 阅读 1,572
赞 22 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

上个月在做一个内部工单系统,后端推送实时状态变更(比如“客户已确认”“财务已打款”),需要在用户不聚焦页面时也收到提醒。一开始想用 WebSocket + 自定义弹窗,但发现桌面端没权限、移动端锁屏就断连、还得自己做去重和静音逻辑……折腾半天,突然想起来——浏览器原生 Notification API 不就是干这个的?

用 Notification API 实现网页端消息推送的完整实践指南

查了下兼容性:Chrome 22+、Firefox 22+、Edge 14+ 都支持,Safari 一直没支持(后面再吐槽)。我们团队内部系统 Chrome 占比 87%,Firefox 9%,基本可以闭眼用。于是拍板:Notification API 上。

核心代码就这几行

真写起来其实挺简单,但有几个关键点必须手动处理,不然上线第一天就被 QA 打死。

首先是权限申请——不能一进来就 request,得等用户有明确操作意图(比如点“开启通知”按钮)才调,不然 Chrome 会直接屏蔽后续所有请求:

// 点击按钮触发
document.getElementById('enable-notif').addEventListener('click', async () => {
  if (Notification.permission === 'granted') {
    showNotification();
  } else if (Notification.permission === 'default') {
    const result = await Notification.requestPermission();
    if (result === 'granted') {
      showNotification();
    }
  }
});

function showNotification() {
  new Notification('工单已更新', {
    body: '客户【张三】已确认交付内容',
    icon: '/assets/icons/notify-64.png',
    tag: 'ticket-12345' // 用于去重和替换
  });
}

最大的坑:性能问题

开始我以为只要 new Notification() 就完事了。结果上线第二天,运维告警说某台机器 CPU 持续 95% —— 定位到是前端疯狂创建 Notification 实例。

原因很简单:后端每秒推 3~5 条变更,前端没做节流,直接全量转发给 Notification。而 Notification 的构造函数不是纯 JS 调用,它会触发系统级 UI 渲染(macOS 是右上角横幅,Windows 是 Action Center 入口),频繁触发导致主线程卡顿,甚至出现“弹出一个、卡两秒、再弹一个”的情况。

更坑的是:Notification.permission === 'granted' 返回 true 后,并不代表你一定能成功创建实例——如果用户刚手动关闭了系统通知权限(比如 macOS 设置里关了 Safari 通知),new Notification() 会静默失败,控制台也不报错,只在 devtools Application → Notifications 里能看到“Failed to show notification”。我花了整整一下午才在 devtools 里翻到这行提示。

解决方案分三步:

  • 加节流:服务端推送过来的数据先进队列,用 setTimeout 合并 500ms 内的同类型通知(比如同一张工单的多次更新只显示最后一次)
  • 加兜底判断:if ('show' in Notification.prototype) 确保浏览器支持,再检查 Notification.permission === 'granted'
  • 加异常捕获:用 try/catch 包裹 new Notification(),失败时 fallback 到页面顶部 banner 提示(用户至少能看见)

又踩坑了:tag 和 replace 机制不按预期工作

文档里写 tag: 'xxx' 可以让相同 tag 的通知自动替换,避免刷屏。但我发现 Chrome 有时替换了,有时又叠在一起……折腾半天发现:Chrome 只有在「前一条通知还没被用户点击/关闭」的前提下,才会用新通知替换旧的。一旦用户点了旧通知,或者它自动消失(默认 4 秒),新通知就会重新弹出来。

所以最后我放弃了纯 tag 替换,改用主动 close:

let currentNotif = null;

function showNotification(title, options) {
  // 先关掉上一个(如果有)
  if (currentNotif && currentNotif.close) {
    currentNotif.close();
  }

  currentNotif = new Notification(title, {
    ...options,
    tag: ticket-${Date.now()} // 强制唯一 tag,避免旧通知残留影响
  });

  // 用户点击后清理引用
  currentNotif.onclick = () => {
    window.focus();
    currentNotif = null;
  };

  // 自动清理(防内存泄漏)
  setTimeout(() => {
    if (currentNotif && currentNotif.close) {
      currentNotif.close();
      currentNotif = null;
    }
  }, 5000);
}

谁也没想到的 Safari 问题

Safari 从始至终不支持 Notification API(包括 iOS 和 macOS)。我们本来打算在 Safari 里降级为 localStorage + 页面 badge,结果测试发现:iOS Safari 连 Notification.permission 都是 undefined,typeof Notification'undefined'。没办法,只能整个模块包裹一层检测:

if ('Notification' in window && typeof Notification.requestPermission === 'function') {
  // 正常流程
} else {
  // Safari / 旧 IE:走 fallback
  console.warn('Notification API not supported, using in-page alert');
  showInPageAlert('工单已更新:客户已确认');
}

这里有个小细节:Safari 在 PWA 模式下(添加到主屏幕)其实是支持 Web Push 的,但需要后端配合 VAPID、Service Worker、PushManager……成本太高,内部系统没必要搞这么重,所以直接放弃。

回顾与反思

最终上线效果还行:Chrome 用户能稳定收到通知,90% 的工单状态变更都能在 2 秒内触达;Firefox 用户偶尔延迟 1~2 秒(推测是其 Notification 渲染机制更保守);Safari 用户看到的是页面顶部黄色 banner,体验降级但功能不丢。

做得好的地方:

  • 权限申请时机控制得准,没被浏览器拦截
  • 节流 + 主动 close 组合拳,彻底解决 CPU 飙高和通知堆积
  • fallback 方案覆盖全,没有白屏或静默失败

还能优化的地方:

  • 通知点击后跳转逻辑目前硬编码在 onclick 里,应该抽成配置项,方便后续扩展(比如不同通知类型跳不同路由)
  • 图标路径写死 /assets/icons/notify-64.png,没适配 dark mode,macOS 深色模式下白色图标看不清——不过目前内部用户没人提,暂时搁置
  • 没做通知历史记录(比如用户错过某条,之后点“查看全部”),因为后端没存推送日志,前端 localStorage 存又容易满,权衡后砍掉了

以上是我踩坑后的总结,希望对你有帮助。这个 API 看似简单,但真用到生产环境,权限、性能、兼容性、用户体验全是连环坑。如果你有更好的节流策略、或者 Safari 下的轻量级替代方案,欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论