说到离线缓存,我相信很多做过前端或者运维的朋友都踩过坑。尤其是当你兴致勃勃地写了个 PWA(渐进式 Web 应用),结果用户断网访问时,页面还是白屏或者加载了旧版本,那种挫败感真是绝了。今天咱们不整那些虚头巴脑的理论,就聊聊怎么真正让网站在没网的时候也能“秒开”,以及当缓存搞砸了怎么救场。
先说说那个已经“入土”的 AppCache
如果你还在用 manifest 文件搞离线缓存,我建议你现在就停下来。不是因为它没用,而是因为它确实是个“遗留问题”。早在 2016 年,WHATWG 就把 AppCache 从标准中除名了。为什么?因为它的容错率极低,一旦缓存清单写错一行,或者网络请求超时,整个应用可能就废了。
但既然你标题里提到了它,咱们还是得简单回顾一下,毕竟老项目里还有不少人这么干。
AppCache 的核心是一个叫 .appcache 的文件,里面列出了哪些资源需要缓存。比如:
CACHE MANIFEST
# 版本号:20231027
CACHE:
index.html
style.css
app.js
logo.png
NETWORK:
api/*
FALLBACK:
/ /offline.html
看着挺简单对吧?但我必须警告你:这玩意儿有个致命的 Bug。如果你更新了 index.html 但没改 appcache 文件的注释版本号,浏览器压根不会重新下载新文件。更坑的是,如果你把 style.css 的名字改成了 styles.css,而清单里还写着旧名字,浏览器可能会把新文件缓存成旧名字的键,导致样式全乱。
所以,除非你在维护一个十年前的老系统,否则请立刻放弃 AppCache,转向 Service Worker。
Service Worker:真正的离线缓存救星
Service Worker 是一个注册在 origin 下的,运行在独立线程里的代理脚本。它能在浏览器和网络之间充当代理服务器,拦截网络请求,并决定如何响应。
1. 注册与生命周期
首先,你得在 JS 文件里注册它:
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered: ', registration);
})
.catch(error => {
console.log('SW registration failed: ', error);
});
});
}
这里有个关键点:SW 是异步激活的。注册之后,它需要先安装,然后才能激活并控制页面。这意味着第一次打开网站时,SW 可能还不可用。
2. 安装阶段:预缓存关键资源
在 sw.js 中,我们通常会在 install 事件中缓存那些静态资源(HTML、CSS、JS、图片)。
const CACHE_NAME = 'my-site-v1';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(ASSETS_TO_CACHE))
.then(() => self.skipWaiting()) // 跳过等待,直接激活
);
});
注意 self.skipWaiting() 这行。默认情况下,新安装的 SW 会等待所有旧页面关闭后才激活。如果你希望用户刷新页面就能立刻体验新功能,就得用这行代码强制激活。
3. 激活阶段:清理旧缓存
每次版本号更新(比如 my-site-v2),旧的缓存就会变成垃圾。我们得在 activate 事件中清理它们。
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
}).then(() => self.clients.claim()) // 立即控制所有打开的页面
);
});
self.clients.claim() 也很重要,它让新 SW 立刻接管当前所有页面,而不需要用户刷新。
4. 拦截请求:策略选择
这是最核心的部分。根据资源的类型,我们选择不同的缓存策略。
策略一:Cache First(缓存优先)
适用于静态资源,如 CSS、JS、图片。如果缓存里有,直接用;没有再去网络拿,并缓存起来。
self.addEventListener('fetch', event => {
const request = event.request;
// 只对 GET 请求做缓存
if (request.method !== 'GET') return;
event.respondWith(
caches.match(request)
.then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(request).then(networkResponse => {
// 克隆响应,因为 Body 只能消费一次
const responseToCache = networkResponse.clone();
caches.open(CACHE_NAME)
.then(cache => cache.put(request, responseToCache));
return networkResponse;
});
})
);
});
策略二:Network First(网络优先)
适用于 API 数据或经常变化的内容。先尝试网络,失败了再用缓存。这样能保证用户看到最新数据,但断网时也能 fallback。
event.respondWith(
fetch(request)
.then(networkResponse => {
const responseToCache = networkResponse.clone();
caches.open(CACHE_NAME)
.then(cache => cache.put(request, responseToCache));
return networkResponse;
})
.catch(() => caches.match(request))
);
策略三:Stale-While-Revalidate(陈旧先复用)
这是最聪明的策略。先返回缓存(即使过期了),同时后台发起网络请求更新缓存。下次访问时就是最新的了。
event.respondWith(
caches.open(CACHE_NAME).then(cache => {
return cache.match(request).then(cachedResponse => {
const fetchPromise = fetch(request).then(networkResponse => {
cache.put(request, networkResponse.clone());
return networkResponse;
});
// 返回缓存或网络响应,谁快用谁
return cachedResponse || fetchPromise;
});
})
);
常见坑点与解决方案
问题一:缓存不更新,页面还是旧的
原因:版本号没变,或者 SW 没有重新注册。
解决:
- 修改
CACHE_NAME的值,比如从v1改为v2。 - 确保 HTML 中引用的 SW 脚本路径或内容发生了变化,否则浏览器不会检测更新。
- 在开发调试时,可以在 Chrome DevTools 的 Application 面板中手动删除缓存或点击“Unregister”。
问题二:断网后页面白屏
原因:没有配置 FALLBACK,或者缓存中没有必要的 HTML 文件。
解决:
确保 index.html 被缓存。如果网络请求失败,提供一个通用的离线页面:
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request)
.catch(() => caches.match('/offline.html'))
);
});
问题三:动态数据被错误缓存
原因:API 请求也被 Cache First 策略缓存了,导致用户看到旧数据。
解决: 对 API 请求使用 Network First 策略,或者根本不缓存 API 响应。可以通过检查 URL 前缀来区分:
if (request.url.includes('/api/')) {
// 使用 Network First
} else {
// 使用 Cache First
}
问题四:SW 没有生效
原因:
- SW 文件不在根目录或同一 origin。
- 使用了 HTTP,但访问的是 HTTPS 页面(除非是 localhost)。
- 浏览器兼容性问题(现代浏览器都支持,但 IE 不行)。
解决:
确保站点是 HTTPS,并且 SW 文件路径正确。在 Chrome 中检查 chrome://inspect/#service-workers。
给小白的建议:循序渐进
如果你是第一次做离线缓存,别想着一上来就搞定所有资源。按这个顺序来:
- 第一步:只缓存
index.html和styles.css。确保断网时页面能显示,虽然可能样式还没加载完。 - 第二步:缓存
app.js和其他静态资源。 - 第三步:处理图片,考虑用较小的尺寸或 WebP 格式。
- 第四步:配置 API 请求的策略,避免脏数据。
记住,离线缓存的目的是提升用户体验,而不是让应用完全离线工作。对于大多数内容型网站,确保核心资源可离线访问,动态数据实时请求,就足够了。
最后一点真心话
缓存最怕的不是技术难,而是不可控。当你部署了一个新版本的 SW,却发现有 10% 的用户还在用旧版,那时候你就知道什么叫头疼了。所以,务必做好版本管理,做好灰度发布,并在控制台监控 SW 的安装和激活情况。
希望这篇文章能帮你把离线缓存这件事搞清楚。如果还有问题,欢迎在评论区留言,咱们一起讨论。毕竟,前端这条路,坑多,但填坑的乐趣也多啊!
