浏览器推送通知必须走HTTPS(localhost除外)且需用户主动授权,无后台静默推送;依赖Service Worker注册与激活,通过Notification.requestPermission()和pushManager.subscribe()获取PushSubscription;后端须用VAPID JWT头及AES-128-GCM加密发送;交互逻辑在sw.js中处理notificationclick/notificationclose事件。

浏览器推送通知必须走 HTTPS(本地 localhost 除外),且用户必须主动授权,没有“后台静默推送”这回事——这是硬性前提,绕不过。
如何注册 Service Worker 并检查支持情况
推送依赖 Service Worker,它必须在 HTTPS 下注册,且脚本路径需与页面同源。不支持的环境(如 Safari 旧版、部分国产 WebView)会直接跳过。
-
navigator.serviceWorker未定义 → 浏览器不支持(IE 全系、Safari - 注册时返回
undefined或报Failed to register a ServiceWorker→ 常见于 HTTP 环境或跨域脚本路径 - Service Worker 文件(如
sw.js)必须放在网站根目录或上级路径,否则注册失败
示例检查逻辑:
if ('serviceWorker' in navigator && 'PushManager' in window) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered', reg))
.catch(err => console.error('SW registration failed:', err));
}
怎样请求用户授权并获取 PushSubscription
调用 Notification.requestPermission() 只是第一步;真正拿到可推送的凭证,得等 Service Worker 激活后,再从 reg.pushManager.subscribe() 获取 PushSubscription 对象。
立即学习“前端免费学习笔记(深入)”;
- 用户点“拒绝”后,
Notification.permission变为'denied',后续调用subscribe()会直接 reject,不会再次弹窗 -
subscribe()的userVisibleOnly: true是强制项,Chrome 以外浏览器可能忽略,但必须写 - 如果 Service Worker 处于
waiting状态(比如更新了sw.js但没跳转),pushManager可能不可用,需先skipWaiting()或触发页面重载
关键订阅代码片段:
navigator.serviceWorker.ready.then(reg => {
reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array('...') // VAPID 公钥
}).then(sub => {
// sub.endpoint 就是发送推送的 URL
});
});
为什么后端发推送总失败:VAPID 头和加密格式最容易错
Chrome / Firefox 要求服务端推送时携带合法 VAPID JWT 头,并对 payload 做 AES-128-GCM 加密。错误常出现在:Authorization 头格式不对、crypto.subtle 导出私钥方式错误、加密时用错 contentEncoding(应为 aes128gcm)。
- VAPID 公钥必须是 base64url 编码的 Uint8Array,不能直接传字符串;私钥必须用
exportKey('jwk')再转 PEM,不能硬编码 - 推送请求的
Content-Encoding必须是aes128gcm,不是aesgcm或空值 - Node.js 后端推荐用
web-push库,它自动处理加密和头生成;手写容易漏掉encryption、crypto-key等 header 字段
典型错误响应:400 Bad Request(密钥格式错)、401 Unauthorized(JWT 过期或签名无效)、410 Gone(endpoint 已失效,用户取消授权或重装浏览器)
如何处理推送到达后的点击和关闭行为
所有交互逻辑写在 sw.js 里,不是页面 JS。用户点击通知时触发 notificationclick,关闭时是 notificationclose —— 但后者仅在用户手动关闭时触发,自动消失不算。
-
self.clients.openWindow(url)只在 Chrome/Firefox 有效,Safari 不支持,需降级用focus()已有 tab - 通知的
data字段只能存序列化数据(如字符串、数字),不能传函数或 DOM 节点 - 若页面已打开,点击通知后应先
clients.matchAll()找到对应 client,再postMessage()通信,而不是重复开新页
示例监听:
self.addEventListener('notificationclick', e => {
e.notification.close();
e.waitUntil(
clients.matchAll({ type: 'window' }).then(windows => {
if (windows.length > 0) windows[0].focus();
else clients.openWindow('/dashboard');
})
);
});
真正难的不是注册或发请求,而是把 VAPID 密钥生命周期、Service Worker 更新机制、跨浏览器兼容性(尤其是 Safari 的限制)和用户授权状态变化串成一条稳定链路——漏掉任意一环,通知就只在开发机上响一声。



















