Please enable Javascript to view the contents

Service Worker 使用注意事项总结

 ·  ☕ 7 分钟

起因是我写了一个儿童游戏导航的网站,https://www.kidsgame.online,是一个PWA应用。为了方便用户添加到桌面,我在网页底部一个添加到桌面的按钮。这个按钮的作用就是调用浏览器的安装PWA功能,把这个网站安装成一个桌面应用。安装成桌面应用后,用户就可以像打开普通软件一样打开这个网站了。

结果触发了两个问题:一是在开发环境总是拦截其他的项目,二是在生产环境应用只是一直显示之前的项目。根本原因就是service worker没有写好。

当然,我要甩锅给AI。因为这是AI写的,AI有时候确实只管杀,不管埋。你要是心里没点数,还真的容易被坑。

添加到桌面按钮

要注销也比较简单,如上图所示:

  • 打开你的 PWA 桌面应用(就是那个旧界面)
  • F12 或右键 → 检查 → 打开开发者工具
  • 切换到 Application 面板(中文叫“应用程序”)
  • 左边栏选 Service Workers
  • 你会看到有 1 个或多个 service worker(通常 scope 是 http://localhost:3000/ 或 https://localhost:3000/)
  • 点击 Unregister(取消注册)那个 service worker
  • 然后右键刷新按钮 → 选 清空缓存并硬性重新加载(Empty Cache and Hard Reload)

当然,我们要从根本上解决这个问题。


PWA(Progressive Web App)简介

Manifest 和 Service Worker 的配合是构建现代 PWA(渐进式Web应用) 的核心,作用是使网页具有类似原生App的特性。Manifest 配置文件定义App图标、名称及启动样式,实现安装功能;Service Worker 运行在后台,负责缓存资源、拦截请求和实现离线访问。

  • 实现离线访问与快速加载: Service Worker 拦截网络请求并利用 Cache API 缓存资源,使得用户在断网或网络差时仍能打开应用,并极大加快页面加载速度。
  • 安装与应用化体验: Manifest 提供 App 描述信息,让浏览器可在桌面或主屏幕生成图标,并以独立窗口(无地址栏)形式启动。
  • 后台同步与推送: Service Worker 可以在后台同步数据(后台同步 API)和接收通知(推送 API),确保应用即便关闭也能更新数据。
  • 性能优化与资源管理: 相比传统的缓存机制,Service Worker 提供了更细粒度、灵活的资源缓存控制。

我之前的网站都会添加 Manifest 文件,但没有写 Service Worker,所以就没有安装成桌面应用的功能。这一次是特意为了小朋友及小朋友家长写的,所以就添加了 Service Worker 来实现安装成桌面应用的功能。结果就出现了上面说的问题。

Service Worker

问题 1:本地开发时,注册了 A 项目的 SW 后,其他项目(不同端口)也被影响,显示 A 项目的界面/缓存

根本原因

Service Worker 是基于 origin + scope 注册的。在本地开发时,所有 localhost 项目共享同一个 origin(http://localhost),只是端口不同。
但浏览器对 Service Worker 的 scope 判断是基于注册时的 URL 路径,而不是端口。
→ 如果你把 SW 注册在根路径(scope: ‘/’),它会控制 整个 origin(即所有 localhost 的端口,只要路径匹配)。
这就导致你在 3000 端口注册了 SW 后,打开 5173、4200、8080 等其他项目时,也会被同一个 SW 拦截 → 缓存错乱、显示旧界面。

推荐解决方案(按优先级排序)

优先级 做法 实现方式 适用场景 优缺点
1(最推荐) 开发环境完全禁用 SW 在注册代码里加条件 几乎所有项目 简单、无副作用
2 限定 scope 为当前端口无关的子路径 register(’/sw.js’, { scope: ‘/pwa_dev/’ }) 需要在 dev 也测试 SW 稍麻烦,但可隔离
3 每次 dev 启动时主动 unregister 所有 SW 写个 dev 专用脚本 偶尔测试 SW 时用 手动操作多

代码示例(最常用写法)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// app/layout.tsx 或 _app.tsx('use client' 组件)
'use client'

import { useEffect } from 'react'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    // 只在生产环境注册 SW
    if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
      window.addEventListener('load', () => {
        navigator.serviceWorker
          .register('/sw.js')
          .then(reg => console.log('SW registered', reg.scope))
          .catch(err => console.error('SW registration failed', err))
      })
    }

    // 可选:开发环境主动清理旧 SW(防止历史遗留)
    if (process.env.NODE_ENV !== 'production' && 'serviceWorker' in navigator) {
      navigator.serviceWorker.getRegistrations().then(registrations => {
        for (let reg of registrations) {
          reg.unregister().then(bool => {
            console.log('Unregistered old SW in dev mode:', bool)
          })
        }
      })
    }
  }, [])

  return (
    <html lang="zh">
      <body>{children}</body>
    </html>
  )
}
  • 把上面这段放进根 layout,基本就能解决 90% 的本地干扰问题。
  • 如果你用 Serwist / Workbox 等库,也是在注入注册代码的地方加 if (process.env.NODE_ENV === 'production') 判断。

问题 2:生产环境部署新版本后,用户浏览器不自动更新(还在用旧缓存 / 旧 SW)

根本原因

Service Worker 的更新机制很保守:

  • SW 文件内容没变 → 浏览器认为没更新(即使你改了其他 JS/CSS/HTML)
  • 即使 SW 文件变了,也要等页面 refresh + activate 阶段才会接管
  • 很多项目没实现 skipWaiting / clients.claim(),导致旧客户端一直卡在 waiting 状态

可靠更新策略

  1. SW 文件本身要能“版本化”(最重要一步)
    • 不要让 SW 文件内容不变 → 每次构建时强制让它变(加版本注释或 hash)
    • 常见做法:在 sw.js 里加一行注释:
1
2
3
// public/sw.js
const CACHE_VERSION = '2026-03-04-v17'  // 每次部署手动或自动改这个
// 或者用构建时注入: const CACHE_VERSION = '__BUILD_TIMESTAMP__'
  1. 强制激活新 SW(skip waiting + claim clients)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// sw.js 示例(基础版 + 更新逻辑)
const CACHE_NAME = 'my-app-v1.2.3'

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => {
      return cache.addAll(['/','/offline.html' /* + precache list */])
    })
  )
  // 关键:新 SW 安装完立刻激活,不要等旧窗口关闭
  self.skipWaiting()
})

self.addEventListener('activate', event => {
  event.waitUntil(
    Promise.all([
      // 清理旧缓存
      caches.keys().then(keys =>
        Promise.all(
          keys.filter(key => key !== CACHE_NAME)
              .map(key => caches.delete(key))
        )
      ),
      // 立即接管所有打开的页面
      self.clients.claim()
    ])
  )
})

self.addEventListener('fetch', event => {
  // 你的缓存策略:cache-first / network-first 等
})
  1. 提示用户更新(可选但强烈推荐)

在客户端检测到有 waiting 的 SW 时,弹窗提示“有新版本,刷新获取”。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 某个 client 组件
useEffect(() => {
  let refreshing = false

  navigator.serviceWorker.addEventListener('controllerchange', () => {
    if (refreshing) return
    refreshing = true
    window.location.reload()
  })

  navigator.serviceWorker.register('/sw.js').then(reg => {
    reg.addEventListener('updatefound', () => {
      const newWorker = reg.installing
      if (newWorker) {
        newWorker.addEventListener('statechange', () => {
          if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
            // 有更新可用,弹出提示
            if (confirm('有新版本可用,是否立即更新?')) {
              newWorker.postMessage({ type: 'SKIP_WAITING' })
            }
          }
        })
      }
    })
  })
}, [])

总结建议

  • 开发环境process.env.NODE_ENV !== 'production' → 不注册 + 主动 unregister
  • 生产环境更新
    1. 用 Serwist(或 Workbox)生成 SW → 自动 hash 化 precache
    2. 实现 skipWaiting() + clients.claim()
    3. 可选加客户端提示更新弹窗

其它注意事项

Service Worker 是运行在浏览器后台的独立线程,用于提升离线体验和性能。注意事项包括:

必须在 HTTPS 环境下运行(localhost除外);无法直接访问 DOM;基于异步事件机制(如 install/activate);需谨慎处理缓存更新与 版本管理;合理管理存储空间以防超出限制。 

一、 开发与环境限制 

  • HTTPS 必需:出于安全考量,Service Worker 只能在安全上下文(HTTPS)中注册。本地开发可以使用 localhost127.0.0.1
  • 无 DOM 访问权限:运行在独立的 Worker 线程中,不能直接操作页面 DOM。
  • 不可用同步 API:无法使用 localStorageXMLHttpRequest (XHR),需使用 IndexedDBfetch API。
  • 浏览器兼容性:大多数现代浏览器支持,但需考虑旧版本浏览器支持情况。 

二、 生命周期与更新管理 

  • 注册作用域 (Scope):Service Worker 默认只能控制注册路径及其子路径下的资源,需注意注册位置。
  • 生命周期更新:新版本 Service Worker 安装完成后会进入等待状态(waiting),直到旧版本控制的所有页面都关闭才能激活(activate)。
  • 强制更新:使用 self.skipWaiting() 可强制新版本立即激活,使用 clients.claim() 可立即接管页面。
  • 旧缓存清理:在 activate 事件中清理过期的旧缓存,防止堆积导致存储空间不足。 ![Chrome for Developers]

三、 性能与最佳实践 

  • 缓存策略:根据资源类型选择合适策略:静态资源用 Cache-First,动态内容用 Network-First,高频更新用 Stale-While-Revalidate。
  • 资源配额:浏览器有缓存配额限制,需精细控制缓存内容,避免缓存过多导致存储空间不足。
  • 使用工具:建议使用 Google 的 Workbox 库来管理缓存和简化生命周期处理。

四、 调试技巧 

  • Chrome 开发者工具:使用 Application -> Service Workers 面板查看状态、更新和模拟离线。
  • 调试检查:勾选 “Update on reload” 可在每次刷新时更新 Service Worker。
  • 强制刷新:使用 Shift + 刷新 可以强制绕过 Service Worker 加载资源。
分享

码农真经
作者
码农真经
Web Developer