跳到主要内容
浏览器
导航
网页浏览器

Service Worker 生命周期与离线缓存

梳理 Service Worker 的注册与作用域规则、install/activate/fetch 事件构成的完整生命周期、Cache Storage 的读写方式,以及缓存优先、网络优先与 stale-while-revalidate 三种策略的实现要点。

发布于 3 分钟阅读 1188 字

Service Worker 是一段运行在独立线程中的脚本,它注册在某个源下,可以拦截该源在作用域内的网络请求,并以脚本产生的结果作为响应。因为不依赖页面是否打开,它既能支撑离线访问,也能在请求到达网络之前完成缓存读写。

它的行为高度依赖生命周期:脚本被安装、等待、激活的过程决定了缓存何时写入、何时生效,也决定了新版本在什么条件下接管页面。

注册与作用域

注册由页面发起,脚本必须与页面同源,并且页面处于安全上下文(https 或本机的 localhost)。作用域决定它能拦截哪些请求:

navigator.serviceWorker.register('/sw.js', { scope: '/' })
  .then((reg) => console.log(reg.scope));

scope 默认为脚本所在目录及其子路径,/js/sw.js 的默认作用域是 /js/;显式指定的 scope 不能超出脚本所在路径的上级范围。只有作用域内的页面请求会被该脚本处理,作用域外的请求仍走默认网络路径。

生命周期

注册之后,脚本按固定次序推进:

状态触发时机常见操作
installing脚本开始安装在 install 事件中预缓存资源
installed安装完成若已有页面被旧脚本控制,则停留在等待状态
activating等待结束,准备接管在 activate 事件中清理旧版本缓存
activated激活完成处理 fetch、message 等事件

install 与 activate 事件通过 event.waitUntil() 延长生命周期,传入的 Promise 未完成前,脚本不会被判定为安装或激活失败之外的其它状态:

const CACHE = 'static-v1';
const ASSETS = ['/', '/index.html', '/app.css', '/app.js'];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE).then((cache) => cache.addAll(ASSETS))
  );
});

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
    )
  );
});

若此刻已有页面由旧版本控制,新脚本会停在等待状态,直到这些页面全部关闭。调用 skipWaiting() 可以跳过等待,调用 clients.claim() 可以让当前未受控的页面立即交由新脚本处理。两者都会改变页面与脚本版本的对应关系,需要自行确保缓存内容与新代码一致。

fetch 事件与缓存策略

fetch 事件在页面发出由该脚本接管的请求时触发,event.respondWith() 决定响应来源。缓存读写由 Cache Storage 提供,接口为 caches.open、cache.match、cache.put、cache.delete。常见策略有三类:

  • 缓存优先:命中缓存即返回,未命中再请求网络并把结果写入缓存。适用于变更不频繁的静态资源。
  • 网络优先:先请求网络,失败时回退到缓存。适用于需要及时更新的内容。
  • stale-while-revalidate:先返回缓存副本,同时在后台请求网络并更新缓存,使下一次访问得到新内容。
self.addEventListener('fetch', (event) => {
  if (event.request.mode !== 'navigate') return;
  event.respondWith(
    caches.match(event.request).then((cached) => {
      const network = fetch(event.request).then((response) => {
        const copy = response.clone();
        caches.open('pages-v1').then((cache) => cache.put(event.request, copy));
        return response;
      });
      return cached || network;
    })
  );
});

注意响应体只能被读取一次,写入缓存前需要 clone()。respondWith() 只能处理可被拦截的请求,且脚本必须已经激活。

更新流程

浏览器在导航等时机读取脚本文件并逐字节比较:内容有变化则安装新版本,内容未变化则不产生更新。即使脚本内容不变,浏览器也会按固定周期(多数实现约为 24 小时以内)重新检查一次,避免长期停留在旧版本。

开发阶段需要留意作用域的缓存:脚本文件本身也会受 HTTP 缓存 影响,若被长时间缓存,更新检查可能读到旧内容。常见的规避方式是让脚本响应带上较短的缓存时间或让其参与协商缓存校验。

离线回退与注意事项

离线能力的完整性取决于是否覆盖了导航请求。页面承载的导航请求若在无网络时未能命中缓存,需要准备一个回退页面作为响应。导航预载(navigation preload)则允许在脚本启动的同时并行发出网络请求,缩短首字节等待时间。

此外,Service Worker 在没有事件处理时会随时被终止,因此不能依赖全局变量保存状态;需要跨事件保留的数据应写入 Cache Storage 或 IndexedDB。它与 同源策略 共同约束了可作用的范围:脚本只能拦截同源、且落在自身作用域内的请求。

小结

Service Worker 的行为由「注册 → 安装 → 等待 → 激活」这条生命周期串联,缓存内容随版本更替,请求处理则交给 fetch 事件中的策略函数。设计时先确认作用域覆盖的路径,再为不同类型的资源选择对应的缓存策略,并明确新版本的接管时机。

网页浏览器 · 相关文章