# Notification.requestPermission()

> Notification.requestPermission() 与 Notification.permission 如何工作、Firefox 72 与 Safari 强制的用户手势规则，以及 Firefox 拒绝时的精确控制台信息。

`Notification.requestPermission()` 询问用户是否允许该源显示系统通知，并以结果状态兑现；`Notification.permission` 随时读取该状态而不弹窗。同一个权限也把关 `ServiceWorkerRegistration.showNotification()`，并且因为推送订阅需要它，也把关 `PushManager.subscribe()`，所以这一个提示是 PWA 所有通知路径的闸门。

## 语法

```js
Notification.requestPermission()
Notification.permission
```

`requestPermission()` 是静态方法，返回 `Promise<NotificationPermission>`，字符串取值为 `"default"`、`"granted"` 或 `"denied"`。它还接受一个旧式回调参数 `Notification.requestPermission(callback)`，这是 Safari 15 之前唯一支持的形式（BCD `api.Notification.requestPermission_static`）。`Notification.permission` 是只读静态属性，取值相同。两者都只存在于窗口中；worker 没有 `Notification.requestPermission`，Service Worker 通过 `registration.pushManager.permissionState()` 或 `navigator.permissions.query({ name: 'notifications' })` 读取状态。

## 参数

| 参数 | 类型 | 含义 |
|---|---|---|
| `deprecatedCallback` | function，可选 | 用户做出决定时以权限字符串调用。为兼容 Safari 7 至 14 保留；新代码直接等待 Promise。 |

三个权限值并不对称。`"default"` 表示用户尚未决定，浏览器按 `"denied"` 处理；`"denied"` 表示用户或企业策略已拒绝，直到用户更改站点设置之前不会再弹出提示；`"granted"` 是唯一能让 `showNotification()` 成功的状态。

## 异常

`requestPermission()` 不会因拒绝而抛出，而是以 `"denied"` 兑现。下面的失败都发生在提示之前。

| 失败 | 位置 | 触发条件 |
|---|---|---|
| `ReferenceError` | 读取 `Notification` | iOS 与 iPadOS 上的 Safari 不定义该接口，除非页面作为 manifest 设置了非默认 `display` 的主屏幕 Web App 运行（BCD 对 `api.Notification` 的备注）。访问静态成员之前先测试 `'Notification' in window`。 |
| 不弹提示直接兑现 `"denied"`，并打出控制台信息 | Firefox 72+ | 调用不在用户产生的事件处理函数内。Firefox 记录 `The Notification permission may only be requested from inside a short running user-generated event handler.`（[dom.properties](https://github.com/mozilla-firefox/firefox/blob/main/dom/locales/en-US/chrome/dom/dom.properties) 中的 `NotificationsRequireUserGesture`，github.com）。 |
| 不弹提示直接兑现 `"denied"`，并打出控制台信息 | Firefox 70+ | 调用来自跨源 `<iframe>`：`The Notification permission may only be requested in a top-level document or same-origin iframe.` |
| 不弹提示直接兑现 `"denied"`，并打出控制台信息 | Firefox | 页面不是安全上下文：`The Notification permission may only be requested in a secure context.` |
| 提示被抑制，状态停留在 `"default"` | Chrome 80+ | Chrome 的静默权限界面用地址栏里一个带斜线的铃铛图标替代弹窗，针对习惯拒绝通知的用户和接受率低的站点（[Introducing quieter permission UI for notifications](https://blog.chromium.org/2020/01/introducing-quieter-permission-ui-for.html)，blog.chromium.org）；Promise 只在用户点击图标并做出选择后才兑现。 |

macOS 上的 Safari 同样要求用户手势，否则不弹提示直接兑现 `"denied"`。

## 示例

示例把「询问」（一次，来自手势）和「读取」（每次加载）分开，最后给出无法显示系统通知的平台的兜底。

### 解释原因后从按钮发起请求

请求在 `click` 处理函数内运行，满足 Firefox 72 与 Safari 的要求。拒绝后按钮仍然可见，用户可以更改站点设置后再试。

```js
const button = document.querySelector('#enable-alerts');

button.addEventListener('click', async () => {
  const state = await Notification.requestPermission();
  if (state === 'granted') {
    button.hidden = true;
    await showWelcomeNotification();
  } else {
    button.textContent = '通知已在浏览器设置中被拦截';
  }
});
```

不要在加载时或定时器里调用 `requestPermission()`：Firefox 会带着上面的控制台信息兑现 `"denied"`，而 Chrome 和 Safari 会按站点记住这次拒绝。

### 不弹窗读取状态

`Notification.permission` 是同步的且不会弹窗，所以它是决定是否渲染「启用」按钮的正确检查。Permissions API 以异步方式给出同样的答案，而且在 Service Worker 内也能用。

```js
function notificationState() {
  if (!('Notification' in window)) return 'unsupported';
  return Notification.permission; // "default" | "granted" | "denied"
}

async function notificationStateInWorker() {
  const status = await navigator.permissions.query({ name: 'notifications' });
  return status.state; // "prompt" | "granted" | "denied"
}
```

Permissions API 把未决定状态写作 `"prompt"`，`Notification.permission` 写作 `"default"`，两个词是同一个意思。

### 检测支持并退回页内提示

iOS 浏览器标签页里没有该接口，Android WebView 里完全没有，而在任何平台上用户都可能已经拒绝。一个函数覆盖三种情况，返回一个应用其余部分可以调用的通知器。

```js
async function getNotifier() {
  if (!('Notification' in window) || !('serviceWorker' in navigator)) {
    return showInPageToast; // 这里没有系统通知：在页面内渲染
  }
  if (Notification.permission !== 'granted') {
    return showInPageToast; // 未决定或已拒绝：不要在这里弹提示
  }
  const registration = await navigator.serviceWorker.getRegistration();
  if (!registration?.active) return showInPageToast;
  return (title, body) => registration.showNotification(title, { body });
}
```

页内 toast 是每个分支的兜底，所以无论平台给出什么答案，调用方都只面对一个签名。

:::observed
Firefox（英文界面）对被拒绝的请求在 Console 打出三条不同的信息，每条都是 [dom.properties](https://github.com/mozilla-firefox/firefox/blob/main/dom/locales/en-US/chrome/dom/dom.properties)（github.com）中的字符串：调用不在手势内时（Firefox 72+）为 `The Notification permission may only be requested from inside a short running user-generated event handler.`；来自跨源 frame 时（Firefox 70+）为 `The Notification permission may only be requested in a top-level document or same-origin iframe.`；纯 HTTP 下为 `The Notification permission may only be requested in a secure context.`。三种情况下 Promise 都兑现为 `"denied"`，且不出现权限气泡。
:::

## 另请参阅

- [Notifications API Standard: requestPermission()](https://notifications.spec.whatwg.org/#dom-notification-requestpermission)（whatwg.org）
- [Notification: requestPermission() static method](https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission_static)（developer.mozilla.org）
- [Notifications API](/zh/reference/notifications/notifications-api/)
- [Web Push 与 PushManager.subscribe()](/zh/reference/notifications/web-push/)
- [iOS 与 iPadOS 上的 Web Push](/zh/reference/notifications/ios-safari-push/)