跳转到内容

Service Worker · 概念

Workbox

发布于

Workbox 是 Chrome 团队维护的一组 JavaScript 库,实现了 service worker 中原本要手写重复的路由、缓存、预缓存与更新消息代码;当前主版本为 7(2023-04 发布)。它只建立在标准的 Service Worker、Cache 与 Fetch API 之上,因此凡是有 service worker 的浏览器都能运行(Chrome 40、Firefox 44、Safari 11.1、Edge 17;BCD api.ServiceWorker),而可选模块在底层 API 缺失时会降级,例如 Firefox 与 Safari 中的 workbox-background-sync。

每个 workbox-* 包对应 service worker 的一项任务;worker 脚本只导入用到的部分,既可以经打包器从 npm 导入,也可以从 CDN 以 ES 模块方式导入。

包 取代手写 worker 中的哪部分
workbox-routing fetch 处理器里的 if/else 链:registerRoute(matcher, handler) 与 NavigationRoute
workbox-strategies 五种策略函数体:CacheFirst、NetworkFirst、StaleWhileRevalidate、NetworkOnly、CacheOnly,带 networkTimeoutSeconds 与插件钩子
workbox-precaching install/activate 中按内容修订号填充与清理缓存
workbox-expiration 缓存条目数与时长上限(maxEntries、maxAgeSeconds)
workbox-cacheable-response cache.put() 之前的 response.ok 检查
workbox-background-sync 在 sync 事件中重放的 IndexedDB 队列;没有 Background Sync 的浏览器中,队列在 worker 下次启动时重放
workbox-broadcast-update Stale-While-Revalidate 刷新改变了缓存响应时发出的 BroadcastChannel 消息
workbox-window 页面侧的注册、更新检测与 skipWaiting 消息传递

预缓存是无法在运行时手写的那部分。构建工具扫描输出目录,生成一个 { url, revision } 数组并替换 worker 中的 self.__WB_MANIFEST 标记;precacheAndRoute() 把每个文件存入名为 workbox-precache-v2-<scope> 的缓存,以 URL 为键、修订号作为 __WB_REVISION__ 查询参数附加其后,并为这些 URL 安装一条 Cache First 路由。更新时只抓取修订号变化的条目;activate 处理器删除不再出现在清单中的条目。文件名本身已含内容哈希的文件得到 revision: null,仅凭 URL 即可标识版本。

两种构建模式都能生成这份清单。GenerateSW(在 workbox-build、workbox-cli、workbox-webpack-plugin 与 vite-plugin-pwa 中)根据配置写出完整的 worker,在 worker 只需要预缓存与运行时缓存规则时已经够用;InjectManifest 接受你自己写的 worker 源文件,替换其中的 __WB_MANIFEST 标记,任何自定义的 fetch、push、sync 或 message 处理器都需要它。取舍在于:GenerateSW 每次构建都重新生成 worker,配置是唯一的事实来源;InjectManifest 让 worker 成为普通代码,代价是导入要自己维护。

页面侧,workbox-window 的 Workbox 类封装了 register(),并抛出 installed、waiting、controlling、activated 事件;wb.messageSkipWaiting() 发送一条 { type: 'SKIP_WAITING' } 消息,GenerateSW 生成的 worker 自带对应的 message 监听器,InjectManifest 的 worker 则要加上第一个示例中那四行监听代码。

第一个示例是一个完整的 InjectManifest worker,含预缓存与一条运行时路由;第二个是页面侧代码,包括浏览器没有 service worker 时的分支。

带预缓存与图片路由的 InjectManifest worker

Section titled “带预缓存与图片路由的 InjectManifest worker”

__WB_MANIFEST 标记在构建时被替换;ExpirationPlugin 限制运行时缓存的规模,预缓存则不需要它,因为清单本身已经限定了范围。

// src/sw.js:swSrc 文件,__WB_MANIFEST 由构建工具填入
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
cleanupOutdatedCaches(); // 删除旧版预缓存格式留下的缓存
precacheAndRoute(self.__WB_MANIFEST); // [{ url: '/index.html', revision: 'a1b2' }, …]
self.addEventListener('message', (event) => {
if (event.data?.type === 'SKIP_WAITING') self.skipWaiting(); // 与 workbox-window 配对
});
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 }),
],
})
);

statuses 中包含 0 会缓存 opaque 的跨源图片,Chromium 在配额核算中把每条按约 7 MB 计入;图片为同源或带 CORS 时应去掉 0,只存储真正的 200 响应。

Workbox.register() 兑现为注册对象,或以与 navigator.serviceWorker.register() 相同的错误拒绝。serviceWorker 不存在时页面继续以仅在线方式工作,所以把导入放在特性检测之后,避免白白加载模块。

page.js
if ('serviceWorker' in navigator) {
const { Workbox } = await import('workbox-window');
const wb = new Workbox('/sw.js');
wb.addEventListener('waiting', () => {
showBanner('新版本已就绪。', () => {
wb.addEventListener('controlling', () => window.location.reload());
wb.messageSkipWaiting();
});
});
wb.register();
} else {
// 没有 service worker:不显示更新横幅;应用直接走网络。
}

在 controlling 事件中刷新,而不是 messageSkipWaiting() 之后立即刷新,是为了等新 worker 完成接管,这样刷新后的页面由新的预缓存提供;过早刷新可能加载到旧 HTML 与新资源的混合。

规范

规范状态
无。