Service Worker 进阶:离线优先架构与后台同步策略

技术架构(更新于 2026年6月4日)

Service Worker:离线优先架构的核心

Service Worker 是浏览器中独立于页面的后台线程,能拦截网络请求、管理缓存、处理推送:

特性 说明
运行环境 独立 Worker 线程,无 DOM 访问
生命周期 install → activate → fetch
作用域 注册路径及其子路径
通信 postMessage / MessageChannel
持久化 事件驱动,不常驻内存

生命周期详解

                  ┌──────────┐
                  │  parsed  │
                  └────┬─────┘
                       │ install()
                  ┌────▼─────┐
            ┌─────│installing│
            │     └────┬─────┘
            │          │ waitUntil()
            │     ┌────▼─────┐
            │     │ installed │ (waiting)
            │     └────┬─────┘
            │          │ activate()
            │     ┌────▼─────┐
            └────▶│activating│
                  └────┬─────┘
                       │
                  ┌────▼─────┐
                  │ activated │ ← 处理 fetch/push/sync
                  └──────────┘

注册与更新

if ('serviceWorker' in navigator) {
  const registration = await navigator.serviceWorker.register('/sw.js', {
    scope: '/',
    updateViaCache: 'none'
  });

  registration.addEventListener('updatefound', () => {
    const newWorker = registration.installing;
    newWorker?.addEventListener('statechange', () => {
      if (newWorker.state === 'activated') {
        showToast('新版本已就绪,刷新生效');
      }
    });
  });
}

Cache API:缓存策略矩阵

五大经典策略

策略 网络优先 适用场景 离线行为
Network Only 非缓存数据(支付/实时) 失败
Network First ✅+降级 频繁更新的 API 返回缓存
Cache First 不变资源(字体/图片) 返回缓存
Cache Only 预缓存的离线页面 返回缓存
Stale While Revalidate 后台更新 HTML/非关键 API 返回旧缓存

策略实现

const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
  '/',
  '/styles/main.css',
  '/scripts/main.js',
  '/offline.html',
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(STATIC_ASSETS))
  );
});

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(
        keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
      )
    )
  );
});

Cache First 实现

async function cacheFirst(request: Request): Promise<Response> {
  const cached = await caches.match(request);
  if (cached) return cached;

  const response = await fetch(request);
  if (response.ok) {
    const cache = await caches.open(CACHE_NAME);
    cache.put(request, response.clone());
  }
  return response;
}

Network First 实现

async function networkFirst(request: Request): Promise<Response> {
  try {
    const response = await fetch(request);
    if (response.ok) {
      const cache = await caches.open(CACHE_NAME);
      cache.put(request, response.clone());
    }
    return response;
  } catch {
    const cached = await caches.match(request);
    if (cached) return cached;
    return caches.match('/offline.html');
  }
}

Stale While Revalidate 实现

async function staleWhileRevalidate(request: Request): Promise<Response> {
  const cache = await caches.open(CACHE_NAME);
  const cached = await cache.match(request);

  const fetchPromise = fetch(request).then((response) => {
    if (response.ok) cache.put(request, response.clone());
    return response;
  });

  return cached || fetchPromise;
}

路由分发

self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);

  if (request.method !== 'GET') return;

  if (url.pathname.startsWith('/api/')) {
    event.respondWith(staleWhileRevalidate(request));
  } else if (url.pathname.match(/\.(css|js|woff2)$/)) {
    event.respondWith(cacheFirst(request));
  } else if (url.pathname.match(/\.(png|jpg|webp|svg)$/)) {
    event.respondWith(cacheFirst(request));
  } else {
    event.respondWith(networkFirst(request));
  }
});

Background Sync:后台同步

注册同步任务

// 页面中注册
async function registerSync() {
  const registration = await navigator.serviceWorker.ready;
  await registration.sync.register('submit-pending-data');
}

// 离线时暂存数据
async function saveOffline(data: object) {
  const db = await openDB('outbox', 1, {
    upgrade(db) {
      db.createObjectStore('pending', { keyPath: 'id', autoIncrement: true });
    }
  });
  await db.add('pending', data);
  await registerSync();
}

Service Worker 处理同步

self.addEventListener('sync', (event) => {
  if (event.tag === 'submit-pending-data') {
    event.waitUntil(submitPendingData());
  }
});

async function submitPendingData() {
  const db = await openDB('outbox', 1);
  const pending = await db.getAll('pending');

  for (const item of pending) {
    try {
      await fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify(item),
      });
      await db.delete('pending', item.id);
    } catch {
      throw new Error('同步失败,将重试');
    }
  }
}

Push API:推送通知

订阅推送

async function subscribePush(publicKey: string) {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: publicKey
  });
  await fetch('/api/push/subscribe', {
    method: 'POST',
    body: JSON.stringify(subscription.toJSON())
  });
}

处理推送

self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? { title: '新消息', body: '' };

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/notification.png',
      badge: '/icons/badge.png',
      vibrate: [200, 100, 200],
      data: { url: data.url },
      actions: [
        { action: 'open', title: '查看' },
        { action: 'dismiss', title: '忽略' }
      ]
    })
  );
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  const url = event.notification.data?.url || '/';

  if (event.action === 'dismiss') return;

  event.waitUntil(
    self.clients.matchAll({ type: 'window' }).then((clients) => {
      for (const client of clients) {
        if (client.url === url && 'focus' in client) {
          return client.focus();
        }
      }
      return self.clients.openWindow(url);
    })
  );
});

Workbox:Google 官方工具链

import { registerRoute } from 'workbox-routing';
import {
  CacheFirst,
  NetworkFirst,
  StaleWhileRevalidate,
} from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';

registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 30 * 24 * 3600 }),
      new CacheableResponsePlugin({ statuses: [0, 200] }),
    ],
  })
);

registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new StaleWhileRevalidate({
    cacheName: 'api',
    plugins: [
      new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 5 * 60 }),
    ],
  })
);

registerRoute(
  ({ request }) => request.mode === 'navigate',
  new NetworkFirst({
    cacheName: 'pages',
    plugins: [
      new CacheableResponsePlugin({ statuses: [0, 200] }),
    ],
  })
);

缓存策略对比

策略 首次加载 后续加载 离线 数据新鲜度 复杂度
Network Only 网络 网络 最新
Network First 网络 网络/缓存 较新
Cache First 网络 缓存 可能旧
Stale While Revalidate 网络 缓存 最终一致

浏览器兼容性

API Chrome Firefox Safari Edge
Service Worker 40+ 44+ 11.1+ 17+
Cache API 40+ 41+ 11.1+ 17+
Background Sync 49+ 不支持 不支持 17+
Push API 50+ 44+ 16+ 17+
Navigation Preload 62+ 不支持 不支持 17+

最佳实践

  1. 预缓存关键资源:install 时缓存 App Shell,保证离线可用
  2. 版本化缓存名app-v1app-v2,activate 时清理旧缓存
  3. 路由分发:不同资源类型使用不同策略
  4. 限制缓存大小:ExpirationPlugin 防止缓存无限增长
  5. 优雅降级:SW 注册失败时页面仍正常工作

总结

Service Worker 是构建离线优先 Web 应用的核心,通过 Cache API 的策略矩阵实现网络与缓存的智能调度,Background Sync 保证离线操作的最终一致性,Push API 实现服务端主动通知。Workbox 工具链大幅降低了实现复杂度。

推荐使用 文件压缩 减少缓存资源体积,PDF 合并 处理离线文档,图片压缩 优化缓存图片。

本站提供浏览器本地工具,免注册即可试用 →

#Service Worker#离线优先#Cache API#后台同步#推送通知