如何用Service Worker实现PWA离线缓存与更新

juming
juming 正式会员超兽战士
发布于 2026-10-07 11:46 ·1 浏览 ·0 回复

Service Worker 是 PWA 实现离线缓存的唯一底层能力:用「缓存版本号 + 预缓存安装 + activate 清理旧缓存 + skipWaiting/clientsClaim」四步,就能同时拿到离线可用和自动更新两个效果。

Service Worker 为什么能实现离线缓存?

结论:Service Worker 是一个跑在独立线程、独立于页面的脚本代理,它注册后能拦截本作用域内所有网络请求,把响应写进 Cache Storage,网络断开时直接从缓存返回。

它的生命周期是 install → installed → activating → activated → redundant,页面关闭后依然存活。浏览器在 2015 年 Chrome 40 起支持,Safari 从 iOS 11.3(2018 年 3 月)开始支持,现在主流浏览器已全部覆盖。

三个硬性前提必须满足:页面运行在 HTTPS 或 localhost(file:// 协议不生效);sw.js 文件必须放在能覆盖目标路径的目录下,因为它的 scope 默认就是自己所在目录;只能缓存 GET 请求,Cache API 不接受 POST。

Service Worker 怎么注册和安装?

结论:注册代码写在主页面脚本里,缓存清单写在 sw.js 的 install 事件里,cache.addAll() 是原子操作——清单里任何一个文件 404,整个缓存的写入都会失败。

注册(主页面):

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js', {
    scope: '/',
    updateViaCache: 'none'   // 顶层 sw.js 不读 HTTP 缓存
  });
}

预缓存(sw.js):

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

self.addEventListener('install', (e) => {
  e.waitUntil(
    caches.open(CACHE)
      .then((c) => c.addAll(ASSETS))
      .then(() => self.skipWaiting())   // 不等旧页面关闭,直接进入 activating
  );
});

skipWaiting() 让新 SW 立即从 waiting 状态跳到 activating,否则默认要等所有旧页面标签关闭才更新。

离线时请求该怎么走?三种缓存策略怎么选

结论:HTML 导航请求用 Network First(失败回退 /offline.html),带 hash 的静态资源用 Cache First,数据接口用 Stale-While-Revalidate。

self.addEventListener('fetch', (e) => {
  const { request } = e;
  if (request.method !== 'GET') return;
  if (new URL(request.url).origin !== location.origin) return;

  if (request.mode === 'navigate') {
    e.respondWith(fetch(request).catch(() => caches.match('/offline.html')));
    return;
  }
  e.respondWith(
    caches.match(request).then((cached) => cached || fetch(request).then((res) => {
      const copy = res.clone();                       // 响应流只能读一次,必须先克隆
      caches.open(CACHE).then((c) => c.put(request, copy));
      return res;
    }))
  );
});

关键点:Cache First 只适合文件名带内容哈希的资源(如 main.a1b2c3.js),否则用户会一直拿到旧版本;HTML 必须走网络优先,否则代码更新后页面永远刷新不出来。

Service Worker 更新是怎么触发的?

结论:浏览器会在每次导航时拉取 sw.js,只要文件字节有任何差异(哪怕只改一个注释或版本号)就装成新的 worker;只改业务代码而不改 sw.js,SW 不会更新。

标准做法是每次发版改 CACHE = 'app-v2',并在 activate 里删掉所有非当前版本的缓存:

self.addEventListener('activate', (e) => {
  e.waitUntil(
    caches.keys().then((keys) => Promise.all(
      keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))
    )).then(() => self.clients.claim())   // 立即接管未受控页面
  );
});

注意 skipWaiting() 有个副作用:旧页面还在运行时,新 SW 已经开始按新规则返回资源,可能出现版本错配。稳妥做法是取消自动 skipWaiting,改为页面监听 controllerchange 后弹提示让用户点击刷新:

navigator.serviceWorker.addEventListener('controllerchange', () => location.reload());

想省掉手写缓存逻辑,可以直接用 Google 的 Workbox 7,workbox-precaching 会自动生成带哈希的预缓存清单并处理版本清理。

有哪些容易踩的坑?

结论:Cache Storage 不会自动过期,必须自己清理旧版本;它的容量按 origin 计算,Chrome 允许使用磁盘剩余空间的 60%,远超 localStorage 的 5MB 限制。

  • 开发阶段调试要在 DevTools → Application → Service Workers 勾选 "Update on reload",否则改动看不到效果。
  • 不要缓存 sw.js 自身,也不要在服务器给它设长 Cache-Control。
  • 第三方 CDN 资源(跨域)需要对方返回 Access-Control-Allow-Origin,或用 new Response('', {status: 200}) 做兜底,否则 addAll 会整体失败。
  • 用户清空浏览器数据会一并删除 Cache Storage,离线能力不是永久保证。

总结:把 sw.js 拆成"预缓存清单 + fetch 策略 + activate 清理"三段,版本号随发版递增,导航走网络优先、静态资源走缓存优先,再用 controllerchange 控制刷新时机,离线可用和自动更新就能同时成立。

版权声明:本文来自 GJ站长论坛《如何用Service Worker实现PWA离线缓存与更新》
原文链接:https://www.gj0.com/thread-262.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 0

还没有回复,来抢沙发~