“离线即走”要求Service Worker在install阶段用event.waitUntil()预缓存根路径下的核心静态资源(HTML/CSS/JS/图标),版本号随构建变更,配合skipWaiting()和clients.claim()立即接管页面;fetch阶段对静态资源严格Cache First、零网络兜底,确保无网时秒开UI且联网后静默更新。
“离线即走”不是指关掉网络就闪退,而是用户在无网或弱网时打开页面,仍能立刻渲染出完整 UI,且后续联网后自动静默更新缓存——不打断当前操作、不弹提示、不重载页面。这要求 Service Worker 在 install/activate 阶段完成资源预取与版本切换,同时 fetch 阶段对静态资源严格走 Cache First,并用 skipWaiting() 和 clients.claim() 消除旧 worker 占据导致的延迟接管。
event.waitUntil() + caches.open().addAll()
这是静默更新的前提:所有核心静态资源(HTML、CSS、JS、关键图标)必须在 install 时一次性写入新缓存,不能留到 fetch 时懒加载。否则首次离线访问会失败。
caches.open('static-v2') 中的版本号(如 v2)必须随每次构建变更,否则浏览器认为无更新,跳过 installaddAll() 的路径列表需绝对准确;相对路径(如 ./main.js)在 SW 环境中会解析失败,一律用根路径(/main.js)addAll() 后接 self.skipWaiting(),否则新 worker 会卡在 waiting 状态,直到所有页面关闭clients.claim()
旧缓存不清理会占用空间,更严重的是:若旧缓存名(如 static-v1)还存在,而 fetch 逻辑又没做版本判断,就可能误读过期资源。
caches.keys() 获取全部缓存名,用 .filter(name => !name.startsWith('static-v')) 或白名单比对,只保留当前版本caches.delete() 是异步操作,必须包进 event.waitUntil(),否则清理可能被中断self.clients.claim() 让新 worker 立即接管当前页(包括已打开但未注册 SW 的 tab),这是“离线即走”不闪屏的关键Cache First,且禁止 fallback 到 network所谓“静默”,就是用户完全感知不到缓存行为。一旦在 fetch 中写了 || fetch(request) 这类兜底逻辑,断网时就会触发 fetch 失败,进而抛错或降级——这不是静默,是暴露问题。
request.url.match(/.(js|css|html|png|svg|woff2)$/i)
caches.match(request),且不做任何 fetch() 尝试;如果 match 返回 undefined,直接 return new Response('', { status: 404 }) 或返回预置 offline 页面request.clone() —— clone 会消耗 stream,导致后续 fetch 无法读 body光靠版本号(static-v2)不够。如果文件内容没变但版本号变了,会导致无效缓存;如果内容变了但版本号没变,又会命中旧缓存。真正可靠的依据是文件内容哈希。
contenthash,生成类似 main.a1b2c3d4.js 的文件名injectManifest 插件或构建后替换模板中的 ASSETS_TO_CACHE
cache.addAll(['/']) 这种方式——它依赖服务器配置,且无法控制缓存粒度最容易被忽略的点:Service Worker 的作用域(scope)默认是 sw.js 所在路径。如果 sw.js 放在 /assets/sw.js,那它只能控制 /assets/ 下的请求。要把 sw.js 放在根目录,并在注册时显式指定 { scope: '/' },否则 HTML 文件本身都缓存不了,“离线即走”就成空谈。