起因是我写了一个儿童游戏导航的网站,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 来实现安装成桌面应用的功能。结果就出现了上面说的问题。

问题 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 时用 | 手动操作多 |
代码示例(最常用写法)
|
|
- 把上面这段放进根 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 状态
可靠更新策略
- SW 文件本身要能“版本化”(最重要一步)
- 不要让 SW 文件内容不变 → 每次构建时强制让它变(加版本注释或 hash)
- 常见做法:在 sw.js 里加一行注释:
|
|
- 如果用 Serwist(强烈推荐),它会自动 precache manifest(包含所有静态资源的 hash),所以 SW 文件内容会因为 manifest 变而变 → 自动触发更新。serwist/serwist: A Swiss Army knife for service workers.
- 强制激活新 SW(skip waiting + claim clients)
|
|
- 提示用户更新(可选但强烈推荐)
在客户端检测到有 waiting 的 SW 时,弹窗提示“有新版本,刷新获取”。
|
|
总结建议
- 开发环境:
process.env.NODE_ENV !== 'production'→ 不注册 + 主动 unregister - 生产环境更新:
- 用 Serwist(或 Workbox)生成 SW → 自动 hash 化 precache
- 实现
skipWaiting()+clients.claim() - 可选加客户端提示更新弹窗
其它注意事项
Service Worker 是运行在浏览器后台的独立线程,用于提升离线体验和性能。注意事项包括:
必须在 HTTPS 环境下运行(localhost除外);无法直接访问 DOM;基于异步事件机制(如 install/activate);需谨慎处理缓存更新与 版本管理;合理管理存储空间以防超出限制。
一、 开发与环境限制
- HTTPS 必需:出于安全考量,Service Worker 只能在安全上下文(HTTPS)中注册。本地开发可以使用
localhost或127.0.0.1。 - 无 DOM 访问权限:运行在独立的 Worker 线程中,不能直接操作页面 DOM。
- 不可用同步 API:无法使用
localStorage或XMLHttpRequest(XHR),需使用IndexedDB和fetchAPI。 - 浏览器兼容性:大多数现代浏览器支持,但需考虑旧版本浏览器支持情况。
二、 生命周期与更新管理
- 注册作用域 (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 加载资源。