WWDC Quick Look 💓 By SwiftGGTeam
Meet Web Push for Safari

Meet Web Push for Safari

观看原视频

Highlight

Safari 16 在 macOS Ventura 为网站和 Web App 增加基于 Push API、Notifications API、Service Worker 和 VAPID 的标准 Web Push;已按标准实现的站点无需 Safari 私有 API,也不需要 Apple Developer account。


核心内容

网站通知长期有一个现实问题:用户不一定开着你的页面。博客、消息、任务提醒、内部工具更新,都希望在页面关闭后还能通知用户。

Safari 过去有 Safari Push Notifications。它会继续工作。但从 macOS Ventura 的 Safari 16 开始,Safari 支持标准 Web Push(00:43)。Apple 明确说,它使用的是其他浏览器也采用的标准组合:Push API、Notifications API 和 Service Worker(01:08)。

这带来一个直接结果:如果站点已经按 Web 标准实现 Web Push,就不需要为 Safari 写私有分支(01:16)。真正要检查的是两件事。第一,不要用浏览器嗅探把 Safari 排除在外,要用 feature detection。第二,如果服务端严格校验推送 endpoint,要允许 push.apple.com 的任意子域名(01:48)。

这场 session 的示例 App 叫 Browser Pets。它已经有 Service Worker,用来加速页面加载并在多个标签页之间同步。Web Push 要做的事,是在这个基础上增加三个环节:注册或复用 Service Worker、在用户点击按钮后订阅推送、在 Service Worker 里处理 pushnotificationclick 事件。

详细内容

先安装 Service Worker

08:27)Web Push 依赖 Service Worker。Service Worker 是独立于当前标签页运行的 JavaScript,它代表整个 domain 工作。Safari 收到服务端推送后,会唤醒它并发送 push 事件。

// BrowserPetsWorker.js

function handleMessageEvent(event) {
    // ...
};
self.addEventListener('message', (event) => {
    handleMessageEvent(event);
});

function primeCaches() {
    // ...
};
self.addEventListener('install', (event) => {
    primeCaches();
});

self.addEventListener('fetch', (event) => {
    event.respondWith(caches.match(event.request));
});

关键点:

  • self.addEventListener('message', ...):Service Worker 可以接收页面或其他上下文发送的消息。
  • self.addEventListener('install', ...):安装阶段适合准备缓存等基础状态。
  • primeCaches():示例里用它代表预热缓存的逻辑。
  • self.addEventListener('fetch', ...):Service Worker 拦截网络请求。
  • event.respondWith(caches.match(event.request)):示例从缓存里查找当前请求的响应。

08:42)页面端先用 feature detection 检查浏览器是否支持 Service Worker。已有注册就复用;没有注册再安装 BrowserPetsWorker.js

// BrowserPetsMain.js

var registration;
if ('serviceWorker' in navigator) {
    let registration = await navigator.serviceWorker.getRegistration();
    if (!registration)
        registration = await navigator.serviceWorker.register('BrowserPetsWorker.js');
}

关键点:

  • 'serviceWorker' in navigator:用功能检测判断当前浏览器是否支持 Service Worker。
  • navigator.serviceWorker.getRegistration():查询当前 scope 下是否已经有注册过的 Service Worker。
  • if (!registration):避免重复注册同一个 worker。
  • navigator.serviceWorker.register('BrowserPetsWorker.js'):把 worker 脚本安装到浏览器里。

订阅必须来自用户手势

02:49)Safari 不允许网站在用户没有主动操作时请求推送权限。session 的 demo 里,用户点击一个铃铛按钮后才出现系统通知授权弹窗。

09:00)Browser Pets 用按钮的 onclick 调用订阅函数。鼠标点击或键盘操作都满足 session 中提到的 user gesture 要求(04:32)。

// BrowserPetsMain.js

async function subscribeToPush() {
    // ...
}

// BrowserPetsMain.html

<button onclick="subscribeToPush()">Register for Updates</button>

关键点:

  • async function subscribeToPush():订阅流程包含异步权限请求和 PushSubscription 获取。
  • <button ...>:把入口放在用户能看见、能理解的界面控件上。
  • onclick="subscribeToPush()":订阅请求由点击事件触发,符合 Safari 的限制。
  • Register for Updates:按钮文案要让用户知道接下来会启用更新通知。

用 VAPID 配置 PushSubscription

09:19)用户点击按钮后,页面端调用 pushManager.subscribe()。Safari 使用标准 VAPID(Voluntary Application Server Identification)机制,让服务端向 Apple 的 push servers 标识自己(09:29)。

// BrowserPetsMain.js

async function subscribeToPush() {
    let serverPublicKey = VAPID_PUBLIC_KEY; 

    let subscriptionOptions = {
        userVisibleOnly: true,
        applicationServerKey: serverPublicKey
    };

    let subscription = await swRegistration.pushManager.subscribe(subscriptionOptions);

    sendSubcriptionToServer(subscription);
}

关键点:

  • VAPID_PUBLIC_KEY:服务端的 VAPID 公钥,用于标识发送推送的服务器。
  • userVisibleOnly: true:页面承诺每次 push 都会产生用户可见通知。
  • applicationServerKey: serverPublicKey:把 VAPID 公钥传给 Push API。
  • swRegistration.pushManager.subscribe(...):基于当前 Service Worker 注册创建推送订阅。
  • subscription:包含服务端发送推送所需的 endpoint 和加密密钥等信息。
  • sendSubcriptionToServer(subscription):把订阅信息传回自己的后端;具体传输方式由站点决定。

10:21subscribe() 会触发系统权限弹窗。用户可能拒绝,页面代码要处理失败路径。用户允许后,返回的 PushSubscription 里有精确 endpoint URL 和用于加密传输的 key(10:30)。

Service Worker 收到 push 后必须显示通知

11:13)服务器发出的 push 到达 Safari 后,Safari 唤醒 Service Worker,并发送 push 事件。Browser Pets 的服务端把通知需要的字段都放在 JSON payload 里,worker 读取后调用 showNotification()

// BrowserPetsWorker.js

self.addEventListener('push', (event) => {
    let pushMessageJSON = event.data.json();

    // Our server puts everything needed to show the notification
    // in our JSON data.
    event.waitUntil(self.registration.showNotification(pushMessageJSON.title, {
        body: pushMessageJSON.body,
        tag: pushMessageJSON.tag,
        actions: [{
            action: pushMessageJSON.actionURL,
            title: pushMessageJSON.actionTitle,
        }]
    }));
}

关键点:

  • self.addEventListener('push', ...):Service Worker 监听来自浏览器的 push 事件。
  • event.data.json():把服务器发送的 PushMessageData 按 JSON 读取。
  • event.waitUntil(...):告诉浏览器该异步任务属于这次 push 事件处理。
  • self.registration.showNotification(...):显示平台原生通知。
  • pushMessageJSON.title:通知标题来自服务端 payload。
  • bodytag:分别配置通知正文和通知标识。
  • actions:为通知配置操作项,这里把服务端给出的 URL 放进 action

这里有一个重要边界。session 多次强调,处理 push 事件不是给 JavaScript 静默后台运行时间(13:39)。Safari 不支持 silent push。处理 push 事件时必须向 Notification Center 发布通知(13:53)。在 macOS Ventura beta 中,如果三次 push 事件没有及时显示通知,站点的 push subscription 会被撤销(14:07)。

点击通知后打开目标页面

12:06)通知显示出来只是第一步。用户点击通知后,Safari 会把 notificationclick 事件发给 Service Worker。Browser Pets 的示例从通知 action 中取 URL,然后打开新窗口。

// BrowserPetsWorker.js

self.addEventListener('notificationclick', async function(event) {
    if (!event.action)
        return;

    // This always opens a new browser tab,
    // even if the URL happens to already be open in a tab.
    clients.openWindow(event.action);
});

关键点:

  • self.addEventListener('notificationclick', ...):监听用户点击通知后的事件。
  • if (!event.action) return;:没有 action URL 时不继续处理。
  • event.action:取出通知里配置的操作值,本例把它当作目标 URL。
  • clients.openWindow(event.action):打开指向该 URL 的新浏览器标签页。
  • 示例注释说明:这段代码总会打开新标签页,即使相同 URL 已经打开。

调试订阅和事件处理

12:28)Web Inspector 可以调试页面里的订阅代码,也可以检查 Service Worker 实例,并在事件处理器上设置断点。这样能覆盖两段关键代码:页面端调用 pushManager.subscribe() 的逻辑,以及 worker 里处理 pushnotificationclick 的逻辑。

Apple Push Notification servers 在发布 push message 出错时会返回 human readable errors(12:55)。调试时要同时看浏览器端和服务端错误,不要只盯页面 JavaScript。

核心启发

  • 做什么:给内容站增加按栏目订阅的 Web Push。 为什么值得做:session 建议在 Web App 内提供细粒度通知控制,用户可以选择博客更新、提交更新等不同类型(04:58)。 怎么开始:先注册 Service Worker,再把“订阅此栏目”按钮绑定到 pushManager.subscribe(),把 PushSubscription 和栏目 ID 一起传给后端。

  • 做什么:给内部运营工具增加后台任务完成提醒。 为什么值得做:Service Worker 可以在网站没有打开标签页时收到 push;macOS Ventura 上即使 Safari 没运行也能处理(07:29)。 怎么开始:任务完成后由服务端向保存的 endpoint 发送 push,worker 在 push 事件中用 showNotification() 展示任务名和结果页链接。

  • 做什么:为现有 Chrome/Firefox Web Push 站点补齐 Safari 支持。 为什么值得做:Safari 采用相同 Web 标准;已有标准实现通常不需要改 Safari 专用代码(01:16)。 怎么开始:移除浏览器嗅探,改用 'serviceWorker' in navigator 等 feature detection;服务端 endpoint allowlist 放开 push.apple.com 的子域名。

  • 做什么:把通知点击变成明确的深链接入口。 为什么值得做notificationclick 事件允许 worker 根据通知携带的数据打开对应页面(07:43)。 怎么开始:在 push JSON 里放入 actionURL,显示通知时写入 actions,点击时用 clients.openWindow(event.action) 打开详情页。

关联 Session

评论

GitHub Issues · utterances