# 通知权限模型

> Notification.permission 的三种状态如何工作、为何 requestPermission() 需要用户手势，以及为何 MDN 建议用户拒绝后就不要再提示。

import CompatTable from '@components/CompatTable.astro';

**一句话：** `Notification.permission` 会报告三种状态之一——`default`、`granted`
或 `denied`——根据 MDN 的说明，代码只应在真实的用户手势响应中调用
`Notification.requestPermission()`，绝不应在页面加载时调用。

## 三种状态

根据 MDN 的 `Notification.permission` 参考文档：

- **`default`** —— 用户尚未做出选择；在用户授予权限之前，浏览器会将其当作
  `denied` 处理。
- **`granted`** —— 用户已同意；在桌面端，页面可以使用 `Notification` 构造函数
  构造通知（根据 MDN 的说明，大多数移动端浏览器会抛出异常，需改用
  `ServiceWorkerRegistration.showNotification()`）。
- **`denied`** —— 用户已拒绝；通知将不会显示。

## 从用户手势中请求权限

根据 MDN 的《使用通知 API》指南，权限请求应在点击等用户手势响应中发出，
浏览器正越来越多地拒绝并非由手势触发的请求——MDN 指出 Firefox 72+ 和
Safari 已经实施了这一限制。应在手势响应中询问，而不是在加载时：

```js
document.getElementById('enable-notifications').addEventListener('click', async () => {
  if (!('Notification' in window)) {
    // 兜底：此浏览器完全没有 Notification API。
    return;
  }
  const permission = await Notification.requestPermission();
  if (permission !== 'granted') {
    // 兜底：用户拒绝（或直接关闭）了提示——不要当作已授权继续执行。
    return;
  }
  await showEnabledNotification();
});

async function showEnabledNotification() {
  // 根据 MDN 的说明，大多数移动端浏览器虽然暴露了 `Notification`，但其构造
  // 函数会抛出 TypeError，需改用
  // `ServiceWorkerRegistration.showNotification()`——因此在有已注册的
  // service worker 时优先走这条路径，而不是假定构造函数在所有已授权场景
  // 下都能用。
  const registration = 'serviceWorker' in navigator
    ? await navigator.serviceWorker.getRegistration()
    : undefined;
  if (registration) {
    await registration.showNotification('通知已开启');
    return;
  }
  try {
    new Notification('通知已开启');
  } catch {
    // 兜底：没有已注册的 service worker，且构造函数抛出了异常（大多数
    // 移动端浏览器如此）——没有已注册的 service worker 就无法再展示通知。
  }
}
```

## 被拒绝后就不要再问

MDN 自己在 `requestPermission()` 示例代码中的注释写道，用户一旦拒绝通知，
"就没有必要再打扰他们了"——不要按计划反复调用 `requestPermission()` 指望得到
不同的答案。MDN 自己的《使用通知 API》指南则采取了更轻的做法，在用户拒绝后
仍保留启用按钮可见，让用户之后有机会改变主意；无论采用哪种方式，请求本身
都不应自动重复。

## 推送订阅并不完全依赖 service worker

根据 Push API 规范，`PushManager` 在 `Window` 和 `ServiceWorkerRegistration`
上都可用：`Window` 上的 `PushManager` 关联的 service worker 注册为 `null`，
而 `ServiceWorkerRegistration` 上的 `PushManager` 则绑定到该注册。规范还定义了
可在窗口上访问的推送订阅，并允许声明式推送消息在不经过页面自身脚本的情况下展示通知。
通过 `subscribe()` 请求推送订阅是独立于上述 `Notification` 权限的另一套权限流程。

## 支持情况

根据 MDN 的浏览器兼容性数据，`Notification.permission` 与
`requestPermission()` 属于**有限可用（Limited availability）**——不属于
Baseline，因为它们在一些广泛使用的浏览器中表现并不一致。桌面端支持较为
广泛（Chrome 32+、Edge 14+、Firefox 22+、Safari 7+），Android 上的
Chrome、Firefox 与 Samsung Internet 对权限属性与 `requestPermission()`
的支持同样完整。Android 上的 WebView 完全不支持权限相关 API。iOS 上的
Safari 仅对用户已添加到主屏幕的 Web App 开放（16.4+），普通浏览器标签页
无法使用。只在用户手势中请求权限是 MDN 给出的建议、也是部分浏览器已经
强制的行为，而非规范本身的要求。

需要注意的是，另一个独立的 `Notification()` 构造函数在部分浏览器上有其
自身的 Android 限制，但这不在本兼容性表的覆盖范围内，因为它并不适用于
这里介绍的权限属性与 `requestPermission()`。

<CompatTable feature="notification-permission" />

各浏览器对相关 API 的具体支持情况参见
[通知 API](/zh/reference/notifications/notifications-api/) 与
[Web Push](/zh/reference/notifications/web-push/)。

## 实践清单

- [ ] 只在用户手势的事件处理函数中调用 `requestPermission()`，绝不在页面加载时调用。
- [ ] 将 `default` 当作"尚未获得许可"处理——不要假定请求一定会成功。
- [ ] 一旦 `Notification.permission` 变为 `denied`，就不要再按计划反复调用
      `requestPermission()` 指望得到不同的答案。
- [ ] 在页面代码中使用该 API 前先做特性检测 `'Notification' in window`——
      根据 MDN 的说明，`Notification.permission` 在没有 `window` 的
      Web Worker 中也可访问。
- [ ] 不要假定推送订阅一定需要激活的 service worker 注册——Push API 还定义了
      窗口范围的路径。

## 延伸阅读

- [通知 API](/zh/reference/notifications/notifications-api/)
- [Web Push](/zh/reference/notifications/web-push/)
- [Notification 接口](/zh/reference/notifications/notification/)