← 返回文章列表
浏览器与网络·912 次阅读

Service Worker 与 PWA 入门:给网页加上离线能力

地铁里刷技术文章,进隧道断网,页面直接白屏——这种体验在 2022 年已经不该存在了。这周给自己的小项目接了 Service Worker,断网也能照常浏览。把实现过程整理成一篇入门指南。

Service Worker 是什么

一句话:跑在独立线程里的可编程网络代理。

它的关键特性:

  • 独立于页面的线程,页面关了它还在
  • 能拦截页面发出的所有网络请求,决定给缓存还是给网络
  • 只能跑在 HTTPS 下(localhost 例外)
  • 生命周期特殊:install → activate → fetch,不随页面刷新重来

有了"拦截请求"这个能力,离线缓存、资源预载、后台同步都成了可能。

最小实现:三步走

第一步:注册(页面主线程)

// main.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then((reg) => console.log('SW 注册成功,范围:', reg.scope))
      .catch((err) => console.error('SW 注册失败:', err))
  })
}

第二步:安装时预缓存(sw.js)

const CACHE_NAME = 'app-v1'
const PRECACHE = ['/', '/index.html', '/app.css', '/app.js', '/logo.png']

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE))
  )
  self.skipWaiting() // 立即激活,不等旧 SW 控制的页面关闭
})

第三步:拦截请求,缓存优先(sw.js)

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request)
    })
  )
})

这三段跑起来,第二次访问就是秒开,断网也能打开首页。

缓存策略:不是只有"缓存优先"

不同资源该用不同策略,这是 SW 缓存的核心学问。

Cache First(缓存优先):上面写的那个。适合带 hash 的静态资源(app.a3f2.js),内容永不变,缓存命中直接用。

Network First(网络优先):适合API 数据,新鲜度优先,断网才退回缓存:

self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      fetch(event.request)
        .then((response) => {
          // 网络成功,顺手更新缓存
          const clone = response.clone()
          caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone))
          return response
        })
        .catch(() => caches.match(event.request)) // 断网用缓存兜底
    )
  }
})

Stale-While-Revalidate(先给缓存,后台更新):体验最好的策略,适合文章、图片这类内容资源:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      const fetchPromise = fetch(event.request).then((response) => {
        const clone = response.clone()
        caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone))
        return response
      })
      // 有缓存先返回缓存,同时后台发请求更新缓存
      return cached || fetchPromise
    })
  )
})

用户永远秒开,下次访问看到的就是新内容。取舍是可能看到旧版内容,别用在强实时数据上。

版本更新:activate 时清旧缓存

发了新版本(CACHE_NAME 从 app-v1 改 app-v2),老缓存要清掉:

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

更新机制的经典坑:新 sw.js 下载后会进入 waiting 状态,默认要等所有受控页面关闭才激活。所以 install 里那句 self.skipWaiting() 很关键;更完整的方案是配合页面提示"有新版本,点击刷新"。

成为完整 PWA 还差什么

SW 解决离线,PWA 还差一点仪式感:

{
  "name": "我的博客",
  "short_name": "博客",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#409eff",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ]
}

加上这个 manifest.webmanifest 并在 HTML 里 link,Android Chrome 就会提示"添加到主屏幕",点开是全屏无地址栏的类原生体验。

调试:Application 面板

DevTools → Application → Service Workers,可以:

  • 查看 SW 状态(installing / waiting / active)
  • 勾选 Offline 模拟断网
  • 勾选 Update on reload 每次刷新更新 SW(开发必勾,不然被旧 SW 坑死)
  • Cache Storage 里查看和清空缓存

开发时的最大教训:写完 SW 记得勾 Update on reload,或者干脆无痕窗口测。

小结

  • Service Worker 是可编程网络代理,HTTPS 限定,独立于页面线程
  • 三步走:注册 → install 预缓存 → fetch 拦截
  • 缓存策略按资源选:静态资源 Cache First、API Network First、内容 SWR
  • 版本更新靠 CACHE_NAME 改名 + activate 清旧 + skipWaiting
  • 开发必勾 Update on reload

接入 SW 后我的小站 Lighthouse PWA 项全绿,断网体验直接拉满。这个 API 的想象空间还很大——后台同步、推送通知都是它的延伸,后面慢慢玩。

看到这里了?如果这篇文章帮到过你

请我喝瓶可乐就好 🥤

收款码

评论0 条

还没有评论,来抢沙发~

发表评论

评论需审核后显示

© 2026 许冬博客 · 专注 JS 技术分享

·RSS 订阅·Powered by Nuxt 4