Safari 上的 Web Push
Meet Web Push for Safari
2022年6月6日
一句话判断
Safari 终于支持 Web Push 了——标准化的 Push API + Notifications API + Service Worker,macOS Ventura 上直接能用,iOS 和 iPadOS 要等”明年”。
这场 Session 讲了什么
这场 Session 系统性地介绍了 Safari 对 Web Push 的实现方案。好消息是,Apple 选择了完全遵循 W3C 标准——Push API、Notifications API、Service Workers,和 Chrome/Firefox 的实现方式一样,不需要学任何 Apple 私有 API。
整个流程是这样的:用户访问你的网站时,通过 PushManager.subscribe() 订阅推送服务。订阅需要 VAPID(Voluntary Application Server Identification)密钥来验证你的服务器身份。订阅成功后你会拿到一个 endpoint URL 和一组密钥,你的后端服务器用这些信息向推送服务发送消息,推送服务再转发到用户的浏览器。收到推送后,Service Worker 里的 push 事件被触发,你可以在里面构建通知内容并显示。
关键细节: Safari 要求推送订阅必须由用户手势触发(比如点击按钮)。你不能在页面加载时自动弹出订阅请求。这和 Chrome 的行为一致,但 Safari 对这个限制执行得更严格——Notification.requestPermission() 如果不是在用户手势的回调里调用,会被静默忽略。
推送消息到达时,即使用户没有打开你的网站标签页,Service Worker 也会被唤醒。这意味着你可以用 Web Push 实现”后台同步”之类的功能。通知可以包含操作按钮、图片、甚至自定义数据。用户点击通知后,你可以在 notificationclick 事件里决定是打开特定页面还是聚焦已有标签页。
平台支持: macOS Ventura 上的 Safari 16 率先支持。iOS 和 iPadOS 上的 Safari 会在”明年”(也就是 2023 年)跟进。Web Push 的通知会和原生 App 的通知一起出现在通知中心里。
值得深挖的点
VAPID 密钥:你的推送身份证明。 VAPID 让你的后端服务器用一对公私钥来证明”这条推送确实是从这个网站发出的”。Safari 要求使用 VAPID,不支持旧的 GCM sender ID 方式。如果你之前做 Android 的 Web Push 用的是 GCM 方式,迁移的时候需要生成 VAPID 密钥对。好消息是 VAPID 是所有主流浏览器的标准做法,所以迁移成本主要是后端改动。
Service Worker 的生命周期和推送。 Safari 对 Service Worker 的管理比较保守——如果用户长时间不访问你的网站,Service Worker 可能会被回收。但这不影响 Web Push 的接收,因为 Safari 会在收到推送时重新唤醒 Service Worker。你需要确保 push 事件处理器里的代码足够健壮,不能假设 Service Worker 有上次运行的缓存状态。
代码片段
订阅推送(前端):
// 检查浏览器支持
if ('PushManager' in window) {
// 必须在用户手势的回调里调用
const permission = await Notification.requestPermission();
if (permission === 'granted') {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: vapidPublicKey // Base64 编码的 VAPID 公钥
});
// 把 subscription 发送到你的后端服务器
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription.toJSON())
});
}
}
Service Worker 中处理推送:
// service-worker.js
self.addEventListener('push', (event) => {
const data = event.data ? event.data.json() : {};
const options = {
body: data.body || '你有新的消息',
icon: '/icon-192.png',
badge: '/badge-72.png',
data: {
url: data.url || '/'
},
actions: [
{ action: 'open', title: '查看详情' },
{ action: 'dismiss', title: '忽略' }
]
};
event.waitUntil(
self.registration.showNotification(data.title || '通知', options)
);
});
// 处理通知点击
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const url = event.notification.data.url;
event.waitUntil(
clients.openWindow(url)
);
});
后端发送推送(伪代码):
import jwt
import httpx
import base64
# VAPID 配置
vapid_private_key = "你的 VAPID 私钥"
vapid_subject = "mailto:admin@example.com"
# 构建 JWT
token = jwt.encode({
"aud": "https://fcm.googleapis.com", # 或其他推送服务
"exp": int(time.time()) + 86400,
"sub": vapid_subject
}, vapid_private_key, algorithm="ES256")
# 发送推送
response = httpx.post(
subscription_endpoint,
headers={
"Authorization": f"vapid t={token}, k={vapid_public_key}",
"Content-Type": "application/json"
},
json={"title": "Hello", "body": "World"}
)
最佳实践
推送订阅必须在用户点击按钮等手势回调中触发,不要在 DOMContentLoaded 或 load 事件中自动请求权限。Safari 会静默拒绝非用户手势触发的权限请求。
推送消息必须是”用户可见的”——也就是说你收到 push 事件后必须显示通知。如果你不显示通知,Safari 可能会替你显示一条默认通知(“此网站有新内容”),这对用户体验很糟糕。
通知点击后的跳转逻辑要考虑已有标签页的情况。用 clients.matchAll() 检查是否已经有打开的标签页,如果有就用 postMessage 通知它更新,而不是新开一个标签页。这样可以避免用户点击通知后出现多个相同的标签页。
还有什么值得关注
- Safari 的推送服务端点域名和其他浏览器不同,但 VAPID 认证方式完全一样,后端不需要针对 Safari 做特殊处理。
- macOS 上的 Safari 支持 Badging API(
navigator.setAppBadge()),可以配合 Web Push 在 Dock 图标上显示未读数。 - 如果用户在系统设置里关闭了你网站的通知权限,
pushManager.subscribe()会 reject。记得处理这个错误。 - iOS/iPadOS 上 Web Push 延迟到 2023 年,届时 Apple 可能会对推送频率或静默推送有额外限制。