# iOS 与 iPadOS 上的 Web Push（16.4+）

> iOS 与 iPadOS 16.4 为主屏幕 Web App 带来的 Web Push：安装前提、标签页里缺失的 Notification 接口、APNs 投递，以及 18.4 的 Declarative Web Push。

iOS 与 iPadOS 16.4（2023-03）为添加到主屏幕的 Web App 加入了 Web Push：与桌面端相同的 Push API、Notifications API 与 Service Worker 组合，经 Apple Push Notification service 投递，并且只有在用户安装应用并点按一个请求权限的控件之后才可用。在 iPhone 的 Safari 标签页里 `Notification` 接口并不存在，所以未安装的站点连「询问」都做不到。

## 工作原理

WebKit 把整个特性绑定在安装上。用户从分享菜单选择「添加到主屏幕」且 manifest 的 `display` 成员为 `standalone` 或 `fullscreen`（或 `minimal-ui`，任何非默认值）时，站点成为主屏幕 Web App；没有这个 manifest 值，图标只是一个在 Safari 中打开的书签，而 Safari 里没有推送（[Web Push for Web Apps on iOS and iPadOS](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/)，webkit.org）。自 16.4 起第三方浏览器也可以提供同样的分享菜单项，生成的应用无论由哪个浏览器添加都以 Web App 方式打开。

### Safari 标签页暴露了什么

browser-compat-data 把 iOS 上的 `Notification` 记为 16.4、部分支持，并附两条备注：除非页面是 manifest 带非默认 `display` 的主屏幕 Web App，否则接口未定义；通知只能由 Service Worker 发出（`api.Notification` 的 `safari_ios`）。两条都有代码后果。标签页里的 `Notification.requestPermission()` 抛出的是 `ReferenceError`，不是被拒绝的 Promise；`new Notification()` 在该平台上完全没有可用路径，`registration.showNotification()` 是显示通知的唯一方式。

### 权限、订阅与投递

在已安装的应用内，权限请求必须跟随直接的用户交互，例如点按订阅按钮；随后 iOS 显示系统通知提示，用户在「设置 > 通知」里按 Web App 管理结果，与原生应用一样。`pushManager.subscribe()` 要求 `userVisibleOnly: true` 和 VAPID `applicationServerKey`；返回的 endpoint 位于 `*.push.apple.com` 主机上，按白名单放行推送端点的服务器必须允许它。WebKit 会撤销 `push` 事件没有产生可见通知的订阅（[Meet Web Push](https://webkit.org/blog/12945/meet-web-push/)，webkit.org），所以吞掉消息或在 `showNotification()` 之前失败的处理函数付出的代价是整个订阅，而不是一条提醒。

通知与「专注模式」集成，manifest `id` 与用户所取名称相同的 Web App 在多台设备之间同步专注设置。Badging API（`navigator.setAppBadge()`）在同一版本为主屏幕 Web App 发布，通知权限授予后即显示角标。

### Declarative Web Push（18.4+）

iOS 与 iPadOS 18.4 为主屏幕 Web App 增加了第二种形态，Safari 18.5 把它带到 macOS：`window.pushManager`（BCD `api.Window.pushManager`：Safari 18.4）让页面无需 Service Worker 即可订阅，而符合声明式 JSON 格式（`"web_push": 8030` 加一个 `notification` 对象）的推送消息由浏览器直接显示，不运行任何 JavaScript。存在 Service Worker 时它仍可修改通知；若其处理函数失败，声明式消息作为兜底显示，所以上述撤销规则在这条路径上不会生效（[Meet Declarative Web Push](https://webkit.org/blog/16535/meet-declarative-web-push/)，webkit.org）。

### 支持位置

macOS Ventura 上的 Safari 16 最先发布 Web Push（2022-10），通过 `webpushd` 守护进程投递，Safari 无需运行。iOS 与 iPadOS 16.4 随后跟进，仅限主屏幕 Web App，本页的 `compat` 表同时跟踪两者。Android WebView 和其他应用内的 iOS WebView 没有推送；Android 上基于 Chromium 的浏览器在任何标签页里都有。

## 示例

示例在 iOS 上运行于已安装的应用内，在其他平台运行于任意标签页；第一个示例就是区分两者的守卫。

### 把未安装的 iPhone 访客引导到安装指引

`'Notification' in window` 这一测试在 iOS 上就是安装检查：标签页里为 `false`，主屏幕应用里为 `true`。它在其他平台上也是正确的第一步，所以不需要任何平台嗅探。

```js
async function enablePush(registration, applicationServerKey) {
  if (!('Notification' in window)) {
    showAddToHomeScreenGuidance(); // iOS 标签页或应用内嵌网页视图：先安装
    return null;
  }
  const permission = await Notification.requestPermission();
  if (permission !== 'granted') return null;
  return registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey });
}
```

从点按处理函数里调用 `enablePush()`；在 iOS 上，直接用户交互之外的调用不弹提示直接兑现 `"denied"`。

### 在 push 处理函数中兑现 userVisibleOnly 的承诺

每个 `push` 事件都必须以 `showNotification()` 结束，包括错误路径，否则 WebKit 撤销订阅。一条通用的兜底通知比什么都不显示更安全。

```js
self.addEventListener('push', (event) => {
  event.waitUntil((async () => {
    let payload = { title: '有新内容', body: '' };
    try {
      payload = event.data.json();
    } catch {
      // 载荷格式错误：仍然要显示点什么，否则订阅会被撤销
    }
    await self.registration.showNotification(payload.title, { body: payload.body });
  })());
});
```

这里的 `try`/`catch` 在 iOS 上比别处更重要：一个发送了错误格式 body 的服务端 bug，否则会悄悄让每一位 iPhone 用户退订。

### 存在声明式管理器时优先使用它

探测你将要使用的管理器，而不是探测 `PushManager` 接口。`window.pushManager` 只在声明式路径上存在；Service Worker 注册上的 `pushManager` 需要活跃的 worker。

```js
async function getPushManager() {
  if ('pushManager' in window) return window.pushManager; // Declarative Web Push，Safari 18.4+
  if (!('serviceWorker' in navigator)) return null; // 完全没有推送：只能用应用内消息
  const registration = await navigator.serviceWorker.getRegistration();
  return registration?.active ? registration.pushManager : null;
}
```

没有任何注册时 `getRegistration()` 以 `undefined` 兑现；`navigator.serviceWorker.ready` 在这种情况下会永远等待，这正是探测避开它的原因。

:::observed
在 iOS 16.4 或更高版本的 Safari 中，同一个 URL 在标签页里和作为主屏幕 Web App 打开时全局对象不同：标签页里 `typeof Notification` 求值为 `"undefined"`，`Notification.requestPermission()` 抛出 `ReferenceError: Can't find variable: Notification`（WebKit 对未定义标识符的标准措辞）；已安装应用里该静态成员存在，点按后出现提示。这一差异记录为 [Notification.json](https://raw.githubusercontent.com/mdn/browser-compat-data/main/api/Notification.json)（github.com）中 `api.Notification` 的 `safari_ios` 备注，也是确认 manifest `display` 值已被识别的最快方法。
:::

## 另请参阅

- [Web Push for Web Apps on iOS and iPadOS](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/)（webkit.org）
- [Meet Declarative Web Push](https://webkit.org/blog/16535/meet-declarative-web-push/)（webkit.org）
- [Push API](https://w3c.github.io/push-api/)（w3.org）
- [Web Push 与 PushManager.subscribe()](/zh/reference/notifications/web-push/)
- [iOS 添加到主屏幕](/zh/reference/installation/ios-add-to-home-screen/)
- [Notification.requestPermission()](/zh/reference/notifications/permissions/)
- [Badging API](/zh/reference/installation/badging/)