跳转到内容

Service Worker · 概念

Service worker 注册与 scope

发布于

一条 service worker 注册把一个脚本 URL 与一个 scope(同源上的 URL 前缀)配对;浏览器会把 URL 以该前缀开头的每一次导航和子资源请求交给这个 worker 处理。register() 自 Chrome 40、Firefox 44、Safari 11.1、Edge 17 起行为未变(BCD api.ServiceWorkerContainer.register),updateViaCache 选项则自 Chrome 68、Firefox 57、Safari 11.1 起可用。

navigator.serviceWorker.register(scriptURL, { scope, type, updateViaCache }) 返回 Promise<ServiceWorkerRegistration>。这个调用是幂等的:用相同脚本与 scope 重复调用会直接兑现为已有注册、不会触发安装,因此页面可以在每次加载时都调用它。同一 scope 换一个脚本 URL 会替换注册(新 worker 走正常的安装与等待流程);换一个 scope 则创建第二条注册。

scope 默认为脚本所在目录:/sw.js 得到 /,/app/sw.js 得到 /app/。注册可以用选项收窄 scope(/app/sw.js 配 scope: '/app/admin/'),但不能把它放宽到脚本目录之外,除非返回脚本的响应带有指明更宽路径的 Service-Worker-Allowed 头。没有这个头时,register('/app/sw.js', { scope: '/' }) 会以 SecurityError 拒绝,Chromium 的原文是 The path of the provided scope ('/') is not under the max scope allowed ('/app/'). Adjust the scope, move the Service Worker script, or use the Service-Worker-Allowed HTTP header to allow the scope.。

scope 匹配是对 URL 的纯字符串前缀比较,不按路径段比较:scope /app 也会匹配 /application/,所以 scope 应以斜杠结尾。同一源上多条注册重叠时,浏览器为每个请求选择匹配 scope 最长的那条,因此对 /app/admin/users 而言 /app/admin/ 胜过 /app/。scope 只限制 worker 控制哪些客户端;worker 自己发出的 fetch() 不受限制。

updateViaCache 决定更新检查时如何使用 HTTP 缓存。'imports'(默认)抓取主脚本时绕过 HTTP 缓存,但允许 importScripts() 的依赖来自缓存;'all' 两者都允许走缓存;'none' 两者都绕过。无论此设置为何,浏览器对主脚本都忽略超过 24 小时的 HTTP 新鲜度。type: 'module' 选项(Chrome 91、Firefox 114、Safari 15)允许脚本使用 import 语句;模块 worker 不能调用 importScripts()。

注册要求安全上下文:除 localhost 外的纯 HTTP 源上 navigator.serviceWorker 为 undefined,因此 'serviceWorker' in navigator 这个特性检测同时也是 HTTPS 检测。脚本必须以 JavaScript MIME 类型提供;Chromium 会拒绝 text/html(404 页面或 SPA 兜底路由的常见结果),报错为 Failed to register a ServiceWorker for scope ('https://example.com/') with script ('https://example.com/sw.js'): The script has an unsupported MIME type ('text/html').。

第一个示例以显式 scope 注册并区分上述两种拒绝原因;第二个示例展示两个 worker 共享一个源,以及如何确认是哪一个在控制页面。

在 load 事件之后注册,可以让脚本抓取不占首屏渲染的关键路径。catch 分支通过错误名与信息区分 MIME 类型失败(部署问题)与 scope 失败(配置问题)。

if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/',
updateViaCache: 'none',
});
console.log('scope:', registration.scope); // "https://example.com/"
} catch (err) {
if (err.name === 'SecurityError') {
console.error('scope not allowed; serve Service-Worker-Allowed or move sw.js', err.message);
} else {
console.error('registration failed', err.message); // MIME 类型、网络或解析错误
}
}
});
} else {
// 非安全源或不支持的浏览器:应用保持仅在线模式。
}

updateViaCache: 'none' 的代价是每次进入 scope 的导航都要为脚本及其导入发一次不走缓存的请求;收益是脚本上的 Cache-Control: max-age=31536000 头不再能把更新拖延最多一天。

同一源下的营销站与应用可以各跑一个 worker,使一方的缓存变更不会让另一方失效。最长 scope 规则为每个请求选路;页面中的 navigator.serviceWorker.controller.scriptURL 可以确认结果。

await navigator.serviceWorker.register('/sw-site.js', { scope: '/' });
await navigator.serviceWorker.register('/app/sw-app.js', { scope: '/app/' });
// 在 /app/dashboard 页面刷新后:
console.log(navigator.serviceWorker.controller.scriptURL); // ".../app/sw-app.js"
// 枚举该源上的全部注册
const all = await navigator.serviceWorker.getRegistrations();
console.log(all.map((r) => r.scope));

两个 worker 的代价是要同时推理两套安装与更新周期,而且 Cache Storage 命名空间默认并不隔离(两者看到同一个 caches,缓存名不能冲突)。改为在 /sw.js 一个 worker 内用 fetch 处理器做路由可以避免这一点,代价是脚本更大。

规范

规范状态
无。