跳转到内容

Service Worker · API

Clients API

发布于

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

// 仅在 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 中调用。

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

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

Section titled “通知点击时聚焦已打开的窗口,否则新开一个”

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

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()

Section titled “在页面侧检测控制状态并等待 claim()”

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

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,标记会一直隐藏到下一次导航;这就是保留默认接管行为所付出的代价。

规范

规范状态
无。