Featured image of post Service Workers:Web 应用程序的高级离线策略Featured image of post Service Workers:Web 应用程序的高级离线策略

Service Workers:Web 应用程序的高级离线策略

高级 Service Worker 离线策略,涵盖缓存优先与网络优先、Workbox 集成、后台同步、定期同步、重新验证时过时和离线优先架构。

Service Worker 是支持离线的渐进式 Web 应用程序的基础。随着 Chrome、Firefox、Safari 和 Edge 浏览器的普遍支持,构建弹性离线体验是一项生产要求,而不是逐步增强。本指南涵盖了缓存、同步和离线优先架构的高级策略。

Service Worker 生命周期和基础知识

Service Worker 会经历三个关键事件:installactivatefetch。在安装过程中,您可以预先缓存静态资源。激活处理旧缓存的清理。每个获取请求都会通过工作线程,让您可以完全控制响应。

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 现在是确保网络可靠性的生产标准工具。从内容的重新验证开始,为突变添加后台同步,并随着架构的成熟而向完全离线优先发展。其结果是一个应用程序尊重用户,无论他们的连接如何。