manifest 属性已废弃,Service Worker 是唯一可行的离线缓存方案

先说一个核心结论:想在 Web 上实现稳定可靠的离线体验,老路已经彻底行不通了。
manifest attribute 已被弃用,别再配 manifest 属性了
一个时代结束了。从 Chrome 94 版本开始,对 HTML 标签里的 manifest 属性的支持被完全移除。其实 Firefox 和 Safari 浏览器更早就放弃了它。这意味着,即使你还在代码里兢兢业业地写着 ,浏览器也只会默默忽略它——不报错,但缓存功能完全失效。这事儿不是开发者用法的问题,而是这个机制自诞生起就伴随着兼容性差、更新策略过于僵硬、无法进行细粒度控制等硬伤,最终被标准层面正式废弃。
Service Worker 是唯一可行的离线缓存替代方案
那么,路在何方?答案是明确的,且只有一条:Service Worker。它不是什么“备选方案”,而是当前 Web 平台唯一能够真正接管网络请求、实现自定义离线缓存逻辑的核心机制。这里有几个关键点必须抓住:
- 首先,它必须通过 Ja vaScript 动态注册,并且只在 HTTPS 环境(或者本地
localhost)下才能生效。 na vigator.serviceWorker.register()这行代码的调用时机很有讲究。建议放在页面加载的早期,比如在底部的里就执行。如果注册得太晚,首屏的关键资源可能就拦不住了。- 注册时指向的脚本文件(比如
sw.js)要特别注意:别给它加查询参数(像sw.js?v=1这样),浏览器会把它当成一个全新的脚本,容易导致重复注册或者更新失败。 - 首次注册成功后,Service Worker 就有了自己的“生命周期”,它独立于页面。即使你关闭了浏览器标签页,它也可能在后台运行;而当你刷新页面时,控制页面的可能还是旧版本,这时候就需要手动触发
skipWaiting()来让新版立即上岗。
缓存策略要按资源类型拆开写,别一股脑 cache.addAll()
实现离线功能,最难也最重要的一环是缓存策略。切忌对所有资源“一视同仁”,用同一个 cache.addAll() 打包处理。不同资源,策略应该截然不同:
- 静态资源:比如 JS、CSS、字体、图片,它们通常版本化明确,适合采用“缓存优先”策略。
- HTML 文档:内容可能频繁更新,建议采用“网络优先,失败回退”的策略,确保用户第一时间看到最新内容,断网时才展示缓存版本。
- API 接口:动态数据通常不应该直接塞进 Cache Storage。更合理的做法是让它走网络,并通过 HTTP 标准的
Cache-Control头部来控制缓存。
具体怎么实现?看看下面这个按资源类型分流的 fetch 事件处理示例:
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (url.origin === location.origin) {
if (url.pathname.endsWith('.html')) {
// HTML:先试网络,断网时返回缓存的 fallback 页面
event.respondWith(
fetch(event.request).catch(() => caches.match('/offline.html'))
);
} else if (/\.(js|css|png|jpg|woff2)$/.test(url.pathname)) {
// 静态资源:缓存优先,后台静默更新
event.respondWith(caches.match(event.request).then(r => r || fetch(event.request)));
event.waitUntil(updateStaticCache()); // 单独函数更新缓存
}
}
});
调试 Service Worker 极容易掉坑,重点盯这三个地方
开发 Service Worker 的过程,几乎就是和调试工具斗智斗勇的过程。经验表明,以下几个地方是高频“雷区”,需要重点盯防:
- 工具选择:Chrome DevTools 里的
Application > Service Workers面板是你的主战场,信息最可靠。千万别只依赖页面的 console.log,因为 Service Worker 的日志默认是独立的,需要手动切换到对应的 Service Worker 上下文才能看到。 - 注册失败:如果看到控制台报
Failed to register a ServiceWorker,先检查两个点:一是sw.js文件是否能正常访问(404?),二是服务器返回的 MIME 类型是否正确(必须是text/ja vascript)。 - 更新不生效:在 Service Workers 面板里,如果新版本的状态显示为 “waiting”,那就说明它卡住了。你需要在新脚本里调用
self.skipWaiting(),并在页面中监听controllerchange事件来触发页面刷新或重载。 - 缓存未命中:有时候你觉得该命中的缓存却没命中。这时可以在 Console 里手动运行
caches.open('my-cache').then(cache => cache.keys()),查看缓存里到底存了些什么键。需要警惕的是,URL 末尾一个斜杠、大小写字母、查询参数的细微差异,都可能导致完全匹配不上。
归根结底,缓存匹配的精度要求极高。别想当然地认为“路径看起来一样就行”,细节决定成败。
