引言
PWA 的价值在于「装得上、离线开、更新可控」:用户能把站点装到桌面或主屏,断网时仍能打开核心页面,发布新版本后又能平滑地提示更新。在 Vite 项目里落地 PWA,最主流的路径是 vite-plugin-pwa——它把 Workbox 的预缓存与运行时缓存封装成一套与 Vite 构建产物天然对齐的配置。
本文从 PWA 的三块基石讲起,逐步搭建 vite-plugin-pwa 的最小配置,深入 Workbox 的预缓存与运行时缓存策略,讲清 prompt 与 autoUpdate 两种更新模式的区别与取舍,最后给出调试方法、生产落地策略与高频陷阱的排查清单。
前置:构建产物与 hash、插件与 base 配置。运行期性能视角见 Vite 客户端运行时性能:加载策略、缓存与核心 Web 指标。
目录
- 1. PWA 的构成:Manifest、Service Worker 与缓存
- 2. vite-plugin-pwa 上手:安装与最小配置
- 3. Workbox 预缓存:globPatterns 与版本控制
- 4. 运行时缓存:路由与策略选择
- 5. 更新策略:prompt 与 autoUpdate
- 6. 离线回退与导航预加载
- 7. 与 Vite 构建产物的配合:hash 与 manifest
- 8. 调试与验证:DevTools 与 Lighthouse
- 9. 常见陷阱:白屏、缓存不更新与 scope
- 10. 生产落地与灰度更新
- 延伸阅读
1. PWA 的构成:Manifest、Service Worker 与缓存
1.1 三块基石
一个可安装、可离线的 PWA 由三部分组成,缺一不可:
| 组成 | 作用 | Vite 中的载体 |
|---|---|---|
| Web App Manifest | 声明名称、图标、启动方式 | manifest.webmanifest |
| Service Worker | 拦截请求、管理缓存 | sw.js |
| 缓存策略 | 决定哪些资源离线可用 | Workbox 配置 |
1.2 为什么 Vite 项目特别适合 PWA
Vite 的生产产物文件名自带内容 hash,这恰好是 Service Worker 预缓存最需要的特性:文件内容变了,文件名就变,缓存清单能精确识别哪些资源需要更新。
Vite 产物:index-a1b2c3.js、vendor-d4e5f6.js
→ 内容变则文件名变(精确 diff),内容不变则缓存命中不破
记忆:PWA 三基石是 Manifest、Service Worker 与缓存策略——Vite 的内容 hash 产物让「哪些资源要更新」变成一次精确的文件名 diff。
2. vite-plugin-pwa 上手:安装与最小配置
2.1 安装
用 npm i -D vite-plugin-pwa workbox-window 安装:workbox-window 用于在页面侧注册与监听 Service Worker 生命周期,vite-plugin-pwa 负责构建期生成 sw.js 与 manifest。
2.2 最小可用配置
// vite.config.ts
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
registerType: 'prompt',
manifest: {
name: 'My App',
short_name: 'App',
theme_color: '#ffffff',
icons: [
{ src: 'pwa-192.png', sizes: '192x192', type: 'image/png' },
{ src: 'pwa-512.png', sizes: '512x512', type: 'image/png' },
],
},
}),
],
})
2.3 页面侧注册
// main.ts
import { registerSW } from 'virtual:pwa-register'
const updateSW = registerSW({
onNeedRefresh() {
if (confirm('检测到新版本,是否刷新?')) updateSW(true)
},
onOfflineReady() {
console.log('离线可用已就绪')
},
})
virtual:pwa-register 是插件提供的虚拟模块,需要确保 vite-plugin-pwa/client 类型已加入 tsconfig 的 types。
记忆:最小配置 =
VitePWA({ registerType, manifest })加页面侧registerSW——插件在构建期生成 sw.js 与 manifest.webmanifest,运行时由 workbox-window 注册。
3. Workbox 预缓存:globPatterns 与版本控制
3.1 预缓存是什么
预缓存(precache)在 Service Worker 安装阶段一次性下载并缓存指定资源,之后离线即可直接命中。哪些资源进预缓存由 globPatterns 决定:
VitePWA({
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
globIgnores: ['**/large-*.js'], // 排除大文件
maximumFileSizeToCacheInBytes: 3 * 1024 * 1024,
},
})
3.2 版本控制
Workbox 用「revision」标识每个预缓存条目。Vite 产物带 hash,因此文件名变化本身就会触发新 revision;而 HTML 等不带 hash 的资源则按内容计算 revision。
index-a1b2c3.js → revision 由文件名承担
index.html → revision 由内容 hash 计算
3.3 generateSW 与 injectManifest
generateSW 由插件自动生成 sw.js,配置即可满足;injectManifest 则由你写 sw.ts、插件只负责注入预缓存清单,适合需要自定义逻辑的场景。
VitePWA({ strategies: 'injectManifest', srcDir: 'src', filename: 'sw.ts' })
记忆:预缓存由
globPatterns决定进哪些资源、Vite 的 hash 文件名天然充当 revision——要自定义 Service Worker 逻辑就切到 injectManifest。
4. 运行时缓存:路由与策略选择
4.1 为什么需要运行时缓存
预缓存只覆盖「构建时已知」的静态资源。接口请求、图片 CDN、第三方字体这些「运行时才知道」的资源,需要 runtimeCaching 定义策略。
4.2 常见策略对照
| 策略 | 行为 | 适合 |
|---|---|---|
| CacheFirst | 先查缓存,没有再请求 | 静态资源、字体 |
| NetworkFirst | 先请求网络,失败回缓存 | 接口数据 |
| StaleWhileRevalidate | 用缓存立即响应并后台更新 | 头像、非关键数据 |
| NetworkOnly | 只走网络 | 支付等强一致请求 |
4.3 配置示例
VitePWA({
workbox: {
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\//,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
networkTimeoutSeconds: 3,
expiration: { maxEntries: 50, maxAgeSeconds: 300 },
},
},
{
urlPattern: /\.(?:png|jpg|webp)$/,
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
expiration: { maxEntries: 100, maxAgeSeconds: 30 * 24 * 3600 },
},
},
],
},
})
expiration 是必须重视的配置——不设上限的缓存会无限增长,最终拖垮用户磁盘配额。
记忆:运行时缓存按资源性质选策略——静态用 CacheFirst、接口用 NetworkFirst、非关键数据用 StaleWhileRevalidate,并且一定要配 expiration 上限。
5. 更新策略:prompt 与 autoUpdate
5.1 两种模式的区别
| 模式 | 行为 | 用户体验 |
|---|---|---|
| prompt | 新版本就绪时触发回调,由用户决定刷新 | 可控,但需 UI 提示 |
| autoUpdate | 新 SW 激活后自动接管,下次导航生效 | 无感,但用户可能看到版本错位 |
VitePWA({
registerType: 'autoUpdate',
workbox: { cleanupOutdatedCaches: true, clientsClaim: true },
})
5.2 prompt 模式的完整流程
1. 用户访问 → 旧 SW 控制页面
2. 新版本部署 → 浏览器检测到 sw.js 变化 → 下载新 SW
3. 新 SW 进入 waiting 状态 → 触发 onNeedRefresh
4. 用户确认 → updateSW(true) → skipWaiting + reload
5. 页面由新 SW 接管
5.3 选择建议
后台管理系统 / 强交互应用 → prompt(避免用户丢失未保存状态)
内容站 / 工具类应用 → autoUpdate(追求无感更新)
记忆:prompt 可控但要 UI、autoUpdate 无感但可能版本错位——有未保存状态的交互型应用一律选 prompt。
6. 离线回退与导航预加载
6.1 导航回退
用户直接访问一个未缓存的深层路由时,Service Worker 需要一个离线回退页:
VitePWA({
workbox: {
navigateFallback: '/offline.html',
navigateFallbackDenylist: [/^\/api\//],
},
})
navigateFallbackDenylist 用来排除 API 与后台路径,避免接口请求被回退到 HTML。
6.2 导航预加载
导航预加载(navigation preload)在 SW 启动的同时并行发起网络请求,缓解 SW 冷启动延迟:
VitePWA({
workbox: {
navigationPreload: true,
},
})
6.3 离线页设计要点
- offline.html 必须进 globPatterns,否则离线时自己也拿不到
- 离线页展示「已缓存内容入口」而非纯报错,且不依赖未缓存的 JS
记忆:离线回退靠
navigateFallback,但要记得把回退页本身加进预缓存、并用 denylist 排除 API;导航预加载用来削 SW 冷启动延迟。
7. 与 Vite 构建产物的配合:hash 与 manifest
7.1 base 路径
部署在子路径时必须同时设置 Vite 的 base 与 PWA 的 scope,否则 Service Worker 的作用域会错位:
export default defineConfig({
base: '/app/',
plugins: [VitePWA({ scope: '/app/', base: '/app/' })],
})
7.2 manifest 的产出
插件会在构建期把 manifest 写入 dist/manifest.webmanifest,并在 HTML 里注入 <link rel="manifest">。可以用 includeAssets 把额外文件带进产物:
VitePWA({
includeAssets: ['favicon.ico', 'robots.txt', 'apple-touch-icon.png'],
})
7.3 构建产物核对
ls dist/sw.js dist/manifest.webmanifest # 确认已生成
grep -o '"[^"]*\.js"' dist/sw.js | head -20 # 看预缓存清单
记忆:子路径部署必须同时对齐
base与scope,否则 SW 作用域错位——构建后先核对 dist 里的 sw.js 与 manifest 是否按预期生成。
8. 调试与验证:DevTools 与 Lighthouse
8.1 Application 面板
Chrome DevTools 的 Application 面板是 PWA 调试主战场:
Manifest → 检查名称、图标、安装性
Service Workers → 查看 SW 状态、强制更新、离线勾选
Cache Storage → 逐条查看缓存了哪些请求与响应
8.2 本地验证离线
1. Application → Service Workers → 勾选 Offline
2. 刷新页面确认核心路由仍可访问;取消 Offline 再验证缓存更新
8.3 Lighthouse 审计
npx lighthouse http://localhost:4173 --view --only-categories=pwa
注意要跑构建后的预览服务(vite preview),开发服务器下 SW 行为与生产不一致。
记忆:调试 PWA 看 Application 面板三处——Manifest 查安装性、Service Workers 查状态、Cache Storage 查缓存内容;Lighthouse 必须跑
vite preview的生产产物。
9. 常见陷阱:白屏、缓存不更新与 scope
9.1 高频陷阱表
| 现象 | 原因 | 处理 |
|---|---|---|
| 更新后白屏 | 旧 HTML 引用已删除的 hash 文件 | 预缓存 HTML、cleanupOutdatedCaches |
| 缓存永不更新 | sw.js 被浏览器 HTTP 缓存 | 给 sw.js 设 no-cache |
| 深层路由 404 | 未配 navigateFallback | 加离线回退页 |
| SW 注册失败 | scope 与 base 不匹配 | 对齐 base 与 scope |
| 接口被缓存 | runtimeCaching 命中过宽 | 收窄 urlPattern |
| 磁盘占用暴涨 | 未配 expiration | 加 maxEntries 与 maxAgeSeconds |
9.2 白屏的根因
白屏最常见的原因是**「缓存里的旧 HTML + 服务器上已删除的旧 hash 资源」**。解决办法是让 HTML 走 NetworkFirst 或干脆不预缓存 HTML,并开启 cleanupOutdatedCaches。
9.3 sw.js 的缓存头
Service Worker 脚本本身必须禁止强缓存,否则浏览器永远拿不到新版本:
# Nginx 示例
location = /sw.js {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
记忆:PWA 翻车九成是「缓存版本错位」——HTML 别硬缓存、sw.js 必须 no-cache、开启 cleanupOutdatedCaches,白屏与不更新基本自解。
10. 生产落地与灰度更新
10.1 上线检查清单
□ sw.js 响应头为 no-cache
□ manifest 图标齐全(192/512/maskable)
□ 离线回退页已进预缓存
□ runtimeCaching 已配 expiration 上限
□ 已用 vite preview 跑过 Lighthouse PWA 审计
□ 已确认 base 与 scope 一致
10.2 灰度更新策略
- 新版本先部署到小流量环境,观察 SW 更新成功率
- 用 prompt 模式时,把「立即刷新」做成不打断操作的浮层
- 记录 onNeedRefresh 触发次数,判断用户是否卡在旧版本
10.3 监控指标
重点盯四个指标:SW 注册成功率(环境与 scope 是否正确)、预缓存命中率(离线可用性)、更新提示转化率(用户是否愿意刷新)、缓存占用(是否需调整 expiration)。
记忆:PWA 生产落地的关键是「可观测 + 可回退」——盯 SW 注册率与更新转化率,灰度部署并保留旧版本可回滚。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。