登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

Service Worker 更新后如何避免旧缓存继续返回

来源:17golang原创

时间:2026-09-12 12:11:11 442浏览 收藏

Service Worker 更新后仍返回旧页面,通常不是“浏览器没有更新”,而是旧 worker、旧 Cache API 条目和仍被旧 controller 控制的页面叠在了一起。稳定做法是让缓存名跟随版本变化,在 activate 中删除旧名,再决定页面何时接管新 worker。

要点速览
  • 用版本化缓存名区分资源集合,不要在新旧版本间复用含义不清的固定名称。
  • 清理旧缓存放在 activate,并通过 event.waitUntil() 等待清理完成。
  • skipWaiting() 解决等待激活,clients.claim() 解决已打开页面的控制权;两者不是同一个动作。

先把旧缓存问题拆成三个状态

一次更新至少涉及三个对象:浏览器正在安装的新 worker、仍负责当前页面请求的旧 worker,以及 Cache API 中按名称保存的资源。新 worker 安装期间旧 worker 继续工作,因此不能在 install 阶段删除旧缓存,否则旧页面可能突然拿不到自己的资源。

另一个容易忽略的边界是页面控制权。worker 激活后,已经打开的文档未必立刻换 controller;如果页面没有重新加载,看到旧内容并不能直接证明缓存清理失败。

Service Worker 更新中的安装、激活、版本化缓存和旧缓存清理静态关系示意图
图1:Service Worker 更新的静态结构示意图,重点看新 worker、当前缓存与旧缓存之间的生命周期边界;这不是运行截图。

用版本化缓存名让 activate 负责清理

下面的最小实现把预缓存和清理分开。install 只创建新集合;activate 列出所有缓存名,删除不在保留名单中的名称。把每个异步动作交给 waitUntil,浏览器才会把安装或激活视为仍在进行。

const CACHE_NAME = "app-static-v3";
const ASSETS = ["/", "/app.js", "/styles.css"];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      // 新版本只写入自己的缓存,避免干扰仍在服务旧页面的 worker。
      return cache.addAll(ASSETS);
    }),
  );
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(
        keys
          .filter((key) => key.startsWith("app-static-") && key !== CACHE_NAME)
          .map((key) => {
            // 删除只属于本应用且不是当前版本的缓存,不碰其他用途的名称。
            return caches.delete(key);
          }),
      ),
    ),
  );
});

这里的判断故意保留前缀范围。直接删除所有非当前缓存,可能误删同一 origin 下由其他功能维护的 CacheStorage 名称。若应用把图片、接口响应和静态文件分成多类缓存,应把各类当前名称都放进保留集合,而不是只留下一个。

skipWaiting 和 clients.claim 要不要一起用

self.skipWaiting() 让等待中的新 worker 尽快进入 active,clients.claim() 则让激活后的 worker 尝试接管作用域内的现有页面。前者改变 worker 生命周期,后者改变页面 controller。两者都打开时,旧页面可能在一次会话中切换到新代码;如果页面状态不能跨版本兼容,更稳妥的做法是只提示用户刷新,或在用户完成编辑后再发送消息让新 worker 执行。

self.addEventListener("install", (event) => {
  event.waitUntil(
    // 只有确认新旧脚本可以共同处理当前页面时,才立即跳过等待。
    self.skipWaiting(),
  );
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    (async () => {
      // 清理完成后再接管页面,减少新代码读到未准备好资源的窗口。
      await removeOldCaches();
      await clients.claim();
    })(),
  );
});

如果选择“下次导航生效”,可以不调用这两个方法,让浏览器按默认生命周期完成切换;如果选择“立即生效”,应同时考虑未保存表单、内存状态和 API 响应格式是否兼容。更新策略是产品行为,不是越快接管越好。

Service Worker controller、clients.claim、页面刷新和 fetch 回退边界静态关系示意图
图2:controller 与缓存响应的边界示意图,展示接管、刷新和网络回退各自解决什么问题;这不是运行截图。

仍然返回旧内容时按三层证据排查

看到的现象优先检查对应判断
缓存列表里有 v2、v3activate 是否执行,waitUntil 是否等待删除清理逻辑未完成或未被当前 worker 接管
缓存只有 v3,页面仍旧navigator.serviceWorker.controller 与页面是否重载页面控制权或内存中的旧文档未切换
controller 已更新但接口旧fetch 事件是否 cache-first、HTTP Cache-Control 是否过期返回来源可能是 fetch 策略或 HTTP 缓存,不是旧 Cache API 名称

对 HTML 入口和带版本指纹的 JS/CSS,常见组合是“网络优先的导航请求 + 版本化静态缓存”。对于用户数据接口,不要照搬静态资源的 cache-first;更新时先确认响应状态和内容类型,再决定是否写入缓存。这样即使某一层仍有旧数据,也能快速定位到底是 worker 生命周期、页面 controller,还是请求策略造成的。

常见问题

删除旧缓存后为什么当前页面没有马上变新?

缓存清理只处理 Cache API 条目,不会自动重建已经打开的文档。检查 controller 是否变化,并在合适时机重新加载页面。

缓存名每次发布都加版本就一定安全吗?

不一定。还要在 activate 删除旧名、控制缓存数量,并确认 install 的资源清单完整,否则可能只是把旧问题换了一个名称。

skipWaiting 是否应该默认开启?

只有新旧代码能兼容当前页面状态时才适合立即接管。编辑器、支付流程或长表单更适合提示刷新并等待用户确认。

相关资料:MDN PWA 缓存指南MDN Using Service Workers

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>