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

前端 Service Worker 的 skipWaiting 和 clientsClaim 怎么安排版本切换

来源:17golang原创

时间:2026-09-08 08:07:38 410浏览 收藏

Service Worker 版本切换不要把 skipWaiting()clients.claim() 当成同一个开关:前者让 waiting worker 更早进入激活,后者让已经打开的同源页面被 active worker 接管。默认策略应是让新版本先 waiting;只有旧页面引用的资源、新缓存和新 fetch 路由能够兼容时,才考虑两者一起立即接管。

最稳妥的安排是:先在 install 中准备新缓存,activate 中完成清理和 clients.claim();非破坏更新可以自动切换,可能删除旧资源或改变协议的更新则等待用户确认或下一次导航。

要点速览
  • skipWaiting() 解决的是“新 worker 还在 waiting”,不负责让页面马上使用它。
  • clients.claim() 只能由 active worker 在 activate 阶段接管 scope 内已有页面。
  • 立即切换前先检查旧页面和新缓存的兼容性,页面侧用 controllerchange 防止重复刷新。

先把两个 API 放回 Service Worker 生命周期

一次更新至少要分清四个状态:新脚本下载后进入 install,安装成功后可能停留在 waiting;旧 worker 退出后,新 worker 才进入 activate,页面是否被它控制则由 controller 关系决定。已有页面通常不会因为新 worker 激活就自动重新加载。

能力作用位置解决的问题不能替代什么
self.skipWaiting()Service Worker 内,常放在 install让 waiting worker 请求尽快进入激活不会单独接管已有页面
self.clients.claim()active 后,常放在 activate 的 waitUntil让 scope 内已有 client 使用当前 worker 控制器不会替你准备缓存或刷新页面
controllerchange页面侧感知当前页面控制器变更不会判断新旧资源是否兼容

skipWaiting() 的返回 Promise 可以不专门等待;而 clients.claim() 应放在 activateevent.waitUntil() 中,让激活阶段的异步工作有明确的生命周期。二者一起用时,旧页面可能在一次打开过程中先由旧逻辑加载,再被新 fetch 逻辑接手,这正是版本兼容检查的入口。

Service Worker install waiting activate 与 clients.claim 控制 Fetch Handler 的生命周期关系图
图1:Service Worker 的 waiting、activate、controller 与 fetch 边界;skipWaiting 和 clients.claim 作用在不同生命周期位置。

先用资源兼容性决定是否立即接管

判断标准不是“新版本已经部署”,而是旧页面继续运行时,能不能安全调用新 worker 的 fetch 逻辑。重点看三件事:旧页面仍会请求哪些 URL,新版本是否删除或改名了这些资源,新 worker 的缓存命名和响应策略是否还能满足旧页面。

例如旧页面仍引用 /assets/app.6.js,而 v7 激活时只保留 app.7.js,并且新的 fetch 路由只从 v7 缓存取文件,那么页面在不刷新的情况下被接管,就可能出现脚本或懒加载模块找不到。此时更适合让 worker 保持 waiting,弹出“发现新版本”,由用户确认后刷新。

反过来,如果资源 URL 使用内容哈希且新旧页面都能访问,或者这次只增加了不影响旧页面的缓存条目,立即接管的风险较低。Workbox 文档也特别提醒:如果懒加载资源使用唯一版本 URL,更新时删除旧预缓存可能让正在运行的旧页面加载失败,不能只看“缓存更新成功”就打开抢占。

旧页面旧缓存与新 Service Worker 新缓存通过兼容契约连接的静态关系图
图2:版本切换前要对齐旧页面、资源缓存和新 fetch 路由;兼容时才适合立即接管。

把缓存准备、清理和接管写在正确的事件里

下面是一种适合“资源兼容、希望尽快切换”的最小写法。install 只负责准备 v7 缓存;activate 先删除不再允许的缓存,再接管页面。缓存清理失败时让 activate 失败,比激活后悄悄缺资源更容易发现问题。

const CACHE_NAME = "app-shell-v7";
const APP_SHELL = ["/", "/assets/app.7.js", "/assets/app.7.css"];

self.addEventListener("install", (event) => {
  // 先把新版本需要的资源完整放入新缓存。
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)),
  );
  // 只有确认新旧资源兼容时,才跳过 waiting。
  self.skipWaiting();
});

self.addEventListener("activate", (event) => {
  event.waitUntil((async () => {
    const keys = await caches.keys();
    // 只删除明确不再允许的旧版本,避免误删运行时缓存。
    await Promise.all(
      keys.filter((key) => key.startsWith("app-shell-") && key !== CACHE_NAME)
        .map((key) => caches.delete(key)),
    );
    // active 之后再接管当前 scope 内已有页面。
    await self.clients.claim();
  })());
});

这里的 skipWaiting 不是缓存准备的替代品:如果 cache.addAll() 失败,install 本身不会成功完成。另一个边界是缓存命名,清理范围应与项目自己的前缀一致,不要把第三方库或运行时响应一并删除。

页面侧只刷新一次,并给等待策略留出入口

如果采用立即接管,页面需要监听控制器变化。否则每个 controllerchange 都调用 location.reload(),可能形成重复刷新,尤其是在注册逻辑和更新逻辑同时运行时。

let hasReloadedForController = false;

navigator.serviceWorker.addEventListener("controllerchange", () => {
  // 一个页面只为本次控制器切换刷新一次,避免循环刷新。
  if (hasReloadedForController) return;
  hasReloadedForController = true;
  window.location.reload();
});

async function askForUpdate() {
  const registration = await navigator.serviceWorker.getRegistration();
  // waiting 存在时才提示用户,不把普通注册误报成更新。
  if (!registration?.waiting) return;
  const accepted = window.confirm("发现新版本,立即刷新吗?");
  if (accepted) {
    // 只有 worker 自己支持消息协议时才发送这个命令。
    registration.waiting.postMessage({ type: "SKIP_WAITING" });
  }
}

如果不想让所有更新都自动接管,可以删掉 install 中的 skipWaiting(),保留 waiting worker;页面发现 registration.waiting 后由用户确认,再通过 postMessage 让 worker 调用 self.skipWaiting()。这比把抢占策略硬编码成永远立即更新更适合有长时间打开页面的后台系统。

使用 Workbox 时,workbox-coreclientsClaim() 会把调用安排到 activate;而旧的 workbox-core.skipWaiting() wrapper 已不建议继续使用,直接调用 self.skipWaiting() 更清楚。无论是否使用 Workbox,先确定页面资源兼容契约,再选自动还是用户确认。

发布前用一张清单决定切换方式

  • 缓存:新版本的 precache 是否在 install 成功后才允许激活,旧缓存删除名单是否明确。
  • 页面:旧 HTML 是否可能继续引用不存在的旧资源,懒加载 URL 是否仍可访问。
  • 协议:新 fetch handler 是否改变响应格式、鉴权头或离线回退语义。
  • 接管:是否确实需要当前页面立即使用新逻辑,是否有一次性刷新保护。
  • 回退:发现不兼容时能否只保持 waiting,或者恢复到可访问的旧资源。

满足兼容性条件时,skipWaiting()clients.claim() 可以缩短用户等待;不满足时,保守地等待下一次导航反而更稳。版本切换的核心不是让 worker 越快变成 active,而是让页面、缓存和 fetch 路由在同一份约束下变化。

常见问题

只调用 skipWaiting,当前页面会立刻被新 worker 控制吗?

不一定。它主要推进 waiting 到 active;已有页面是否被控制还取决于 clients.claim,或者等页面下一次导航/刷新。

clients.claim 能不能放在 install 里?

不应这样安排。clients.claim 需要 active worker 才能稳定接管,通常放在 activate 的 event.waitUntil 中。

每次发布都同时使用两个 API 可以吗?

只有在新旧页面和资源兼容时才适合。若新版本删除旧懒加载文件、改变响应协议或清理旧缓存过早,应让新 worker 保持 waiting。

页面监听 controllerchange 后为什么会刷新两次?

通常是没有设置一次性标志,或注册逻辑重复绑定了监听器。把刷新保护放在模块级状态,并确保注册代码只执行一次。

参考:MDN Service Worker APIMDN skipWaiting()MDN clients.claim()Chrome for Developers Workbox Core

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