Service Worker 是支持离线的渐进式 Web 应用程序的基础。随着 Chrome、Firefox、Safari 和 Edge 浏览器的普遍支持,构建弹性离线体验是一项生产要求,而不是逐步增强。本指南涵盖了缓存、同步和离线优先架构的高级策略。
Service Worker 生命周期和基础知识
Service Worker 会经历三个关键事件:install、activate 和 fetch。在安装过程中,您可以预先缓存静态资源。激活处理旧缓存的清理。每个获取请求都会通过工作线程,让您可以完全控制响应。
const CACHE_NAME = "my-app-v2";
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) =>
cache.addAll(["/", "/styles/main.css", "/scripts/app.js"])
)
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
)
);
});
self.addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request));
});
在安装中使用 self.skipWaiting() 并在激活中使用 clients.claim() 在更新后立即控制所有打开的页面。
缓存策略深入探讨
选择正确的缓存策略取决于资源类型、更新频率和连接要求。
| 战略 | 最适合 | 权衡 |
|---|---|---|
| 缓存优先 | 静态资源、图像 | 过时的内容直到下一次软件更新 |
| 网络优先 | API 响应、HTML | 在线时速度较慢 |
| 重新验证时过时 | 混合内容 | 即时加载+后台刷新 |
| 仅缓存 | 不可变的哈希资产 | 如果丢失则没有后备 |
| 仅限网络 | 实时数据、突变 | 没有离线支持 |
Stale-while-revalidate 是大多数内容的建议默认值。它立即返回缓存的版本并在后台获取新的副本:
async function staleWhileRevalidate(request) {
const cache = await caches.open("dynamic-v1");
const cached = await cache.match(request);
const fetchPromise = fetch(request).then((response) => {
cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}
通过使用挂起的请求映射来处理同时页面加载期间的竞争条件,以避免重复的网络获取。
后台同步和定期同步
后台同步 API 允许您推迟操作,直到用户具有稳定的连接。这对于离线表单提交和排队 API 突变来说是理想的选择。
// Register a sync event in the page
async function queueSubmission(data) {
const db = await openDB("offline-queue", 1);
await db.add("submissions", data);
await navigator.serviceWorker.ready.then((sw) =>
sw.sync.register("submit-data")
);
}
// Handle sync in the service worker
self.addEventListener("sync", (event) => {
if (event.tag === "submit-data") {
event.waitUntil(processQueue());
}
});
定期同步更进一步,允许按浏览器定义的时间间隔进行后台更新。使用它来预取新闻文章、天气数据或社交媒体源。请注意,定期同步需要用户授予的权限,目前在 Safari 中受到限制。
离线优先架构
离线优先意味着设计您的应用程序,使离线成为默认设置,并增强连接性。 IndexedDB 充当事实来源,远程同步在后台进行。
interface SyncQueue {
id: string;
action: "create" | "update" | "delete";
endpoint: string;
body: unknown;
createdAt: Date;
}
class OfflineStore {
private db: IDBDatabase;
async enqueue(entry: SyncQueue) {
const tx = this.db.transaction("sync-queue", "readwrite");
await tx.store.add(entry);
await navigator.serviceWorker.ready.then((sw) =>
sw.sync.register("sync-queue")
);
}
async processQueue() {
const tx = this.db.transaction("sync-queue", "readonly");
const entries = await tx.store.getAll();
for (const entry of entries) {
try {
await fetch(entry.endpoint, {
method: "POST",
body: JSON.stringify(entry.body),
});
const deleteTx = this.db.transaction("sync-queue", "readwrite");
await deleteTx.store.delete(entry.id);
} catch {
break; // Stop processing if offline
}
}
}
}
冲突解决策略很重要:最后写入获胜很简单,但有损; CRDT 和操作转换在协作场景中保留用户意图。考虑使用 RxDB 或 PouchDB 等框架来提供内置的离线优先支持和自动复制功能。
高级缓存管理和测试
缓存版本控制可防止在部署后提供过时的资产。在缓存名称中使用版本字符串并在激活期间删除旧缓存。使用 navigator.storage.estimate() 管理存储配额并请求持久存储以防止驱逐:
async function requestPersistentStorage() {
if (navigator.storage && navigator.storage.persist) {
const granted = await navigator.storage.persist();
console.log(`Persistent storage: ${granted ? "granted" : "denied"}`);
}
}
对于测试,Chrome DevTools 提供手动离线模拟和缓存检查。 Playwright 的自动化测试可以验证离线行为:
import { test, expect } from "@playwright/test";
test("app works offline", async ({ page, context }) => {
await page.goto("https://my-app.com");
await context.setOffline(true);
await page.reload();
await expect(page.locator("h1")).toHaveText("My App");
});
导航预加载可减少服务工作线程启动对性能的影响。在安装处理程序中启用它,并使用预加载响应作为网络优先的后备。
Service Worker 现在是确保网络可靠性的生产标准工具。从内容的重新验证开始,为突变添加后台同步,并随着架构的成熟而向完全离线优先发展。其结果是一个应用程序尊重用户,无论他们的连接如何。

