PWA 与 Service Worker:离线化、缓存策略与服务扩展

系统性 PWA 实战指南:Service Worker 注册与生命周期(install/activate/fetch)、缓存策略(Cache-First/Network-First/Stale-While-Revalidate/Cache-Only/Network-Only)、Workbox 自动化缓存、Background Sync、Push 通知、Web App Manifest、Add to Home Screen(A2HS)、Service Worker 更新机制与版本控制、PWA 与 SPA/SSR 结合。附 Vue/React/Next.js 完整配置。

PWA 的目标不是「替代原生应用」,而是「在 Web 的能力边界内提供尽可能接近原生的体验」。 一个合格的 PWA 可以在弱网甚至离线环境下工作、支持推送通知、能被用户添加到主屏幕——这些曾经只属于原生应用的能力,现在浏览器已经免费开放。


一、PWA 核心组成

PWA = HTTPS + Service Worker + Web App Manifest
      ├── HTTPS          — 安全上下文(SW 要求)
      ├── Service Worker — 代理网络请求、缓存、后台任务
      └── Web App Manifest — 应用元数据(名称、图标、主题色)

扩展能力:
├── Push API           — 服务器推送通知
├── Background Sync    — 离线操作,恢复网络后自动同步
├── Notifications API  — 本地通知
└── Add to Home Screen — 安装到主屏幕

二、Web App Manifest

2.1 manifest.json

{
  "name": "My Awesome App",
  "short_name": "MyApp",
  "description": "A progressive web app built with Vue",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#3b82f6",
  "orientation": "portrait",
  "scope": "/",
  "icons": [
    { "src": "/icons/icon-72x72.png", "sizes": "72x72", "type": "image/png" },
    { "src": "/icons/icon-96x96.png", "sizes": "96x96", "type": "image/png" },
    { "src": "/icons/icon-128x128.png", "sizes": "128x128", "type": "image/png" },
    { "src": "/icons/icon-144x144.png", "sizes": "144x144", "type": "image/png", "purpose": "any maskable" },
    { "src": "/icons/icon-152x152.png", "sizes": "152x152", "type": "image/png" },
    { "src": "/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/icon-384x384.png", "sizes": "384x384", "type": "image/png" },
    { "src": "/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png" }
  ],
  "screenshots": [
    { "src": "/screenshots/narrow.png", "sizes": "750x1334", "type": "image/png", "form_factor": "narrow" },
    { "src": "/screenshots/wide.png", "sizes": "1920x1080", "type": "image/png", "form_factor": "wide" }
  ],
  "categories": ["productivity", "utilities"],
  "lang": "zh-CN"
}

2.2 display 模式

行为适用
standalone无浏览器 UI,像原生应用✅ 推荐
fullscreen全屏,无系统 UI游戏、沉浸式
minimal-ui最小浏览器控件阅读器
browser普通浏览器标签无特殊处理

三、Service Worker 生命周期

3.1 三个核心事件

// sw.js
const CACHE_NAME = 'my-app-v2';
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/app.js',
  '/app.css',
  '/icons/icon-192x192.png'
];

// 1. Install:首次注册时触发,预缓存核心资源
self.addEventListener('install', (event) => {
  console.log('[SW] Install');
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(STATIC_ASSETS))
      .then(() => self.skipWaiting()) // 立即激活新 SW
  );
});

// 2. Activate:新 SW 接管页面,清理旧缓存
self.addEventListener('activate', (event) => {
  console.log('[SW] Activate');
  event.waitUntil(
    caches.keys().then((cacheNames) =>
      Promise.all(
        cacheNames
          .filter((name) => name !== CACHE_NAME)
          .map((name) => caches.delete(name))
      )
    ).then(() => self.clients.claim()) // 立即控制所有页面
  );
});

// 3. Fetch:拦截网络请求
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // 命中缓存则返回,否则走网络
      return response || fetch(event.request);
    })
  );
});

3.2 注册 Service Worker

// main.ts / main.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/sw.js')
      .then((registration) => {
        console.log('SW registered:', registration.scope);

        // 监听更新
        registration.addEventListener('updatefound', () => {
          const newWorker = registration.installing;
          newWorker?.addEventListener('statechange', () => {
            if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
              // 新 SW 已安装但等待激活 → 提示用户刷新
              showUpdateNotification();
            }
          });
        });
      })
      .catch((err) => console.error('SW registration failed:', err));
  });
}

function showUpdateNotification() {
  const confirmed = confirm('新版本可用,是否刷新?');
  if (confirmed) {
    navigator.serviceWorker.controller?.postMessage({ type: 'SKIP_WAITING' });
    window.location.reload();
  }
}

3.3 SW 更新机制

Service Worker 更新流程:

1. 浏览器每 24h 自动检查 sw.js 是否有变化(字节对比)
2. 检测到变化 → 下载新 SW → 触发 install
3. 新 SW 进入 "waiting" 状态(旧 SW 仍在控制页面)
4. 用户关闭所有相关标签页 → 新 SW activate
5. 或使用 skipWaiting() 强制立即激活

⚠️ 注意:
- SW 安装后持久存在,不是每次加载都重新安装
- 更新时旧缓存不会自动删除,需在 activate 中清理
- 建议通过版本号管理缓存(CACHE_NAME = 'app-v3')

四、缓存策略

4.1 五种策略

策略对比:
┌─────────────────────────────────────────────────────────────┐
│ Cache-First                                                 │
│   先读缓存 → 命中返回 / 未命中 fetch → 写入缓存             │
│   适合:静态资源(JS/CSS/图片)、不常变的数据               │
├─────────────────────────────────────────────────────────────┤
│ Network-First                                               │
│   先 fetch → 成功返回 + 写入缓存 / 失败读缓存               │
│   适合:实时数据、API 响应                                  │
├─────────────────────────────────────────────────────────────┤
│ Stale-While-Revalidate                                      │
│   先读缓存返回 → 同时 fetch 更新缓存 → 下次命中新缓存       │
│   适合:新闻列表、商品列表(显示旧数据比空白好)            │
├─────────────────────────────────────────────────────────────┤
│ Cache-Only                                                  │
│   只读缓存,不发起网络请求                                  │
│   适合:构建时预缓存的离线页面                              │
├─────────────────────────────────────────────────────────────┤
│ Network-Only                                                │
│   只走网络,不缓存                                          │
│   适合:实时性要求极高的请求                                │
└─────────────────────────────────────────────────────────────┘

4.2 策略实现

// sw.js — 策略路由
self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);

  // 1. 静态资源:Cache-First + 长期缓存
  if (request.destination === 'style' || request.destination === 'script') {
    event.respondWith(cacheFirst(request, STATIC_CACHE));
    return;
  }

  // 2. API 请求:Network-First + 短时间缓存
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(request, API_CACHE, { maxAge: 5 * 60 * 1000 }));
    return;
  }

  // 3. 图片:Stale-While-Revalidate
  if (request.destination === 'image') {
    event.respondWith(staleWhileRevalidate(request, IMAGE_CACHE));
    return;
  }

  // 4. 页面导航:Network-First,离线回退到离线页面
  if (request.mode === 'navigate') {
    event.respondWith(
      fetch(request).catch(() => caches.match('/offline.html'))
    );
    return;
  }

  // 默认
  event.respondWith(fetch(request));
});

// Cache-First
async function cacheFirst(request, cacheName) {
  const cache = await caches.open(cacheName);
  const cached = await cache.match(request);
  if (cached) return cached;

  const response = await fetch(request);
  cache.put(request, response.clone());
  return response;
}

// Network-First
async function networkFirst(request, cacheName, options = {}) {
  const cache = await caches.open(cacheName);
  try {
    const networkResponse = await fetch(request);
    cache.put(request, networkResponse.clone());
    return networkResponse;
  } catch (err) {
    const cached = await cache.match(request);
    if (cached) return cached;
    throw err;
  }
}

// Stale-While-Revalidate
async function staleWhileRevalidate(request, cacheName) {
  const cache = await caches.open(cacheName);
  const cached = await cache.match(request);

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

  return cached || fetchPromise;
}

五、Workbox:自动化 PWA

5.1 为什么选择 Workbox

手写 Service Worker 容易出错,Workbox 提供:

  • 预缓存清单自动生成
  • 多种缓存策略内置
  • 缓存过期与大小限制
  • 后台同步、离线分析
  • TypeScript 支持

5.2 注入式预缓存(Vite 集成)

pnpm add -D workbox-window
pnpm add -D vite-plugin-pwa
// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa';

export default {
  plugins: [
    VitePWA({
      registerType: 'autoUpdate',
      workbox: {
        globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
        runtimeCaching: [
          {
            urlPattern: /^https:\/\/api\.example\.com\/.*/i,
            handler: 'NetworkFirst',
            options: {
              cacheName: 'api-cache',
              expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 }
            }
          },
          {
            urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp|avif)$/,
            handler: 'CacheFirst',
            options: {
              cacheName: 'images',
              expiration: { maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 30 }
            }
          }
        ]
      },
      manifest: {
        name: 'My Awesome App',
        short_name: 'MyApp',
        theme_color: '#3b82f6',
        icons: [
          { src: '/icon-192x192.png', sizes: '192x192', type: 'image/png' },
          { src: '/icon-512x512.png', sizes: '512x512', type: 'image/png' }
        ]
      }
    })
  ]
};

5.3 Vue 3 中使用

// main.ts
import { registerSW } from 'virtual:pwa-register';

const updateSW = registerSW({
  immediate: true,
  onNeedRefresh() {
    // 显示更新提示
    const toast = document.createElement('div');
    toast.innerHTML = `
      <div style="position:fixed;bottom:20px;right:20px;background:#333;color:#fff;padding:12px 20px;border-radius:8px;cursor:pointer;">
        新版本可用,点击更新
      </div>
    `;
    toast.onclick = () => updateSW(true);
    document.body.appendChild(toast);
  },
  onOfflineReady() {
    console.log('App ready to work offline');
  }
});

六、Background Sync

6.1 离线操作,在线同步

// 页面中注册后台同步
navigator.serviceWorker.ready.then((registration) => {
  document.getElementById('submit-btn').addEventListener('click', async () => {
    const data = { message: 'Hello from offline' };

    // 尝试直接发送
    try {
      await fetch('/api/messages', {
        method: 'POST',
        body: JSON.stringify(data),
        headers: { 'Content-Type': 'application/json' }
      });
    } catch (err) {
      // 网络失败 → 注册后台同步
      await registration.sync.register('send-message');
      // 存入 IndexedDB 等待同步
      await db.messages.add({ ...data, synced: false });
    }
  });
});
// sw.js 中接收同步事件
self.addEventListener('sync', (event) => {
  if (event.tag === 'send-message') {
    event.waitUntil(sendPendingMessages());
  }
});

async function sendPendingMessages() {
  const messages = await db.messages.where('synced').equals(false).toArray();
  for (const msg of messages) {
    try {
      await fetch('/api/messages', {
        method: 'POST',
        body: JSON.stringify(msg),
        headers: { 'Content-Type': 'application/json' }
      });
      await db.messages.update(msg.id, { synced: true });
    } catch (err) {
      console.error('Sync failed for message:', msg.id);
    }
  }
}

七、Push 通知

7.1 服务端推送

// 1. 页面订阅 Push
const subscription = await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
});

// 2. 将 subscription 发送到服务器保存
await fetch('/api/subscribe', {
  method: 'POST',
  body: JSON.stringify(subscription),
  headers: { 'Content-Type': 'application/json' }
});
// sw.js 接收推送
self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? { title: 'Notification', body: '' };

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icon-192x192.png',
      badge: '/badge-72x72.png',
      data: { url: data.url },
      actions: [
        { action: 'open', title: '打开' },
        { action: 'close', title: '关闭' }
      ]
    })
  );
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  if (event.action === 'open') {
    clients.openWindow(event.notification.data.url);
  }
});

八、PWA Checklist

检查项Lighthous ePWA 要求实现
HTTPS✅ 必须部署配置
Service Worker✅ 必须sw.js + 注册
Manifest✅ 必须manifest.json +
离线回退✅ 推荐offline.html + cache
图标✅ 必须多尺寸 PNG + maskable
主题色✅ 推荐theme-color meta + manifest
响应式✅ 必须Viewport meta + CSS
A2HS 提示可选beforeinstallprompt
Push 通知可选Push API + 服务端
Background Sync可选Sync API + IndexedDB

参考与延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章

  1. API 设计与 BFF 层:REST、GraphQL、tRPC 选型与前后端协作
  2. WebAssembly 前端工程化实践:编译链、性能对比与混合架构
  3. 现代浏览器 API 与 Web 平台能力地图