Meet Web Push for Safari
Media & Web 进阶 20m

Safari 上的 Web Push

Meet Web Push for Safari

2022年6月6日

在 Apple 官方观看视频

一句话判断

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"}
)

最佳实践

推送订阅必须在用户点击按钮等手势回调中触发,不要在 DOMContentLoadedload 事件中自动请求权限。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 可能会对推送频率或静默推送有额外限制。
WWDC 2022