# 添加推送通知

> 把 Service Worker 订阅到推送服务，在合适的时机请求通知权限，并在 push 事件到达时展示消息。

import { Steps } from '@astrojs/starlight/components';

**目标：** 即使应用不在前台，服务器也能把消息送达用户。据 MDN，Push API 让 Web 应用能够接收
服务器推送来的消息，无论该 Web 应用是否处于前台，甚至是否已被加载；据 MDN，Service Worker
会在需要时被启动以处理到达的推送消息。

这里涉及两个彼此独立、却常被混淆的 API。推送是传输层：据 MDN，它把消息投递到 worker 的
`push` 事件。通知是展示层：据 MDN，worker 对消息的响应方式之一，就是用
`ServiceWorkerRegistration.showNotification()` 显示一条通知。

## 浏览器与生态支持

据 MDN，Push API 属于 Baseline「广泛可用」——在大量设备与浏览器版本上已经成熟，并自 2023 年
3 月起在各浏览器中可用。据 MDN，`PushManager.subscribe()` 在部分或全部支持它的浏览器中仅在
安全上下文（HTTPS）可用。较弱的一环是 `Notification.requestPermission()`：据 MDN，该静态方法
**不属于** Baseline，因为它在一些使用最广泛的浏览器中并不工作。

## 步骤

<Steps>

1. **先有一个激活的 Service Worker。** 据 MDN，应用要接收推送消息，必须拥有一个激活的
   Service Worker；激活之后，它才能通过 `PushManager.subscribe()` 订阅。若还没有，请先看
   [入门](/zh/guides/getting-started/)。

2. **在用户手势中请求权限。** 据 MDN，`Notification.requestPermission()` 返回一个 Promise，
   其结果为 `granted`、`denied` 或 `default`，并且该请求应当在用户交互的响应中发起。据 MDN，
   当取值为 `default` 时，用户的决定未知，此时应用会按照权限被拒绝来处理。

3. **订阅同样要来自用户手势。** 据 MDN，`subscribe()` 调用应当在用户手势（例如点击按钮）的
   响应中完成；这不仅是最佳实践，也是因为浏览器正在明确禁止非用户手势触发的通知，MDN 指出
   Firefox 自 72 版起已经这样做。

4. **把订阅信息发给你的服务器。** 据 MDN，得到的 `PushSubscription` 包含应用发送推送消息所
   需的全部信息：一个 endpoint，以及发送数据所需的加密密钥。

5. **在 worker 中处理 `push` 并展示内容。** 据 MDN，到达的推送消息被投递给 `push` 事件处理
   函数，它可以通过 `ServiceWorkerRegistration.showNotification()` 显示通知来作出响应。

</Steps>

## 特性检测与回退

据 MDN，`ServiceWorkerRegistration.pushManager` 是使用推送消息的入口，并返回对 `PushManager`
接口的引用——因此在展示订阅界面之前，先检测这个接口：

```js
async function subscribeToPush(applicationServerKey) {
  if (!('PushManager' in window)) {
    // 不支持推送：保持订阅界面隐藏，回退到你已有的应用内渠道
    // （例如站内收件箱视图，或邮件摘要）。
    return null;
  }

  const permission = await Notification.requestPermission();
  if (permission !== 'granted') return null;

  const registration = await navigator.serviceWorker.ready;
  return registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey,
  });
}
```

据 MDN，`applicationServerKey` 是一个 Base64 编码的字符串或 `ArrayBuffer`，内含一个 ECDSA
P-256 公钥，推送服务器用它来认证你的应用服务器；一旦指定，你的应用服务器发出的所有消息都
必须使用 VAPID 认证方案，并带上用对应私钥签名的 JWT。据 MDN，该密钥**不是**你用来加密数据
的那个 ECDH 密钥。

在 worker 中，把消息变成一条通知：

```js
self.addEventListener('push', (event) => {
  const body = event.data ? event.data.text() : '你有一条新消息。';
  event.waitUntil(self.registration.showNotification('新消息', { body }));
});
```

## 实践清单

- **`userVisibleOnly: true` 实际上不可省略。** 据 MDN，`applicationServerKey` 在 Chrome、
  Edge 等部分浏览器中是必需的，并且当 `userVisibleOnly` 未设为 `true` 时它们会拒绝该
  Promise。因此请按「每条推送都对应一条可见通知」来设计，而不是静默的后台投递。
- **把 endpoint 当作机密。** 据 MDN，订阅 endpoint 是一个唯一的能力 URL：知道这个 endpoint
  就足以向你的应用发送消息，因此必须保密，否则其他应用也能向你的用户推送。
- **订阅时要防 CSRF。** MDN 明确警告：实现 `PushManager` 订阅时，防范应用中的 CSRF/XSRF
  问题至关重要。
- **投递量并非到处都无限制。** 据 MDN，唤醒 Service Worker 投递推送消息会增加资源消耗，尤其
  是电量，而目前尚无标准机制来处理这一点：Firefox 对每个应用允许有限的推送消息配额（每次
  访问站点时刷新，且会产生通知的消息不计入该上限），Chrome 则没有限制。
- **订阅会过期、会变化。** 据 MDN，当推送订阅已失效或即将失效时（例如推送服务设置了过期
  时间）会触发 `pushsubscriptionchange` 事件——所以要把已保存的订阅当作可能失效的东西看待，
  并在失效时重新订阅。
- **每个订阅只属于一个 worker。** 据 MDN，每个订阅对某个 Service Worker 是唯一的。
- **拒绝是明确的。** 据 MDN，`denied` 表示用户已明确拒绝当前源展示系统通知的权限；MDN 自己的
  示例也注明，这种情况下应当保持尊重，不必再去打扰用户。

## 下一步

- [Web Push 参考](/zh/reference/notifications/web-push/) —— 以参考形式呈现的协议与订阅模型。
- [Notifications API 参考](/zh/reference/notifications/notifications-api/) —— 展示层的一半，
  包括 `showNotification()` 接受哪些参数。
- [通知权限](/zh/reference/notifications/permissions/) —— 权限模型的细节。
- [iOS Safari 推送](/zh/reference/notifications/ios-safari-push/) —— 本站的一篇平台专项条目。

← 返回[指南](/zh/guides/)总览。