静态html离线可用的关键是缓存覆盖加载链全部环节:install阶段用cache.addall()原子缓存绝对路径资源,fetch事件按destination分流处理,activate阶段清理旧缓存,html由sw全权托管。

静态 HTML 页面离线能用,关键不是“缓存了多少”,而是“缓存结构是否覆盖页面加载链的每个环节”——漏掉一个 script 或一个 font,页面就白屏或功能残缺。
install 阶段必须缓存完整依赖链,不能只写 /index.html
浏览器加载 /index.html 后,会按顺序发起对 main.css、app.js、logo.png 等资源的请求。如果这些没进缓存,即使 HTML 本身缓存成功,页面也会在离线时触发大量 net::ERR_FAILED。
-
cache.addAll()是原子操作:列表中任意一项返回 404、CORS 拒绝、MIME 不匹配(比如字体被当成text/plain),整个缓存就中断,一个文件都不会写入 - 所有路径必须是绝对根路径:
'/index.html'✅,'index.html'❌,'./main.css'❌,'/static/app.js?v=1.2'❌(查询参数导致键不复用) - 检查服务器实际返回内容:动态生成的
/index.html若带Cache-Control: no-store,cache.addAll()会直接拒绝缓存它
fetch 事件里必须按 request.destination 分流,不能无差别 match
把所有请求都丢给 caches.match(event.request),等于让 Service Worker 把登录接口、表单提交也当静态资源去查缓存,结果就是 POST 失败、cookie 丢失、用户以为点了提交但什么都没发生。
-
event.request.destination === 'document':这是页面首次加载或刷新的请求,必须命中缓存,否则白屏;建议 fallback 到预存的'/offline.html'字符串响应 -
=== 'script'或=== 'style':可直接caches.match()返回,静态资源缓存优先没问题 -
=== 'font'或=== 'image':建议加 try/catch,失败就跳过缓存,避免因单个图标缺失阻断整页渲染 -
=== 'json'或空字符串(如fetch('/api/user')):这类 API 请求应放行到网络,或走stale-while-revalidate,别塞进离线主缓存
缓存命名和版本控制要显式管理,避免旧缓存卡死
缓存名写死成 'v1' 看似简单,但更新资源后,新 SW 安装时旧缓存不会自动删,浏览器可能继续用过期的 app.js,而你根本不知道。
- 缓存名建议带哈希或时间戳:
`static-v${VERSION}`,其中VERSION来自构建时注入(如 Webpack 的DefinePlugin) -
activate阶段必须调用caches.delete()清理非当前版本的缓存,否则旧缓存永远占位 - 不要依赖
self.skipWaiting()强制激活——它会让新 SW 立即接管已有页面,可能造成 JS 版本错配;更稳妥的是监听controllerchange事件后手动刷新
HTML 文件本身不能靠 HTTP 缓存头,得由 SW 全权托管
很多人给 index.html 加 Cache-Control: max-age=3600,指望浏览器自己缓存,结果发现离线打不开——因为 HTTP 缓存和 SW 缓存是两套机制,且现代浏览器对 HTML 的 HTTP 缓存非常保守。
-
index.html的Cache-Control应设为no-cache或max-age=0,确保每次访问都触发 SW 的fetch事件 - SW 中对
destination === 'document'的请求,必须用caches.match('/')或caches.match('/index.html')显式返回,不能依赖 HTTP 层 - 若用 SPA 路由(如 Vue Router history 模式),
install阶段只需缓存'/',fetch里所有document请求都统一返回它即可
真正难的不是写几行 cache.addAll(),而是把 HTML 解析执行时每一步可能发出的请求,都提前映射进缓存结构里——字体跨域、图片大小写、JS 动态 import() 路径、甚至 <link rel="manifest"> 指向的 manifest.json,漏一个,离线体验就断一环。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











