install、activate、fetch:先建立一个可控的 Worker
Service Worker(SW)不是“后台线程版脚本”,而是由浏览器管理、按作用域接管请求的独立 worker。页面只能通过 navigator.serviceWorker.register() 注册;worker 没有 DOM,不能读取 document。本节先做一个最小离线 shell,再解释三个生命周期事件为什么必须把异步工作交给 waitUntil。
1. 注册与作用域
把下面文件放到同一静态站点的根目录,用 python3 -m http.server 4173 启动,再打开 http://localhost:4173/。localhost 是安全上下文;部署时必须使用 HTTPS。
<!-- index.html -->
<h1 id="status">网络加载</h1>
<script>
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then(reg => {
document.querySelector('#status').textContent =
reg.active ? 'Worker 已激活' : 'Worker 正在安装';
})
.catch(error => {
document.querySelector('#status').textContent = `注册失败:${error.message}`;
});
</script>/sw.js 的默认作用域是它所在目录;显式 scope: '/' 只有在脚本路径允许时才生效。第一次加载时,当前页面通常还没有被新 worker 控制,刷新一次后再看 navigator.serviceWorker.controller,不要把“注册成功”误当成“本页已经被拦截”。
2. install 是准备阶段
install 只针对这个 worker 版本执行一次。把异步预缓存 Promise 传给 event.waitUntil(),浏览器才会等待它并把拒绝视为安装失败。安装失败比带着缺文件的半成品继续运行更安全。
// sw.js
const VERSION = 'shell-v1';
const SHELL = ['/', '/index.html', '/offline.html'];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(VERSION).then(cache => cache.addAll(SHELL))
);
});先在站点根目录增加一个 offline.html。改错其中一个路径后刷新 DevTools 的 Service Workers 面板,观察安装失败;修正路径并重新注册,安装才会再次尝试。这个错误对照说明了 waitUntil 的成功/失败契约。
3. activate 负责接管和清理
新 worker 安装成功后可能处于 waiting,直到旧 worker 不再控制页面。activate 是删除旧缓存、迁移 worker 自己拥有的存储的安全位置:此时浏览器保证旧 worker 已退出。清理时只删除本应用的前缀,不能用 caches.keys() 后无条件全删。
self.addEventListener('activate', event => {
const keep = new Set([VERSION]);
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.filter(key => key.startsWith('shell-') && !keep.has(key))
.map(key => caches.delete(key))
))
);
});4. fetch 只拦截能解释的请求
fetch 会覆盖作用域内的导航和子资源。未调用 respondWith 就保持浏览器默认网络行为,因此先限制同源 GET;不要把支付、登录或任意 POST 请求粗暴塞进静态缓存。
self.addEventListener('fetch', event => {
const request = event.request;
const url = new URL(request.url);
if (request.method !== 'GET' || url.origin !== self.location.origin) return;
event.respondWith(
caches.match(request).then(cached => cached || fetch(request))
);
});缓存中的 Response 和网络响应都是可消费的流。若要同时返回并写入缓存,必须对响应调用 clone(),下一篇会把这一点放进完整策略。
练习
- 在
fetch中只对导航请求(request.mode === 'navigate')提供/offline.html回退;图片和 API 仍然走网络。断网访问一个不存在的路径,记录实际返回的状态码与页面内容。 - 把
SHELL改成包含一个不存在的文件,观察安装状态;说明为什么不能只在catch里打印错误然后让 worker 激活。
下一步
完成练习后查看本章答案,再学习Cache-first、network-first 与 stale。