用Service Worker实现前端离线缓存与优雅降级
从一次地铁断网说起
上个月通勤时,我点开一个常看的资讯站点,在地铁隧道里信号瞬间归零。正当我准备退出时,页面居然直接加载出来了,并且是最近一次访问的完整内容。那一刻我意识到,对方在前端离线缓存上下了真功夫。Service Worker 作为浏览器背后的“服务人员”,正在悄悄改变 Web 应用的体验边界。
很多人对它的印象停留在“离线包”或“PWA 标配”,但真正用好 Service Worker,远不止注册一个 `sw.js` 那么简单。如何让用户在弱网甚至无网环境下依然能完成核心操作,如何平衡缓存新鲜度与加载速度,这才是我们今天要聊的优雅降级。
理解 Service Worker 的拦截机制
Service Worker 本质上是浏览器与网络之间的代理层。它独立于页面运行,可以拦截页面发出的所有 fetch 请求,并决定是从缓存返回、走网络,还是两者结合。
注册代码非常简单:
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}
但真正决定体验差异的,是 `sw.js` 内部的缓存策略。我们的目标不是简单地“缓存所有东西”,而是根据资源类型设计不同的降级路径。
核心缓存策略:从网络优先到缓存优先
在实际项目中,我倾向于将资源分为三类,分别采用不同的策略:
1. 静态资源(JS/CSS/图片)——缓存优先,网络兜底
这类资源文件名通常带 hash,内容几乎不变。采用 cache-first 策略,命中缓存直接返回,加载速度极快。同时使用 `cache.addAll` 在 install 阶段预缓存核心资源。
self.addEventListener('install', event => {
event.waitUntil(
caches.open('static-v1').then(cache =>
cache.addAll(['/', '/index.js', '/styles.css'])
)
);
});
2. 页面文档 ——网络优先,缓存降级
HTML 文档需要保持新鲜度,但也需要能离线访问。我的做法是先请求网络,如果网络失败(比如断网或超时),则回退到上一次缓存的页面。这才是“优雅降级”的关键——用户看到的不应该是“无法连接”,而是一个可用的旧页面。
self.addEventListener('fetch', event => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.then(response => {
const clone = response.clone();
caches.open('pages-v1').then(cache => cache.put(event.request, clone));
return response;
})
.catch(() => caches.match(event.request).then(cached =>
cached || caches.match('/offline.html')
))
);
}
});
3. 接口数据 ——Stale While Revalidate
对于列表或详情接口,可以使用 stale-while-revalidate 策略:先快速返回缓存数据保证页面即时渲染,然后在后台请求新数据更新缓存,下次访问就是最新的。这在弱网环境下体验提升非常直观。
优雅降级的核心:做一个体面的备用页
当用户真的处于离线状态且没有任何缓存时,最后的兜底方案应该是一个设计良好的离线页面。这个页面应当包含:品牌信息、网络状态提示、最近浏览过的内容入口,甚至一个简单的重试按钮。
更好的做法是,在页面启动时通过 `navigator.onLine` 和 `online`/`offline` 事件监听网络状态,离线时自动隐藏评论区、收藏按钮等需要网络的功能模块,而不是等用户点击后才弹错误提示。这种主动式降级远比被动报错来得体面。
版本管理与更新策略
Service Worker 的缓存更新有一个经典陷阱:用户永远不知道你更新了代码。解决方案是在 `install` 阶段使用 `skipWaiting()`,并在 `activate` 阶段清理旧缓存:
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(key => key !== 'static-v2' && key !== 'pages-v1')
.map(key => caches.delete(key)))
).then(() => self.clients.claim())
);
});
同时在前端增加提示机制——当新版本 SW 激活时,通过 postMessage 通知页面刷新,避免用户一直停留在旧版本中。
小结
Service Worker 提供的不仅是缓存能力,更是一种设计思维的转变:把网络异常从异常变为常态,把离线体验从“不可用”变为“可用但降级”。它需要我们在开发时就考虑每一个资源和接口的最坏情况。
从地铁上的那次流畅体验出发,我意识到真正好的前端架构不是把晴天优化到极致,而是在下雨天时,用户依然能到达目的地。Service Worker 给了我们一把伞,而优雅降级教会我们如何撑开它。
管理员
黑卡会员