添加推送通知
发布于
完成本指南后,即使应用已关闭,你的服务器也能用一条消息唤醒用户的设备:页面把 Service Worker 订阅到
浏览器的推送服务,服务器向订阅 endpoint 发送请求,worker 把 push 事件变成一条系统通知。
这里涉及两个 API:推送是把消息送到 worker 的传输层,Notifications API 是用户看到的展示层。
你需要一个已激活的 Service Worker(没有的话先做入门)、一个 HTTPS 源, 以及服务端的一对 VAPID 密钥。支持情况:Push API 自 2023-03 起在各浏览器可用;Safari 只向主屏幕 Web 应用 投递,始于 iOS 与 iPadOS 16.4,macOS Ventura 上的 Safari 16.1 同样支持。各浏览器的明细见 Web Push 网页推送支持。
1. 先确认支持,再提供按钮
Section titled “1. 先确认支持,再提供按钮”ServiceWorkerRegistration.pushManager 是入口;浏览器没有推送能力时 window 上不存在 PushManager。
这种情况下隐藏订阅控件,沿用你已有的应用内渠道(站内收件箱、邮件摘要)。在 iOS 上,Safari 标签页里
这个检测同样不通过,只有用户把应用添加到主屏幕之后才通过,所以回退文案应当说明这一点。
export function pushSupported() { return 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window;}2. 在点击中请求权限
Section titled “2. 在点击中请求权限”Notification.requestPermission() 解析为 granted、denied 或 default;把 default 当作拒绝处理。
在一个说明了通知用途的按钮的点击处理函数里调用它。Firefox 72 拒绝非用户手势触发的请求,Safari 同样要求
直接的用户交互,而 denied 在用户到浏览器设置里改回之前都是最终结果,所以页面加载时的一次冒失请求
可能让你永久失去这条渠道。
subscribeButton.addEventListener('click', async () => { const permission = await Notification.requestPermission(); if (permission !== 'granted') { subscribeButton.disabled = true; subscribeButton.textContent = '本站的通知已关闭'; return; } await subscribe();});3. 用 VAPID 公钥订阅
Section titled “3. 用 VAPID 公钥订阅”仍在手势之内,调用 pushManager.subscribe(),传入 userVisibleOnly: true 与服务端的 VAPID 公钥作为
applicationServerKey。userVisibleOnly 不为 true 时 Chrome 与 Edge 拒绝该 Promise,并且它们要求提供
这个密钥(或在 manifest 里写旧式的 gcm_sender_id)。密钥是一个 ECDSA P-256 公钥,在服务端做 Base64url
编码;它不是用来加密载荷的 ECDH 密钥。
function base64UrlToUint8Array(base64Url) { const padding = '='.repeat((4 - (base64Url.length % 4)) % 4); const base64 = (base64Url + padding).replace(/-/g, '+').replace(/_/g, '/'); return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));}
async function subscribe() { const registration = await navigator.serviceWorker.ready; const existing = await registration.pushManager.getSubscription(); const subscription = existing || (await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: base64UrlToUint8Array(VAPID_PUBLIC_KEY), })); await fetch('/api/push/subscribe', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(subscription.toJSON()), });}4. 保存订阅,并把它当作会过期的东西
Section titled “4. 保存订阅,并把它当作会过期的东西”subscription.toJSON() 给出 endpoint 以及服务端加密与发送所需的 p256dh 与 auth 密钥。
endpoint 是一个能力 URL:持有它的任何人都能向该用户推送,所以只在服务端保存,并为订阅接口做好 CSRF 防护。
每个订阅只属于一个 Service Worker 注册,推送服务可能设置过期时间,到期时 worker 会收到
pushsubscriptionchange;在那里重新订阅并更新服务器。
self.addEventListener('pushsubscriptionchange', (event) => { event.waitUntil( self.registration.pushManager .subscribe(event.oldSubscription.options) .then((subscription) => fetch('/api/push/subscribe', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(subscription.toJSON()), }), ), );});5. 在 worker 中显示通知
Section titled “5. 在 worker 中显示通知”push 事件的 event.data 装着解密后的载荷。订阅时承诺过消息对用户可见,所以每条推送都显示一条通知;
Chrome 没有投递配额,Firefox 则限制不产生通知的推送数量,并在每次访问时刷新配额。处理 notificationclick
以聚焦已打开的窗口,或新开一个。
self.addEventListener('push', (event) => { const data = event.data ? event.data.json() : { title: '新消息', body: '' }; event.waitUntil( self.registration.showNotification(data.title, { body: data.body, icon: '/icons/icon-192.png', data: { url: data.url || '/' }, }), );});
self.addEventListener('notificationclick', (event) => { event.notification.close(); event.waitUntil( self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => { const open = clients.find((client) => 'focus' in client); return open ? open.focus() : self.clients.openWindow(event.notification.data.url); }), );});6. 从 DevTools 发一条测试推送
Section titled “6. 从 DevTools 发一条测试推送”写服务端代码之前,打开 Chrome DevTools,进入 Application > Service workers(英文界面),在 Push
按钮旁的输入框里填入载荷,点击 Push。worker 的 push 处理函数会带着这段文本运行;event.data.json()
遇到纯文本会抛错,所以用 {"title":"Hi","body":"test"} 来测。操作系统会弹出带你的图标的通知,
点击它会聚焦应用。随后用一个会签发 VAPID JWT、并用保存的密钥加密的 Web Push 库,从服务器发送同样的载荷。
- Web Push:订阅、权限与投递
- Notifications API:系统通知
- 通知权限模型
- iOS 与 Safari 的 Web Push(16.4+)
- PushManager: subscribe() method(developer.mozilla.org)
- Web Push for Web Apps on iOS and iPadOS(webkit.org)
- Notification: requestPermission() static method(developer.mozilla.org)
← 返回指南总览。