跳转到内容

Service Worker · 概念

Service worker 调试

发布于

调试 service worker 就是去读页面自己显示不出来的三样东西:注册的状态(installing、waiting、active)、Cache Storage 的内容,以及哪些请求是由 worker 应答的。Chrome 40+ 与 Edge 79+ 在 DevTools › Application 中集中展示这三项;Firefox 44+ 把它们分散在 about:debugging 与 Storage 面板;Safari 11.1+ 在 Web Inspector 中列出注册,但没有缓存查看器,也没有强制更新控件(BCD api.ServiceWorker;厂商文档见来源)。

各家面板不同,但每个面板都只是同一个注册对象、同一个 worker 所见 caches 的一种视图,因此面板能做的事都可以在 worker 的控制台上下文里完成。

Application › Service workers 列出该源的每条注册,带 scope、脚本 URL 与一行状态,格式为 #<id> activated and is running、#<id> waiting to activate 或 #<id> trying to install。顶部三个复选框只改变当前标签页的行为:Offline 让 worker 内与页面中的 fetch() 像断网一样失败;Update on reload 在每次导航时强制逐字节比较并把抓取到的脚本作为新 worker 安装(绕过 24 小时脚本缓存规则);Bypass for network 把所有请求直接送往网络,不在 worker 中触发 fetch。每条注册有 Update、Unregister 链接,处于等待状态的 worker 旁还有 skipWaiting 链接,无需改代码即可对它调用 skipWaiting()。Application › Cache storage 以 <缓存名> - <源> 列出缓存,显示每个条目的请求 URL、响应头与正文预览,并支持按条目或按整个缓存删除。Network 面板中,由 worker 产生的响应在 Size 列显示 (ServiceWorker),worker 自己发出的请求带齿轮图标。Console 面板左上角的上下文选择器(默认 top)以脚本 URL 列出 worker;选中后,worker 内的 console.log 会显示在这里,也可以在 worker 的全局作用域中执行 await caches.keys() 或 await self.clients.matchAll()。

about:debugging#/runtime/this-firefox 在 Service Workers 下列出注册,带 scope、状态与两个按钮:Inspect 打开一个以 worker 为上下文的工具箱(其 Console 就是 worker 控制台),Unregister 注销。页面的工具箱也在 Application › Service Workers 中显示该 worker(Firefox 79+),已停止的 worker 旁有 Start 按钮。Firefox 没有“重新加载时更新”开关;等价做法是在页面控制台调用 registration.update(),或先 Unregister 再刷新。Cache Storage 出现在 Storage 面板的 Cache Storage 下,列出各缓存及其请求 URL,并可删除条目。

启用 Develop 菜单(Safari › 设置 › 高级 › Show features for web developers,Safari 17 之前的标签为 Show Develop menu in menu bar),再打开 Develop › Service Workers,它按 scope 列出注册,并打开一个附着到 worker 上下文的 Web Inspector 窗口。Storage 标签显示 IndexedDB 与 Local Storage,但不显示 Cache Storage,因此要列出缓存条目只能在 worker 控制台执行 await caches.keys() 与 await (await caches.open(name)).keys()。没有离线模拟与强制更新;改用 Network Link Conditioner(macOS)或飞行模式(iOS),以及 registration.update()。

下面每种故障都在某个面板中有可见的特征。

症状 显示位置 原因与修法
新代码没有运行 Chrome:第二个 worker 旁显示 waiting to activate 旧标签页仍被控制;关闭它们、点 skipWaiting,或在代码中提供 skipWaiting() 路径
部署被忽略最多一天 Network 面板显示脚本来自 (disk cache) 脚本被 HTTP 缓存;注册时传 updateViaCache: 'none',或给脚本 URL 加 Cache-Control: no-cache
请求没被拦截 Network 的 Size 列没有 (ServiceWorker) 页面在 scope 之外、worker 尚未激活,或开着 Bypass for network
安装后缓存为空 Cache storage 没有缓存,worker 控制台显示 addAll() 被拒绝 某个预缓存 URL 返回了非 2xx 状态;addAll() 是原子的,任一失败则什么都不存
安装卡住 状态停在 trying to install event.waitUntil() 收到了一个永不落定的 Promise

下面两段代码用脚本重现最常用的两个面板操作,因此在没有对应按钮的浏览器中同样可用。

在页面控制台强制更新检查并列出缓存内容

Section titled “在页面控制台强制更新检查并列出缓存内容”

registration.update() 执行的正是 “Update on reload” 触发的那次逐字节比较;页面中的 caches.keys() 列出的也正是 Application 面板显示的那些缓存,因为同一源上的页面与 worker 共享 caches。

const registration = await navigator.serviceWorker.getRegistration();
await registration.update(); // 抓取 /sw.js,有差异则安装
console.log(registration.waiting?.state); // 有更新在等待时为 "installed"
for (const name of await caches.keys()) {
const cache = await caches.open(name);
const keys = await cache.keys();
console.log(name, keys.map((request) => request.url));
}

页面侧的列举在没有缓存查看器的 Safari 中同样有效;代价是 opaque 响应只能看到 URL,它们的正文在任何上下文中都读不出来。

用一个调试开关记录每次 fetch 决策,生产环境下为空操作

Section titled “用一个调试开关记录每次 fetch 决策,生产环境下为空操作”

在 fetch 处理器里打日志,是 Workbox 开发构建日志的手工版本。以注册 URL 为开关可以让生产 worker 保持安静且无需构建步骤;console 不可用时(较老的 WebView)守卫直接什么都不做。

sw.js
const DEBUG = new URL(self.location.href).searchParams.has('debug'); // register('/sw.js?debug')
self.addEventListener('fetch', (event) => {
event.respondWith((async () => {
const cached = await caches.match(event.request);
if (DEBUG && typeof console !== 'undefined') {
console.log(cached ? 'cache' : 'network', event.request.method, event.request.url);
}
return cached ?? fetch(event.request);
})());
});

注册 /sw.js?debug 安装的是另一个脚本 URL,因而是另一个 worker;测量前记得改回不带查询串的注册,否则带日志的 worker 会一直控制该标签页。

规范

规范状态
无。