离线策略
发布于
完成本指南后,断网状态下你的应用照样能加载并保持可用,因为一个 Service Worker 会为每类请求从正确的地方 作答:应用本身用缓存的外壳,数据走网络并以缓存回退,两者都未命中的导航则返回一张存好的离线页。 并不存在单一的「离线模式」;离线是一组按请求做出的决定:何时信任缓存、何时信任网络。
你需要一个已注册的 Service Worker(没有的话先做入门),并了解 install
与 activate 何时触发,见
Service worker 生命周期:install、activate 与更新。
下面的代码是一份完整的 sw.js,每一步往里加一个处理函数。
在 install 时预缓存外壳
Section titled “在 install 时预缓存外壳”在 install 中打开一个带版本号的缓存,用 addAll() 放入每个视图都需要的 HTML、CSS 与 JavaScript,
再加上离线页。缓存名里的版本号是下一次发布得以整体替换的依据。只要有一个文件抓取失败,addAll() 就会
拒绝,而 waitUntil() 收到被拒绝的 Promise 会放弃本次安装,所以只列出你能控制的文件。
const VERSION = 'v3';const SHELL_CACHE = `shell-${VERSION}`;const DATA_CACHE = `data-${VERSION}`;const SHELL = ['/', '/app.css', '/app.js', '/offline.html'];
self.addEventListener('install', (event) => { event.waitUntil(caches.open(SHELL_CACHE).then((cache) => cache.addAll(SHELL)));});带哈希文件名的外壳文件可以在缓存存活期间一直缓存优先;不带哈希的 /app.js 只在 VERSION 改变、
worker 重新安装时才更新。
按请求类型路由每个 fetch
Section titled “按请求类型路由每个 fetch”逐请求决定。导航请求先走网络让用户看到最新 HTML,网络失败时用缓存的外壳。外壳资源缓存优先:即时响应, 由下一个 worker 版本更新。API 响应网络优先、以最近一次成功的副本回退,代价是在线时每次请求都要等一次 完整的网络往返。图片与字体用 stale-while-revalidate:缓存副本立即返回,后台抓取刷新它,所以可能有一次 请求拿到过期内容。
self.addEventListener('fetch', (event) => { const { request } = event; if (request.method !== 'GET') return; // POST 之类直接走网络
const url = new URL(request.url); if (request.mode === 'navigate') { event.respondWith(networkFirst(request, SHELL_CACHE, '/offline.html')); } else if (SHELL.includes(url.pathname)) { event.respondWith(caches.match(request).then((hit) => hit || fetch(request))); } else if (url.pathname.startsWith('/api/')) { event.respondWith(networkFirst(request, DATA_CACHE)); } else if (request.destination === 'image' || request.destination === 'font') { event.respondWith(staleWhileRevalidate(request, DATA_CACHE)); }});
async function networkFirst(request, cacheName, fallbackPath) { const cache = await caches.open(cacheName); try { const fresh = await fetch(request); if (fresh.ok) cache.put(request, fresh.clone()); return fresh; } catch { const cached = await cache.match(request); if (cached) return cached; if (fallbackPath) return cache.match(fallbackPath); throw new Error(`offline and ${request.url} is not cached`); }}
async function staleWhileRevalidate(request, cacheName) { const cache = await caches.open(cacheName); const cached = await cache.match(request); const refresh = fetch(request).then((response) => { if (response.ok) cache.put(request, response.clone()); return response; }); return cached || refresh;}cache.put() 会消耗响应体,所以代码先 clone() 再存,返回原始响应。各策略的取舍以及与 Workbox 的对应关系见
选择缓存策略。
在 activate 时删除旧缓存
Section titled “在 activate 时删除旧缓存”activate 在新 worker 接管页面、旧 worker 退出之后运行,这是删除上一版本缓存的安全时机。缓存在整个源内共享,
所以按自己的前缀匹配,而不是全部删除。这个处理函数要短:fetch 会排队等待过长的激活。
self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((names) => Promise.all( names .filter((name) => (name.startsWith('shell-') || name.startsWith('data-')) && !name.endsWith(VERSION)) .map((name) => caches.delete(name)), ), ), );});用 Background Sync 排队失败的写入
Section titled “用 Background Sync 排队失败的写入”networkFirst 对 GET 返回的是旧数据;失败的 POST 需要重试。在存在 ServiceWorkerRegistration.sync
的地方,把请求体存进 IndexedDB 并注册一个 sync 标签;网络恢复时浏览器触发 sync。该 API 只有 Chromium
实现(版本见 Background Sync 后台同步支持),所以注册抛错时页面也要能工作。
// 页面中,POST 失败之后:async function queueForSync(tag) { const registration = await navigator.serviceWorker.ready; if (!('sync' in registration)) return false; // 改为提示「已保存到本地,稍后重试」 try { await registration.sync.register(tag); return true; } catch { return false; }}
// sw.js 中:self.addEventListener('sync', (event) => { if (event.tag === 'outbox') event.waitUntil(replayOutbox(event.lastChance));});event.lastChance 在浏览器的最后一次重试时为 true,这是告诉用户写入已丢弃的时机。
预缓存大文件前先查存储预算
Section titled “预缓存大文件前先查存储预算”每个缓存都计入源的配额,而逐出可能一次清空全部。navigator.storage.estimate() 返回保守估计的 quota
与当前 usage(单位字节);因为压缩与去重,数字只是近似值。对必须保留的数据请求
navigator.storage.persist(),见存储持久化、配额与逐出。
const { usage, quota } = await navigator.storage.estimate();console.log(`${(usage / 1048576).toFixed(1)} MB used of ${(quota / 1048576).toFixed(0)} MB`);断网重新加载
Section titled “断网重新加载”先在线加载一次应用让 install 跑完,然后在 Chrome DevTools 打开 Network 面板,在 Disable cache
复选框旁的 Network throttling 下拉菜单中选择 Offline(英文界面),再重新加载。外壳从 shell-v3
绘制,/api/ 请求返回最近缓存的副本,导航到未缓存的 URL 时显示 /offline.html。也要从冷启动测试已安装的
应用;热标签页的内存里早已备好一切。
- 选择缓存策略
- Service worker 生命周期:install、activate 与更新
- Service worker 离线兜底
- 存储持久化、配额与逐出
- The Offline Cookbook(web.dev)
- Background Synchronization API(developer.mozilla.org)
- StorageManager: estimate() method(developer.mozilla.org)
← 返回指南总览。