Vite PWA 与离线缓存实战:vite-plugin-pwa、Workbox 与更新策略

从零搭建 Vite PWA 的完整路径:manifest 与 Service Worker 的构成、vite-plugin-pwa 的最小配置、Workbox 预缓存与运行时缓存策略、prompt 与 autoUpdate 更新模式、离线回退与导航预加载、构建产物 hash 配合、DevTools 调试验证,以及白屏与缓存不更新等高频陷阱的排查。

引言

PWA 的价值在于「装得上、离线开、更新可控」:用户能把站点装到桌面或主屏,断网时仍能打开核心页面,发布新版本后又能平滑地提示更新。在 Vite 项目里落地 PWA,最主流的路径是 vite-plugin-pwa——它把 Workbox 的预缓存与运行时缓存封装成一套与 Vite 构建产物天然对齐的配置。

本文从 PWA 的三块基石讲起,逐步搭建 vite-plugin-pwa 的最小配置,深入 Workbox 的预缓存与运行时缓存策略,讲清 prompt 与 autoUpdate 两种更新模式的区别与取舍,最后给出调试方法、生产落地策略与高频陷阱的排查清单。

前置:/vite-build-optimization/(构建产物与 hash)、/vite-config-guide/(插件与 base 配置)。运行期性能视角见 /vite-runtime-performance-optimization/。


目录


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 注册率与更新转化率,灰度部署并保留旧版本可回滚。


延伸阅读

  • /vite-build-optimization/ — 构建产物与内容 hash
  • /vite-config-guide/ — 插件配置与 base 路径
  • /vite-runtime-performance-optimization/ — 运行时性能与加载策略
  • /vite-env-production-best-practices/ — 生产环境最佳实践
  • /vite-ci-cd-optimization/ — CI 流水线与部署优化
  • 前端工程化专题 — 前端性能与工程化全景

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「vite」更多文章

  1. Vite 项目的 Web Vitals 与性能监控:指标采集、构建期埋点与 RUM 上报
  2. 从 Webpack 迁移到 Vite:配置映射、loader 与插件对应与常见坑
  3. Vite 国际化与多语言构建:按语言分包、懒加载与回退策略