HTML文件不该放进离线包,因其作为页面入口和版本锚点一旦锁死,会导致紧急回滚滞后;应让HTML走网络请求以实现即时更新,而JS/CSS等静态资源才放入离线包并采用content-hash命名确保匹配。

HTML 文件不该放进离线包,这是最常被忽略的前提
把 index.html 放进离线包,等于把「页面入口」和「资源版本锚点」一起锁死。一旦离线包更新策略滞后(比如只在 App 启动时检查),用户就无法及时拿到修复 bug 的新 HTML —— 即便你已在线上发布新版,本地仍加载旧 HTML,而它引用的 JS/CSS hash 又匹配旧离线包,整个页面卡在错误状态。
真正可行的方案是:HTML 必须走网络请求(哪怕只用 100ms),其他静态资源(js/、css/、图片)才放进离线包。这样 HTML 本身就成了版本控制的“活开关”,每次加载都能拿到最新逻辑。
- HTML 不缓存 → 避免版本滞留,支持紧急回滚
- JS/CSS 带 content-hash 命名 → 确保离线包中资源与 HTML 引用严格匹配
- 离线包只存静态资源 → 减少校验失败风险(HTML 文件变动频繁,易导致 manifest 或 SW 缓存失效)
Service Worker 中如何校验 HTML 版本并触发更新
不能依赖 window.applicationCache,它已被 Chrome 95+、Firefox 84+ 彻底移除;也不能靠 manifest 文件——现代构建工具默认不生成 .appcache,且服务器若未配置 text/cache-manifest MIME 类型,该机制直接失效。
正确做法是在 Service Worker 的 fetch 事件中拦截 HTML 请求,用 version header 或 query 参数做轻量校验:
立即学习“前端免费学习笔记(深入)”;
self.addEventListener('fetch', event => {
if (event.request.url.endsWith('.html')) {
event.respondWith(
fetch(event.request)
.then(res => {
// 检查响应头是否含新版本标识
const version = res.headers.get('X-HTML-Version');
if (version && version !== CACHED_HTML_VERSION) {
// 触发离线包整体更新逻辑(如下载新资源包)
updateOfflineBundle();
}
return res;
})
);
}
});
-
X-HTML-Version由后端在 HTML 响应头中注入,值可为 commit hash 或语义化版本号 - 不要在 SW 中解析 HTML 内容提取
<meta name="version">—— 解析开销大,且易被注释或 JS 动态修改干扰 - 校验失败不阻塞 HTML 加载,只作为后台信号触发后续离线资源更新
离线包资源更新时 HTML 与资源 hash 不匹配怎么办
常见现象:用户本地有 v2.3 的离线包,但新 HTML 引用了 main.a1b2c3.js,而离线包里只有 main.d4e5f6.js —— 导致 JS 404,页面白屏。
根本原因是 HTML 和离线包未原子更新。解决方案不是“让 HTML 也进包”,而是建立资源映射关系:
- 构建阶段生成
asset-manifest.json,记录当前 HTML 所需的所有资源路径与 hash - 离线包下载后,先校验该 manifest 是否与 HTML 中的
data-expected-manifest属性一致 - 不一致时,跳过离线资源加载,降级走网络(
network-first),避免白屏 - 同时上报 mismatch 事件,用于监控离线包分发异常
示例 HTML 片段:<html data-expected-manifest="v2.3-8a9b0c">
为什么 localStorage 不能用来存 HTML 版本或资源清单
localStorage 是同步阻塞 API,存一个 JSON 字符串看似简单,但在页面初始化阶段调用 localStorage.getItem('html_version'),会卡住渲染主线程,尤其在低端 Android 设备上延迟可达 20–50ms。
更严重的是:它不支持事务和并发控制。多个 Tab 或 iframe 同时读写同一 key,可能产生脏数据;且无法监听变更,无法触发自动刷新。
- 要用
IndexedDB存资源清单 —— 异步、支持索引、可批量写入 - HTML 版本号别存,直接从网络响应头读取 —— 减少本地状态维护成本
- 如果真要缓存 HTML 字符串(如 PWA 首屏快照),必须用
CacheStorage+Response对象,而非字符串序列化
真正难的不是“怎么存”,而是“什么时候删”——离线包更新后,旧 HTML 引用的旧资源若没清理干净,会悄悄占用存储空间,最终触发 iOS WebKit 的 50MB 硬限制崩溃。这个边界条件,多数人直到上线两周后才注意到。



















