service worker本身不支持多级版本回滚或灰度发布,因其生命周期由浏览器全局控制,install/activate事件无法按用户维度条件触发,且注册后默认强缓存、不主动重拉;真正实现需协同构建层(带版本号的sw.js)、cdn/服务端(分流html入口)、运行时缓存键设计(url+version)三者配合。

Service Worker 本身不支持“多级版本回滚”或“灰度发布”——它只是一个客户端脚本,无法主动控制服务器流量、无法感知用户分群、也无法自动加载多个并行版本的静态资源。真正能实现这些能力的,是配合 Service Worker 使用的 Cache API 管理策略 + 后端路由/CDN 分流 + 前端运行时决策逻辑。
为什么直接用 Service Worker 做灰度会失败
常见错误现象:self.skipWaiting() 强制激活新 SW 后,所有用户立即加载新版资源;caches.keys() 列出多个缓存名,但没做版本映射逻辑,导致旧缓存残留却无法命中;灰度用户刷新后仍走旧版 SW,因为 navigator.serviceWorker.register() 默认只注册一次,且浏览器对同一 scope 的 SW 有强缓存。
根本原因在于:Service Worker 的生命周期由浏览器控制,不是按需加载的模块;它的 install / activate 阶段是全局事件,无法按用户维度条件触发。
- SW 注册后,除非显式
unregister()或 scope 变更,否则不会重新 fetch 新脚本 - 灰度开关(如 cookie / localStorage)在 install 阶段不可读,只能在 fetch 事件中读取,但此时缓存策略已固定
- 多个版本的 JS/CSS 文件若未带哈希后缀,CDN 和浏览器可能复用旧缓存,造成资源错配
必须配合的三个外部控制点
要让 Service Worker “看起来”支持灰度和多级回滚,得靠三处协同:
-
构建层:每个发布版本生成唯一
sw.js?v=1.2.3,并在 HTML 中动态注入该 URL(避免 SW 被强缓存) -
CDN/反向代理层:根据请求头(如
x-gray-flag: true)或 Cookie 返回不同 HTML 入口文件,其中包含对应版本的 SW 注册代码和资源路径 -
运行时缓存键设计:在
fetch事件中,用request.url + '?v=' + currentVersion作为 cache key,而不是裸 URL;这样同一资源在不同灰度通道下可存为不同缓存项
示例片段(fetch 事件中):
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
const version = getActiveVersionFromCookie(); // 从 document.cookie 读取,非 install 阶段
const cacheKey = url.origin + url.pathname + '?v=' + version;
event.respondWith(
caches.match(cacheKey).then(res => res || fetch(event.request))
);
});
多级回滚如何落地(不是靠 SW 自身)
所谓“多级”,是指保留 v1.0.0、v1.1.0、v1.2.0 三个缓存副本,并允许前端在运行时切换。这不能靠 SW 自动管理,必须由业务代码驱动:
- 每次新版本上线,构建产物打上语义化版本标签,上传至独立 CDN 路径(如
/static/v1.2.0/main.js) - 主 HTML 通过后端模板或 JS 动态加载对应版本的
sw.js和入口资源,同时把当前可用版本列表写入window.__VERSIONS__ - 回滚操作本质是修改 localStorage 中的
preferredVersion,然后调用location.reload(),触发新 SW 加载对应版本资源 - Service Worker 的作用只是“忠实执行缓存读写”,不参与决策——决策在页面 JS 层完成
关键细节:SW 的 activate 事件里必须清理过期缓存,但不能删光——要保留最近 N 个版本的 cache name,比如:
self.addEventListener('activate', event => {
const validCaches = ['v1.0.0', 'v1.1.0', 'v1.2.0'].map(v => `static-${v}`);
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.map(key => !validCaches.includes(key) && caches.delete(key))
))
);
});
最易被忽略的一点:Service Worker 对 document 请求默认不拦截(除非明确声明 navigationPreload),所以 HTML 入口文件的灰度必须由服务端或 CDN 完成,SW 层只管后续静态资源。把这个责任错放到 SW 上,是绝大多数失败案例的起点。










