用 Service Worker 实现离线缓存(最小可用 PWA)

Service Worker 是 PWA 的核心:拦截网络请求,按策略返回缓存 / 网络 / 兜底页面,
让网页在无网时也能用。下面写一个最简单但完整的离线缓存层。

1. 注册

// main.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js', { scope: '/' })
      .then(reg => console.log('SW registered:', reg.scope))
      .catch(err => console.error('SW failed:', err))
  })
}

scope: '/' 让 SW 拦截整个站点。SW 文件必须从 同源 root 路径 提供
(不能放 CDN)。

2. SW 主体

// public/sw.js
const VERSION = 'v3-2026-05-23'
const PRECACHE = `precache-${VERSION}`
const RUNTIME = `runtime-${VERSION}`

const PRECACHE_URLS = [
  '/',
  '/index.html',
  '/assets/main.css',
  '/assets/main.js',
  '/offline.html',
]

// 安装:预缓存 shell 资源
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(PRECACHE)
      .then(cache => cache.addAll(PRECACHE_URLS))
      .then(() => self.skipWaiting())   // 立刻激活,不等老 SW 退出
  )
})

// 激活:清理老版本缓存
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys()
      .then(keys => Promise.all(
        keys.filter(k => k !== PRECACHE && k !== RUNTIME)
          .map(k => caches.delete(k))
      ))
      .then(() => self.clients.claim())
  )
})

// fetch:按策略响应
self.addEventListener('fetch', event => {
  const { request } = event
  if (request.method !== 'GET') return   // 只缓存 GET

  const url = new URL(request.url)
  if (url.origin !== self.location.origin) return   // 不管跨域

  // 1. 导航请求 → 网络优先,失败回退到 offline.html
  if (request.mode === 'navigate') {
    event.respondWith(
      fetch(request).catch(() => caches.match('/offline.html'))
    )
    return
  }

  // 2. 静态资源(hash 文件名)→ 缓存优先(永久)
  if (url.pathname.match(/\.(css|js|woff2|png|svg|ico)$/)) {
    event.respondWith(
      caches.match(request).then(cached => {
        if (cached) return cached
        return fetch(request).then(response => {
          if (response.ok) {
            const clone = response.clone()
            caches.open(RUNTIME).then(c => c.put(request, clone))
          }
          return response
        })
      })
    )
    return
  }

  // 3. API 请求 → 网络优先,失败用缓存
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(
      fetch(request).then(response => {
        if (response.ok) {
          const clone = response.clone()
          caches.open(RUNTIME).then(c => c.put(request, clone))
        }
        return response
      }).catch(() => caches.match(request))
    )
    return
  }
})

3. offline 页

<!-- public/offline.html -->
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>离线</title></head>
<body>
  <h1>当前离线</h1>
  <p>请检查网络后重试。已缓存的页面仍然可以打开。</p>
</body>
</html>

4. 三种缓存策略速查

策略 适用 实现
Cache-first 静态资源(hash 文件名)、字体 match → 否则 fetch + cache
Network-first HTML / API(要新鲜数据) fetch → 失败用 cache
Stale-while-revalidate 头像、缩略图(旧的也能用) match 立刻返回 + 异步 fetch 更新

实现 stale-while-revalidate:

caches.match(request).then(cached => {
  const fetched = fetch(request).then(response => {
    if (response.ok) {
      caches.open(RUNTIME).then(c => c.put(request, response.clone()))
    }
    return response
  })
  return cached || fetched   // 有 cache 立刻给,同时后台更新
})

5. Manifest(让浏览器认你是 PWA)

// public/manifest.json
{
  "name": "My App",
  "short_name": "MyApp",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#2563eb",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ]
}
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#2563eb">

满足 PWA 安装条件(SW + manifest + HTTPS + 图标),Chrome 地址栏会出现
"安装应用"图标。

6. 版本更新与"重启提示"

// main.js
navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.addEventListener('updatefound', () => {
    const nw = reg.installing
    nw.addEventListener('statechange', () => {
      if (nw.state === 'installed' && navigator.serviceWorker.controller) {
        // 新版本可用
        if (confirm('有新版本,立即刷新?')) {
          window.location.reload()
        }
      }
    })
  })
})

7. 调试

Chrome DevTools → Application → Service Workers:

  • "Update on reload" 勾上,每次 F5 强制 SW 重新安装
  • "Bypass for network" 临时禁用 SW
  • "Unregister" 卸载(开发期常用)

Cache 内容在同面板的 Cache Storage 看。

8. Workbox

手写 SW 没问题,但生态成熟后用 Google 的 Workbox:

npm i -D vite-plugin-pwa workbox-window

vite-plugin-pwa 配几行就生成 SW + manifest + precache,并处理"新版本提示"
的边界。生产推荐。

踩过的坑

  • SW 必须 HTTPS(localhost 例外)。HTTP 网站永远 register 不上。
  • 改了 SW 文件浏览器要等"老 SW 没有 client" 时才激活新版;测试期间
    关掉整个 tab 才生效,反复测试很烦——用 self.skipWaiting() +
    self.clients.claim() 强制立刻接管。
  • SW 缓存了一个 redirect 响应后,整个站点行为奇怪。Cache.put()
    不要存 response.redirected 的 response。
  • 缓存条目 LRU 上限是浏览器决定的(Chrome 总配额几百 MB-几 GB);
    超额时按时间删除最老条目,缓存可能突然失效。
精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。