# Screen Wake Lock API

> navigator.wakeLock.request("screen") 在页面可见期间保持屏幕常亮。WakeLockSentinel、每种 NotAllowedError、隐藏时自动释放与 iOS 例外。

`navigator.wakeLock.request("screen")` 请求操作系统在文档保持可见期间不要调暗或锁定屏幕，并以一个 `WakeLockSentinel` 兑现；这把锁由你主动释放，页面一旦被隐藏浏览器也会替你释放。它适合菜谱页、演示文稿、实时追踪界面这类场景；对后台任务毫无作用，因为隐藏的文档既持有不了也申请不到锁。

Chrome 84、Edge 84、Firefox 126 与 macOS 上的 Safari 16.4 无附加条件地实现了该 API。iOS 上的 Safari 16.4 至 18.3 在 browser-compat-data 中记为 partial：在独立运行的主屏幕 Web App 中锁不起作用（[WebKit bug 254545](https://webkit.org/b/254545)），该条目把 iOS 18.4 列为首个完整支持的版本。

## 语法

```js
navigator.wakeLock.request()
navigator.wakeLock.request(type)

sentinel.release()
```

`request()` 返回 `Promise<WakeLockSentinel>`。sentinel 上的 `release()` 返回 `Promise<undefined>`，锁消失时（无论由谁释放）在 sentinel 上触发 `release` 事件。两个接口都带 `[SecureContext]` 且只暴露在 `Window` 上：在 `localhost` 以外的 `http://` 源上 `navigator.wakeLock` 为 `undefined`，worker 中则完全没有唤醒锁。

## 参数

`request()` 接受一个可选参数；它兑现的 sentinel 暴露三个成员和一个事件。

| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `type` | `WakeLockType`（枚举） | 否，默认 `"screen"` | 规范定义的唯一取值是 `"screen"`。曾讨论过 `"system"` 类型（熄屏但让 CPU 保持运行），后来被放弃；传入它违反 WebIDL 枚举约束。 |

| `WakeLockSentinel` 成员 | 类型 | 说明 |
|---|---|---|
| `released` | `boolean`（只读） | 持有锁期间为 `false`；`release()` 兑现后或浏览器释放后为 `true`。Chrome 87 加入该属性（Chrome 84 至 86 的 sentinel 没有它）。 |
| `type` | `WakeLockType`（只读） | 回显请求的类型，即 `"screen"`。 |
| `release()` | `Promise<undefined>` | 把这个 sentinel 从文档的活动锁列表中移除；同类型再无其他 sentinel 时释放平台锁。 |
| `release` 事件 | `Event` | 每个 sentinel 被释放时触发一次，包括浏览器在页面隐藏、文档卸载或系统干预时执行的释放。 |

多个 sentinel 可以同时存活；最后一个释放时平台锁才释放。`document.visibilityState` 变为 `"hidden"` 时浏览器会释放全部 sentinel，页面重新可见时不会自动恢复。

## 异常

`request()` 以 `DOMException` 或 `TypeError` 拒绝。规范定义的每一种拒绝都使用同一个名称。

| 异常 | 条件 |
|---|---|
| `NotAllowedError` | 文档不是 fully active；或 `screen-wake-lock` Permissions Policy 拒绝了该文档（默认允许列表为 `'self'`）；或浏览器拒绝为该文档提供此类型的锁；或调用时、或权限检查结束时 `document.visibilityState` 为 `"hidden"`；或 `screen-wake-lock` 权限的结果为 `"denied"`。 |
| `TypeError` | `type` 不是 `WakeLockType` 枚举的成员（WebIDL 转换在算法开始前就失败）。 |

电量低或处于省电模式是规范允许浏览器拒绝锁的理由（第 12 节 Security considerations），在 Chromium 中这种拒绝表现为 `NotAllowedError`。更底层的操作系统失败则被刻意隐藏：规范注明被系统拒绝的锁与成功获取的锁不可区分，因此 Promise 照样兑现，`released` 保持 `false`。`release()` 不会拒绝；对已释放的 sentinel 调用它会立即兑现。

:::observed
Chrome 在后台标签页中调用 `navigator.wakeLock.request("screen")` 时以 `NotAllowedError: The requesting page is not visible` 拒绝；文档受 `Permissions-Policy: screen-wake-lock=()` 约束时以 `NotAllowedError: Access to Screen Wake Lock features is disallowed by permissions policy` 拒绝；`navigator.wakeLock.request("system")` 则抛出 `TypeError: Failed to execute 'request' on 'WakeLock': The provided value 'system' is not a valid enum value of type WakeLockType.`。三条字符串见 Chromium 的 [`wake_lock.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/wake_lock/wake_lock.cc)（chromium.googlesource.com）。
:::

## 示例

两个示例都先检测 `navigator.wakeLock`，缺失时（Firefox 126 之前、Safari 16.4 之前、去掉了该权限的 Android WebView 构建）执行回退分支。

### 健身计时期间保持屏幕常亮，切换标签页后重新获取

页面一旦隐藏锁就会丢失，所以 `visibilitychange` 监听器在页面回到前台且训练仍在进行时重新申请。API 不存在时，应用显示一行提示，而不是任由屏幕悄悄熄灭。

```js
let sentinel = null;
let workoutRunning = false;

async function keepScreenOn() {
  if (!('wakeLock' in navigator)) {
    document.querySelector('#hint').textContent =
      '请在系统设置中调长屏幕超时时间；此浏览器没有唤醒锁。';
    return;
  }
  try {
    sentinel = await navigator.wakeLock.request('screen');
    sentinel.addEventListener('release', () => {
      sentinel = null;
    });
  } catch (err) {
    console.warn(`${err.name}: ${err.message}`);
  }
}

document.querySelector('#start').addEventListener('click', async () => {
  workoutRunning = true;
  await keepScreenOn();
});

document.querySelector('#stop').addEventListener('click', async () => {
  workoutRunning = false;
  await sentinel?.release();
});

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible' && workoutRunning && !sentinel) {
    keepScreenOn();
  }
});
```

浏览器已经释放锁之后调用 `sentinel?.release()` 是安全的：`release` 监听器已把 `sentinel` 置为 `null`，可选链会跳过这次调用。

### 让复选框跟随真实的锁状态

声称"屏幕正保持常亮"的控件必须跟随 sentinel 而不是跟随点击，因为浏览器可能在用户没碰复选框的情况下释放锁。在 `release` 处理函数里读取 `released` 能让两者保持一致。

```js
const box = document.querySelector('#keep-awake');
let sentinel = null;

box.disabled = !('wakeLock' in navigator);

box.addEventListener('change', async () => {
  if (box.checked) {
    try {
      sentinel = await navigator.wakeLock.request('screen');
      sentinel.addEventListener('release', () => {
        box.checked = !sentinel.released; // 浏览器放手后变为 false
      });
    } catch (err) {
      box.checked = false; // 被拒绝：页面隐藏、策略拒绝或权限被拒
    }
  } else {
    await sentinel?.release();
  }
});
```

`navigator.wakeLock` 缺失时直接禁用复选框，用户一开始就知道这里没有该选项，而不是第一次切换时才失败。

## 另请参阅

- [Media Session API](/zh/reference/capabilities/media-session/)，播放界面同时需要锁屏控件时的搭档
- [Idle Detection API](/zh/reference/capabilities/idle-detection/)，用于响应用户离开，而不是阻止屏幕锁定
- [iOS 与 Safari 上的 PWA](/zh/reference/platforms/ios-safari/)，主屏幕 Web App 的例外情况记录在那里
- [Screen Wake Lock API: request() method](https://www.w3.org/TR/screen-wake-lock/#the-request-method)（w3.org）
- [Screen Wake Lock API: Security considerations](https://www.w3.org/TR/screen-wake-lock/#security-considerations)（w3.org）
- [WebKit bug 254545: Screen Wake Lock in Home Screen web apps](https://webkit.org/b/254545)（webkit.org）
- [Stay awake with the Screen Wake Lock API](https://developer.chrome.com/docs/capabilities/web-apis/wake-lock)（developer.chrome.com）