当前位置:首页 > 文章列表 > 文章 > 前端 > Service Worker 更新后让页面安全切换版本的方案

Service Worker 更新后让页面安全切换版本的方案

来源:17golang原创 2026-11-13 19:41:30 0浏览 收藏

我在前端发布中最不愿意看到的提示,不是“有新版本”,而是用户点完更新后页面出现一半旧、一半新的状态:HTML 已换了,JavaScript 还在旧缓存里,或者多个标签页同时刷新。Service Worker 的更新流程本身不会替你决定“什么时候让页面切换”,需要把安装、等待、激活、控制权变化和刷新连成一条明确的生命周期。

比较稳妥的方案是:新 worker 先使用新缓存名完成安装,旧 worker 继续服务现有页面;页面检测到 waiting worker 后提示用户,确认后再发送消息调用 skipWaiting();新 worker 激活并通过 clients.claim() 接管页面,页面只在 controllerchange 中刷新一次。这样切换点是可见且可控的。

官方资料:https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration

要点速览
  • 缓存名要随应用资源版本变化,新旧缓存并存到新 worker 激活。
  • 不要在发现新 worker 时立即刷新,先让用户确认或由业务策略决定切换时机。
  • skipWaiting() 负责跳过 waiting,clients.claim() 负责接管作用域内页面,页面用一次性标记防止重复刷新。

先把更新链路拆成四个状态

Service Worker 更新时,浏览器会把新脚本作为新的 worker 安装。已有页面通常仍由旧的 active worker 控制,新 worker 安装完成后会停在 waiting,直到旧页面不再使用旧 worker,或代码主动要求它提前激活。ServiceWorkerRegistration.updatefound 可以用来观察“有新的 installing worker”这一事实,而不是把它误当成“页面已经切换”。

状态页面能做什么切换注意
installing等待新 worker 完成安装和预缓存旧 worker 仍负责当前页面
waiting展示“发现新版本”并等待确认不要立即覆盖旧页面的资源
active新 worker 可以处理后续事件是否控制已有页面还要看 claim
controllerchange页面知道控制者发生改变刷新动作需要幂等保护
Service Worker 从安装等待到页面控制权交接的静态说明图
图1:Service Worker 从 waiting 到页面控制权交接的静态说明图,不是浏览器截图。

先预热新缓存,不要直接覆盖旧缓存

缓存名应明确包含应用资源版本,例如 app-static-v2。新 worker 在 install 阶段打开新缓存并写入完整的静态资源列表,旧的 app-static-v1 暂时保留。这样正在运行的旧页面仍能从它自己的资源集合读取,不会因为新版本还没准备好而拿到半套文件。

// sw.js:新版本使用独立缓存,安装完成前不碰旧缓存。
const CACHE_NAME = "app-static-v2";
const APP_SHELL = ["/", "/index.html", "/assets/app-v2.js", "/assets/app-v2.css"];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      // addAll 失败时让安装失败,避免激活一个不完整的资源集合。
      return cache.addAll(APP_SHELL);
    }),
  );
});

self.addEventListener("message", (event) => {
  if (event.data?.type === "ACTIVATE_NEW_VERSION") {
    // 只在页面确认后跳过 waiting,避免后台更新突然打断用户操作。
    self.skipWaiting();
  }
});

这里没有在 install 里无条件调用 skipWaiting()。如果应用正在编辑表单、播放媒体或执行不可中断的任务,立即切换可能让页面与新资源的状态不一致。把消息作为切换闸门,能够让产品决定“现在更新”还是“下次打开更新”。

页面检测到 waiting 后再让用户确认

页面侧需要同时处理两种情况:本次注册过程中出现新的 installing worker,以及页面打开时 registration 已经有 waiting worker。两者都交给同一个绑定函数,避免只监听 updatefound 而漏掉已安装完成的更新。

// app.js:把 waiting worker 交给页面上的更新提示组件。
function showUpdatePrompt(registration) {
  const waitingWorker = registration.waiting;
  if (!waitingWorker) {
    return;
  }

  // 这里的 UI 可以是 Toast、顶部横幅或设置页按钮。
  renderUpdateBanner(() => {
    // 用户确认后才发送切换消息,避免后台更新直接打断当前会话。
    waitingWorker.postMessage({ type: "ACTIVATE_NEW_VERSION" });
  });
}

async function registerAppWorker() {
  if (!("serviceWorker" in navigator)) {
    return;
  }

  // 更新检查不读取主 worker 脚本的 HTTP 缓存,减少版本发现延迟。
  const registration = await navigator.serviceWorker.register("/sw.js", {
    updateViaCache: "none",
  });

  if (registration.waiting) {
    // 页面加载时已经存在 waiting worker,直接显示更新提示。
    showUpdatePrompt(registration);
  }

  registration.addEventListener("updatefound", () => {
    const installingWorker = registration.installing;
    if (!installingWorker) {
      return;
    }
    installingWorker.addEventListener("statechange", () => {
      // installed 且页面已有 controller,说明这是一次可提示的更新。
      if (installingWorker.state === "installed" && navigator.serviceWorker.controller) {
        showUpdatePrompt(registration);
      }
    });
  });
}

registerAppWorker().catch((error) => {
  // 注册失败不应阻塞页面本身,记录后继续走网络加载。
  console.error("Service Worker 注册失败", error);
});

updateViaCache: "none" 只影响更新 Service Worker 脚本及其导入脚本时的缓存使用方式,不等于把应用的所有资源都禁用缓存。实际项目仍要根据部署链路设置 HTTP 缓存头,并让构建产物拥有可区分的文件名。

用 controllerchange 完成一次性刷新

用户确认后,新 worker 会从 waiting 进入 activating,再成为 active。若在 activate 阶段调用 clients.claim(),作用域内的页面会收到 controllerchange。页面可以把这个事件作为“资源控制权已经交接”的信号,但必须设置刷新锁,否则多个事件、多个标签页或重复注册都可能造成刷新循环。

// app.js:只有原本已经被 worker 控制的页面才因更新自动刷新。
const hadControllerBeforeRegistration = Boolean(navigator.serviceWorker.controller);
let isReloadingForUpdate = false;

navigator.serviceWorker.addEventListener("controllerchange", () => {
  if (!hadControllerBeforeRegistration || isReloadingForUpdate) {
    // 首次安装或已经触发过刷新时,不再重复导航。
    return;
  }

  isReloadingForUpdate = true;
  // 新 controller 已接管请求,刷新后页面会读取同一版本的资源集合。
  window.location.reload();
});

首次安装时页面原本没有 controller,是否刷新应由产品体验决定;上面的 hadControllerBeforeRegistration 用来把“首次接管”和“已有版本更新”区分开。如果应用需要首次安装也刷新,可以把这个条件改成持久化的版本确认逻辑,而不是简单删除保护。

activate 阶段再清理旧缓存

旧缓存的删除要放在新 worker 激活阶段,并通过 event.waitUntil() 等待清理完成。关键点是只保留当前版本,其余缓存统一删除;不要在安装阶段删除旧缓存,因为此时旧页面可能仍在使用它。

// sw.js:新 worker 激活后再清理旧版本,并接管当前作用域页面。
self.addEventListener("activate", (event) => {
  event.waitUntil(
    (async () => {
      const cacheNames = await caches.keys();
      await Promise.all(
        cacheNames
          .filter((name) => name !== CACHE_NAME)
          .map((name) => {
            // 只删除旧版本缓存,不触碰当前正在使用的 app-static-v2。
            return caches.delete(name);
          }),
      );

      // 让作用域内的现有页面获得新的 controller。
      await clients.claim();
    })(),
  );
});
Service Worker 版本化缓存预热与激活后清理的静态关系图
图2:版本化缓存从预热到清理的静态关系图,不是运行截图。

如果旧页面仍在执行长任务,强制切换仍可能造成业务中断,所以“确认后 skipWaiting”只是技术能力,不是所有产品都必须采用的默认策略。对编辑器、支付流程或长表单,可以只提示用户并把切换推迟到下一次自然导航。

这几类现象通常对应什么边界

现象优先检查处理方向
一直显示 waiting页面是否发送了激活消息、worker 是否完成 install检查消息类型和 addAll 失败原因
更新后仍拿到旧 JS缓存名、构建文件名和 fetch 策略让资源版本可区分,激活后再删旧缓存
页面连续刷新controllerchange 是否有锁增加一次性标记,并区分首次安装
新 worker 接管但页面不更新是否监听了 controllerchange,是否调用了 claim将接管和刷新动作连接起来

上线前把切换策略固定成清单

  1. 为每次构建生成新的静态资源文件名或缓存版本名,避免新旧文件在同一缓存键上竞争。
  2. 安装阶段只创建和填充新缓存,资源缺失时让安装失败,不激活半成品。
  3. 页面同时处理已有 registration.waiting 和 updatefound 产生的 waiting worker。
  4. 把 skipWaiting 放在用户确认或明确的业务切换点之后。
  5. 在 activate 中用 event.waitUntil 完成旧缓存清理,再用 clients.claim 交接控制权。
  6. 用 controllerchange 触发一次性刷新,并为多标签页、表单和长任务保留延迟切换方案。

这套流程的核心不是“让 Service Worker 立即更新”,而是把版本准备、切换许可和页面刷新拆开。只要旧页面仍能获得完整的旧资源,新页面只在新 worker 已经接管后读取新资源,发布时最棘手的新旧版本混用问题就有了明确的边界。

相关问题

为什么不直接在 install 中调用 skipWaiting?

可以这样做,但它会让新 worker 尽快进入激活流程,可能打断正在编辑、播放或提交中的页面。需要可控发布时,把调用放在页面确认消息之后更稳妥。

clients.claim 和 skipWaiting 是一回事吗?

不是。skipWaiting 处理 worker 从 waiting 到 active 的推进;clients.claim 让已经位于作用域中的页面获得新的 controller,二者解决的是不同阶段。

缓存版本名变了就一定不会混用吗?

不能单独保证。还要保证资源清单完整、旧缓存只在 activate 后清理,并让页面在 controllerchange 后再刷新;构建产物也应使用可区分的文件名。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
CSS color-scheme 怎样让原生表单控件适配深色模式CSS color-scheme 怎样让原生表单控件适配深色模式
上一篇
CSS color-scheme 怎样让原生表单控件适配深色模式
IndexedDB versionchange 处理旧连接阻塞升级的方法
下一篇
IndexedDB versionchange 处理旧连接阻塞升级的方法
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    1015次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    1019次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1144次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1119次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    960次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码