Skip to content

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。

html
<!-- 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(),浏览器才会等待它并把拒绝视为安装失败。安装失败比带着缺文件的半成品继续运行更安全。

js
// 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() 后无条件全删。

js
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 请求粗暴塞进静态缓存。

js
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(),下一篇会把这一点放进完整策略。

练习 ​

  1. 在 fetch 中只对导航请求(request.mode === 'navigate')提供 /offline.html 回退;图片和 API 仍然走网络。断网访问一个不存在的路径,记录实际返回的状态码与页面内容。
  2. 把 SHELL 改成包含一个不存在的文件,观察安装状态;说明为什么不能只在 catch 里打印错误然后让 worker 激活。

下一步 ​

完成练习后查看本章答案,再学习Cache-first、network-first 与 stale。

用心记录每一步成长