# Clients API

> self.clients 暴露的 Clients 接口及其 get()、matchAll()、openWindow()、claim() 成员，抛出 InvalidAccessError 的条件，以及通知点击聚焦窗口的模式。

`Clients` 只在 service worker 内以 `self.clients` 暴露，是 worker 看待它所控制（或可以控制）的文档与 worker 的视角：查找它们、聚焦它们、打开新窗口，以及接管在 worker 激活之前就已加载的页面。该接口与 service worker 本身同期发布，自 Chrome 40、Edge 17、Firefox 44、Safari 11.1 起可用（BCD `api.Clients`）。

## 语法

```js
// 仅在 service worker 内
const client = await self.clients.get(id);
const clients = await self.clients.matchAll(options);
const windowClient = await self.clients.openWindow(url);
await self.clients.claim();
```

四个方法都返回 Promise。`self.clients` 是一个 `Clients` 对象；每个结果是 `Client`（worker 与文档）或 `WindowClient`（文档），后者额外提供 `focused`、`visibilityState`、`focus()` 与 `navigate()`。

## 成员

该接口有四个方法，没有属性。

| 成员 | 返回值 | 行为 |
|---|---|---|
| `get(id)` | `Promise<Client \| undefined>` | 以 `id` 匹配的客户端兑现（id 来自 `FetchEvent.clientId`、`ExtendableMessageEvent.source.id` 或先前的 `matchAll()`），没有匹配时为 `undefined`。 |
| `matchAll(options)` | `Promise<Client[]>` | 所有匹配 `options` 的客户端，最近聚焦的排在前面。`options.type` 可为 `'window'`（默认）、`'worker'`、`'sharedworker'` 或 `'all'`；`options.includeUncontrolled`（默认 `false`）会加入同源但不受本 worker 控制的客户端。 |
| `openWindow(url)` | `Promise<WindowClient \| null>` | 在 `url` 打开一个顶层浏览上下文。新窗口同源时以其客户端兑现，跨源时为 `null`，并且只在 worker 正在处理 `notificationclick` 这类用户发起的事件时才会成功。 |
| `claim()` | `Promise<void>` | 让处于 active 状态的 worker 成为 scope 内所有未被其他 worker 控制的客户端的控制者，并在每个页面的 `navigator.serviceWorker` 上触发 `controllerchange`。 |

在 worker 激活之前打开的页面不受它控制；不刷新就接管这些页面，`claim()` 是唯一途径。它与 `skipWaiting()` 搭配会在页面打开状态下更换 worker，所以更新流程条目把这一组合视为需要显式选择的行为。

## 异常

拒绝原因都是 `DOMException` 实例；`get()` 从不拒绝。

- `openWindow()` 的 `InvalidAccessError`：调用没有发生在用户发起的事件期间。Chromium 的报错是 `Not allowed to open a window.`；`notificationclick` 处理函数之外的任何调用都适用，包括 `push` 处理函数，以及 `notificationclick` 内部 `setTimeout` 回调里的调用。
- `openWindow()` 的 `TypeError`：`url` 相对于 worker 地址不是合法 URL，或者是 `about:blank`。
- `claim()` 的 `InvalidStateError`：该 worker 不是注册上处于 active 状态的 worker，例如在 `install` 中调用。

:::observed
在 Chrome（英文界面）中，从 `push` 事件处理函数调用 `self.clients.openWindow('/chat/')`，或在 `notificationclick` 中先 `await` 一个超出用户激活窗口的无关 `fetch()` 再调用，都会以 `InvalidAccessError: Not allowed to open a window.` 拒绝，这是 Chromium `service_worker_clients.cc` 中的字符串常量。同样的调用若在 `notificationclick` 的 `event.waitUntil()` 内同步发出，则兑现为一个 `WindowClient`，其 `url` 是绝对 URL，`focused` 为 `true`。
:::

## 示例

第一个示例运行在 service worker 中，是该接口的典型用法；第二个运行在页面中，展示如何检测 worker 是否已控制页面。

### 通知点击时聚焦已打开的窗口，否则新开一个

优先复用已有窗口而不是新开：寻找已经位于目标路径的客户端，聚焦它，只有一个都没有时才调用 `openWindow()`。把工作放在 `waitUntil()` 内，可以让 `openWindow()` 所需的用户激活窗口保持有效。

```js
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  event.waitUntil((async () => {
    const all = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
    const existing = all.find((c) => new URL(c.url).pathname === '/chat/');
    if (existing) {
      await existing.focus();
      existing.postMessage({ type: 'open-thread', id: event.notification.data.threadId });
      return;
    }
    const opened = await self.clients.openWindow(`/chat/?thread=${event.notification.data.threadId}`);
    if (opened === null) console.warn('Window opened cross-origin; cannot message it');
  })());
});
```

需要 `includeUncontrolled: true`，因为在上一个 worker 处于 active 状态时从通知打开的标签页，可能还不受本 worker 控制。对 `WindowClient` 调用 `postMessage()`，消息会到达页面 `navigator.serviceWorker` 的 `message` 事件。

### 在页面侧检测控制状态并等待 `claim()`

`claim()` 在 worker 中执行，但效果体现在页面上。首次加载与强制刷新后 `navigator.serviceWorker.controller` 为 `null`；下面的页面只在存在控制者之后才启用离线 UI，否则按仅在线站点运行。

```js
function enableOfflineUi() {
  document.querySelector("#offline-badge").hidden = false;
}

if (!("serviceWorker" in navigator)) {
  // 完全没有 service worker：仅在线，无需等待。
} else if (navigator.serviceWorker.controller) {
  enableOfflineUi();
} else {
  // 尚未受控：worker 中的 claim() 会在这里触发 controllerchange。
  navigator.serviceWorker.addEventListener("controllerchange", enableOfflineUi, { once: true });
}
```

worker 中若没有 `claim()`，本次加载不会触发 `controllerchange`，标记会一直隐藏到下一次导航；这就是保留默认接管行为所付出的代价。

## 另请参阅

- [Service Workers specification: Clients interface](https://w3c.github.io/ServiceWorker/#clients-interface)（w3c.github.io）
- [Clients: openWindow() method](https://developer.mozilla.org/en-US/docs/Web/API/Clients/openWindow)（developer.mozilla.org）
- [Service worker 生命周期](/zh/reference/service-worker/lifecycle/)
- [Service worker 更新流程与 skipWaiting()](/zh/reference/service-worker/update-skipwaiting/)
- [Web Push](/zh/reference/notifications/web-push/)
- [通知操作与徽标](/zh/reference/notifications/notification-actions-badge/)