用 Notification API 实现网页端消息推送的完整实践指南
项目初期的技术选型
上个月在做一个内部工单系统,后端推送实时状态变更(比如“客户已确认”“财务已打款”),需要在用户不聚焦页面时也收到提醒。一开始想用 WebSocket + 自定义弹窗,但发现桌面端没权限、移动端锁屏就断连、还得自己做去重和静音逻辑……折腾半天,突然想起来——浏览器原生 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 下的轻量级替代方案,欢迎评论区交流。

暂无评论