Service Worker + PWA 完全教程
Service Worker 把 Web 从「需要联网才能用」推到「可离线、可安装、可推送」的新阶段。配合 Web App Manifest,普通网站可以在手机桌面以独立图标启动、全屏运行、断网时仍能浏览主要内容、关闭后接收推送通知。Twitter Lite、星巴克、Uber、Pinterest 等公司都靠 PWA 把页面变成「类原生」体验、让转化率提升 30 到 200。本文从 SW 生命周期、缓存策略到 Workbox 库实战、推送通知、离线 UX、Manifest 配置、安装引导、调试技巧,系统讲清 2026 年的 PWA 完整开发流程。
1. Service Worker 是什么:可编程的网络代理
Service Worker 是运行在浏览器后台、独立于网页主线程的脚本。最核心的能力是拦截 origin 内所有出站网络请求(fetch event),开发者可以自定义响应:从 Cache Storage 取本地缓存、改写请求、合成响应、打日志、做后台同步。这让网页第一次拥有了「中间层代理」的能力。
SW 工作在浏览器主进程之外,没有 DOM 访问权限。它能用的 API 包括 Fetch、Cache Storage、IndexedDB、Push、Notification、postMessage(与页面通信)。它不能用 localStorage、document、window 这些窗口相关 API。
使用前提两条:HTTPS(localhost 与 file 例外)、用户首次访问注册。注册代码:navigator.serviceWorker.register("/sw.js"),浏览器开始下载 sw.js 并启动生命周期。SW 一旦激活,控制 origin 内所有同域页面(按 scope 划分),后续访问自动经过 SW。
关键限制:SW 不能跨域;scope 默认是 sw.js 所在目录及子目录;浏览器有 30 秒空闲后冻结 SW 节省内存(事件触发即唤醒)。Chrome、Firefox、Safari、Edge 全部支持,2026 年覆盖率 99,无需 fallback 担心。
2. 生命周期:install / activate / fetch
Service Worker 有三个核心事件构成生命周期。install 在 SW 第一次注册或检测到 sw.js 内容变化时触发。开发者通常在这里 precache 必要资源(HTML shell、关键 CSS/JS、首屏图片):event.waitUntil(caches.open("v1").then(cache => cache.addAll([...]))。waitUntil 让浏览器等待 Promise 完成才标记 install 成功。
activate 在新 SW 接管页面前触发,是清理旧版本缓存的窗口。常见模式:列出所有 caches、删除非当前版本的:caches.keys().then(names => Promise.all(names.filter(n => n !== "v1").map(n => caches.delete(n))))。同时调 self.clients.claim() 让新 SW 立即接管已打开的页面(默认要等下次访问)。
fetch 在页面发起任何网络请求时触发。SW 可以选择:直接返回缓存、转发给网络再缓存、合成响应(offline fallback 页面)、放任不管让浏览器走默认路径。这是 SW 最常用的能力。
版本更新机制:用户访问时浏览器后台拉 sw.js 与上次比对,字节有差就重新走 install。但新 SW install 后处于 waiting 状态,等所有当前页面关闭后才 activate。要立即生效用 self.skipWaiting() 跳过 waiting,配合 clients.claim() 立即接管。生产更新策略:内容更新让 SW 后台 install,发消息给页面提示用户「新版本就绪,刷新生效」,由用户主动触发刷新避免数据丢失。
3. 缓存策略:四种主流模式
缓存策略决定每个请求 SW 怎么处理。四种主流模式按场景搭配。
cache-first(缓存优先):先查 Cache Storage,命中即返;未命中走网络并把响应缓存。适合不变或长 TTL 资源:带 hash 文件名的 JS/CSS(main.abc123.js)、字体、图标、Web Components 静态产物。优势:极快(无网络往返)、节省流量。劣势:内容更新需变 URL(hash)。
network-first(网络优先):先请求网络,成功即返并更新缓存;失败(超时或离线)回退缓存。适合时效性数据:HTML 页面、API 响应、用户动态内容。优势:内容总是最新、断网仍可用。劣势:每次都要等网络,体感比 cache-first 慢。
stale-while-revalidate(缓存即返,后台更新):立即返缓存(如有),同时后台请求网络更新缓存供下次使用。适合大部分静态资源:CSS、图片、非时效内容。优势:体感极快 + 内容最终一致。劣势:用户首次访问后第一次更新会看到旧内容(下次刷新才新)。
network-only / cache-only:极端模式。前者强制走网络(用于 POST、登录、支付等关键请求),后者强制走缓存(用于纯离线 fallback 页面)。
实战配方:HTML 用 network-first(含 3 秒超时回退缓存);带 hash 的 JS/CSS 用 cache-first;图片用 stale-while-revalidate;API GET 用 network-first 或 stale-while-revalidate(视时效要求);用户写操作(POST/PUT)必走 network-only。配合 Core Web Vitals 优化 进一步提升首屏。
4. Workbox:手写 SW 的终结者
手写 Service Worker 有大量样板代码:precache 清单生成、缓存版本号、过期清理、策略实现、调试日志。Workbox 是 Google 出品的 SW 工具集,把这些封装成声明式 API。
核心模块。workbox-precaching:自动生成 precache 清单(含文件 hash),install 时 addAll,activate 时清理旧版本。workbox-routing:registerRoute(matcher, strategy) 把 URL 模式与策略绑定。workbox-strategies:四种策略的开箱即用类(CacheFirst、NetworkFirst、StaleWhileRevalidate、NetworkOnly)。workbox-expiration:缓存条目数与年龄上限自动清理。workbox-broadcast-update:缓存更新时给页面发消息。workbox-background-sync:离线时 POST 请求暂存、上线后重试。
典型 sw.js:导入 Workbox precache 与 register,列出需要预缓存的 self.__WB_MANIFEST(构建时注入),再为图片、字体、API 分别 registerRoute。整份 sw.js 30 到 50 行覆盖完整 PWA。
构建集成:Vite 用 vite-plugin-pwa(封装 Workbox),astro.config 加配置即可;Next.js 用 next-pwa;Nuxt 有 @vite-pwa/nuxt;Astro 用 @vite-pwa/astro。配置中声明 workbox.runtimeCaching 数组,每项含 urlPattern + handler + options,等于声明式 SW。CI 构建时自动生成 precache 清单与 hash,无需手动维护。
5. Web App Manifest:让 PWA 可安装
Web App Manifest 是一份 manifest.webmanifest 或 manifest.json,描述应用元数据,让浏览器把网站作为「应用」处理:桌面图标、splash 屏、全屏运行、操作系统集成。
必填字段。name(完整应用名,安装时显示);short_name(桌面图标下,限 12 字符以避免截断);start_url(启动 URL,常带 ?source=pwa 区分流量来源);display(standalone 类原生、minimal-ui 简化浏览器、fullscreen 完全沉浸、browser 普通);icons(至少 192x192 与 512x512 PNG,建议加 maskable 适配 Android 自适应图标);theme_color(地址栏与系统栏颜色,配合 meta name=theme-color);background_color(splash 屏背景)。
HTML head 引用:link rel=manifest href=/manifest.webmanifest。Chrome / Edge 检测到合规 manifest + HTTPS + 注册了响应 fetch 的 SW,触发 beforeinstallprompt 让网站可安装。
增强字段。shortcuts 数组定义长按图标的快捷动作(4 个常用入口);screenshots 在 Chrome 安装弹窗中显示 app 预览图;categories 类别(productivity、social、entertainment);orientation 锁定方向(portrait / landscape);scope 限定 SW 控制范围;related_applications + prefer_related_applications 引导用户去原生 app 商店。
iOS Safari 不读 manifest 的图标与 splash,需要单独的 link rel=apple-touch-icon 与 meta name=apple-mobile-web-app-capable=yes 等 Apple 专属标签。Android Chrome 与桌面 Chrome 完整支持 manifest。
6. 推送通知:Web Push 与 Notifications API
推送是 PWA 的杀手级能力。即使浏览器关闭,用户依然能收到消息(手机锁屏推送、桌面 Toast)。技术栈两层:Web Push(订阅/推送传输)+ Notifications API(显示通知)。
订阅流程。前端调 navigator.serviceWorker.ready 拿 SW 注册,再 registration.pushManager.subscribe 传入 userVisibleOnly true 与 applicationServerKey 公钥两个选项,弹权限确认;用户同意后浏览器返回 PushSubscription 对象(含 endpoint、keys.p256dh、keys.auth),前端把它发给后端保存。
VAPID(Voluntary Application Server Identification)密钥对是后端身份认证。生成 VAPID 公私钥(web-push npm 库一行),公钥嵌前端、私钥后端保管。后端发推送时用 web-push 库携带 VAPID JWT 签名 POST 到 endpoint,浏览器供应商(Chrome FCM、Firefox Mozilla Push Service)转发到用户设备。
SW 收推送的 push 事件:event.waitUntil(self.registration.showNotification(title, options)),options 含 body、icon、badge、tag(合并)、actions(按钮)、data(自定义透传)。用户点通知触发 notificationclick 事件,常见处理 clients.openWindow(url) 打开页面或 clients.matchAll().then(c => c.focus()) 聚焦已打开的标签。
合规与体验注意。绝对不要在用户进站第一秒弹权限请求,转化率极低且伤体验。最佳实践:用户操作后(如点了关注、订阅按钮)才请求权限,并提前用自家 UI 解释推送会做什么。Apple Safari 16.4 起 iOS PWA 支持推送但需用户先「添加到主屏幕」。
7. 离线 UX:fallback 页面与数据策略
SW 让网页可离线,但「能离线」不等于「离线体验好」。离线 UX 三个层次。
第一层:导航 fallback。当 SW network-first 网络失败且缓存也无对应 HTML 时,返回一个 offline.html(precache 时一并存入),页面提示「当前离线,已浏览页面仍可访问」并列已缓存路由。在 fetch event 写:if (event.request.mode === "navigate") event.respondWith(fetch(event.request).catch(() => caches.match("/offline.html")))。
第二层:数据离线。读操作用 IndexedDB 持久化已加载数据,离线时从 IDB 直接读。写操作用 background sync:离线时把 POST 请求暂存到 IDB,浏览器恢复网络后 sync event 重试上传。Workbox 的 BackgroundSyncPlugin 一行启用。
第三层:UI 状态提示。前端监听 navigator.onLine 与 online/offline 事件,离线时全局显示横幅「当前离线,部分功能不可用」;写操作时弹 toast「已暂存,恢复网络后自动同步」。让用户对状态有明确预期。
实测案例:Twitter Lite 用 Service Worker + IndexedDB 实现地铁断网仍能浏览已加载推文与发推(暂存);上线后用户日均会话时长提升 65。这种「断网也能用」的体验是 PWA 最大的差异化。
8. 安装引导与调试技巧
beforeinstallprompt 事件让自定义安装入口成为可能。Chrome / Edge 检测到 manifest + SW 合规后触发该事件;调 event.preventDefault() 拦截浏览器默认弹窗保存事件,到自家「安装」按钮 onClick 时再调 event.prompt() 触发原生确认。然后 await event.userChoice 拿用户选择(accepted / dismissed)做埋点。
用户拒绝后当前会话不会再触发 beforeinstallprompt。再次提示策略:localStorage 存「上次拒绝时间」,过 7 到 14 天再次满足触发条件时(事件再来)才显示。Chrome 还有「30 天内多次拒绝不再触发」的内置限制。
iOS Safari 不支持 beforeinstallprompt。要引导用户用原生「分享 → 添加到主屏幕」,加文案与截图说明。检测 iOS:navigator.userAgent.match(/iPhone|iPad|iPod/) 且 navigator.standalone 为 false(未安装)。
调试三件套。Chrome DevTools Application 面板:Service Workers 看注册状态、跳过 waiting、unregister;Cache Storage 看每个 cache 的内容;Manifest 看解析结果与图标预览。Lighthouse 跑 PWA 类别检查可安装性、SW 注册、HTTPS、manifest 字段等。chrome://serviceworker-internals 看全部 SW 状态与日志。
常见坑。SW 文件加 Cache-Control: no-cache 或 max-age=0,否则浏览器缓存 sw.js 几小时不更新。生产构建用 hash 文件名让产物无限期缓存,但 sw.js 本身必须每次校验。开发时 DevTools 勾「Update on reload」每次刷新都重新注册,方便调试。
这套体系建好后,普通网站秒变 PWA:可装、可离线、可推送、可后台同步,体验直追原生 app。详见 JS bundle 体积优化 把 SW 与 precache 流量也压到极致。
常见问题
Service Worker 与 Web Worker 有什么区别?
Web Worker 是给单个页面跑后台 JS 计算的线程,页面关闭即销毁,主要用于 CPU 密集任务(图片处理、加密、Wasm 计算)卸载主线程。Service Worker 是浏览器为整个 origin 注册的代理(proxy),生命周期独立于页面,能在页面关闭后继续运行(推送通知、后台同步),核心能力是拦截网络请求并自定义响应(缓存策略),让网站可离线运行、可安装、可推送。SW 必须 HTTPS(localhost 例外),WW 无此限制。
cache-first 与 network-first 怎么选?
cache-first(缓存优先):先查缓存,有就返,没有再请求网络并缓存。适合不变或低频更新资源:字体、图标、CSS/JS 长期 hash 文件名的产物。network-first(网络优先):先请求网络,超时或失败再回退缓存。适合时效性资源:HTML 页面、API 数据、用户内容。stale-while-revalidate(先返缓存再后台更新):立即返缓存给用户,后台拉新版本下次访问用。是体验最佳的折中,适合大部分静态资源。Workbox 把这三种策略封装成一行配置。
Workbox 还有必要用吗?
强烈建议。手写 Service Worker 容易出错——precache manifest 维护、缓存版本号、清理旧缓存、缓存策略实现都繁琐且 bug 多。Workbox 是 Google 出品的 SW 工具集,把缓存策略封装成 registerRoute(matcher, strategy) 一行;自动生成 precache 清单与 hash;提供路由匹配、过期管理、后台同步、广播更新等模块。Vite 项目用 vite-plugin-pwa(基于 Workbox)、Webpack 用 workbox-webpack-plugin、Next.js 用 next-pwa。一行配置等于手写 200 行 SW。
Web App Manifest 必填字段有哪些?
让 PWA 可安装的最小必填集合:name(应用全名)、short_name(图标下显示,最多 12 字符)、start_url(启动 URL,通常 / 或 /?source=pwa)、display(standalone 全屏类应用 / minimal-ui 最简浏览器壳 / fullscreen 完全沉浸 / browser 普通标签)、icons 数组(至少含一个 192x192 与 512x512 PNG,maskable 图标更佳)、theme_color(地址栏与系统栏颜色)、background_color(splash 屏背景)。description、scope、orientation、shortcuts、screenshots 是增强项。Chrome 还要求页面注册了响应 fetch 的 SW 才允许安装。
beforeinstallprompt 用户可以拒绝吗?怎么再次提示?
beforeinstallprompt 事件触发时调用 event.preventDefault() 拦截浏览器默认弹窗、保存事件对象,自定义安装入口让用户点击后再调 event.prompt() 触发原生确认。用户拒绝后 prompt 当前会话不会再次触发。再次提示策略:保存「用户拒绝时间戳」到 localStorage,过 7 天再次满足条件时显示自家「轻提示」。Chrome 还限制 30 天内多次拒绝不再发 beforeinstallprompt。Safari/iOS 不支持 beforeinstallprompt,需要在文档里教用户用「分享 → 添加到主屏幕」。