PWA Service Worker 技术实现文档

目录


概述

PWA(Progressive Web App)通过 Service Worker 提供离线访问能力,使 Web 应用能够:

  • 在离线状态下访问已缓存的内容
  • 缓存静态资源提高加载速度
  • 支持添加到主屏幕
  • 提升用户体验和应用稳定性

工作原理

用户请求 → Service Worker
           ↓
    缓存命中?→ 是 → 返回缓存内容
           ↓ 否
    发起网络请求
           ↓
    响应成功 → 缓存并返回
           ↓ 失败
    返回离线页面/错误信息

技术栈

技术 版本 用途
Nuxt.js 2.x Vue.js SSR 框架
@nuxtjs/pwa 3.x PWA 集成模块
Workbox 5.x Service Worker 生成工具

安装依赖

# NPM npm install @nuxtjs/pwa # Yarn yarn add @nuxtjs/pwa

核心文件说明

1. package.json

{ "name": "my-pwa-app", "version": "1.0.0", "description": "示例 PWA 应用", "dependencies": { "@nuxtjs/pwa": "^3.3.3", "nuxt": "^2.0.0" } }

注意name 字段必须符合 npm 命名规范:

  • 只能包含小写字母、数字、连字符、下划线、点
  • 必须以字母开头
  • 不能包含中文字符

2. nuxt.config.js

module.exports = { // 启用 PWA 模块 modules: [ '@nuxtjs/pwa' ], // PWA 配置 pwa: { // 应用基本信息 name: '我的应用', shortName: '我的应用', description: '应用描述', themeColor: '#35a373', backgroundColor: '#ffffff', display: 'standalone', // 图标配置 icon: { source: '/favicon.ico', sizes: [16, 32, 48, 64, 96, 128, 256], purpose: 'any maskable' }, // Workbox 配置 workbox: { // 运行时缓存策略 runtimeCaching: [...], // 预缓存配置 preCaching: [...], // 离线页面 offlinePage: '/offline' } } }

3. plugins/sw.js(可选)

@nuxtjs/pwa 会自动生成 Service Worker 注册代码。如需自定义注册逻辑:

// plugins/sw.js export default function({ app }, inject) { // 只在客户端运行 if (process.client && 'serviceWorker' in navigator) { navigator.serviceWorker .register('/sw.js', { scope: '/', updateViaCache: 'all' }) .then(registration => { console.log('[SW] Service Worker registered:', registration.scope) }) .catch(error => { console.error('[SW] Registration failed:', error) }) } }

4. pages/offline.vue

<template>
  <div class="offline-page">
    <div class="content">
      <h1>离线模式</h1>
      <p>当前网络不可用</p>
      <button @click="reload">重新连接</button>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    reload() {
      location.reload()
    }
  }
}
</script>

<style scoped>
.offline-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
}
</style>

配置详解

应用元数据

pwa: { // 完整名称(浏览器标签页显示) name: '我的完整应用名称', // 简短名称(主屏幕图标下方显示) shortName: '我的应用', // 应用描述(用于 SEO 和安装提示) description: '应用功能描述', // 主题色(状态栏、地址栏颜色) themeColor: '#35a373', // 背景色(启动画面背景) backgroundColor: '#ffffff', // 显示模式 // standalone: 独立应用(隐藏浏览器 UI) // fullscreen: 全屏 // minimal-ui: 最小 UI // browser: 浏览器模式 display: 'standalone' }

图标配置

icon: { // 图标源文件路径(支持 PNG、SVG) source: '/favicon.ico', // 需要生成的尺寸(像素) sizes: [16, 32, 48, 64, 96, 128, 192, 256, 384, 512], // 图标用途 // any: 任何场景 // maskable: 可遮罩图标(适应设备形状) // monochrome: 单色图标 purpose: 'any maskable' }

manifest.json 自动生成

@nuxtjs/pwa 会自动生成 /_nuxt/manifest.{hash}.json

{ "name": "我的应用", "short_name": "我的应用", "start_url": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#35a373", "icons": [...] }

缓存策略

1. NetworkFirst(网络优先)

适用场景:API 请求、动态数据

工作流程

1. 尝试网络请求
2. 成功 → 缓存响应并返回
3. 失败/超时 → 返回缓存内容

配置示例

{ urlPattern: '/api/.*', handler: 'NetworkFirst', options: { cacheName: 'api-cache', expiration: { maxEntries: 100, // 最多缓存 100 个响应 maxAgeSeconds: 86400 // 缓存有效期 24 小时 }, networkTimeoutSeconds: 10 // 网络超时时间(秒) } }

参数说明

参数 说明
urlPattern 匹配 URL 的正则表达式
cacheName 缓存存储名称
maxEntries 最大缓存条目数
maxAgeSeconds 缓存有效期(秒)
networkTimeoutSeconds 网络超时时间(秒)

2. CacheFirst(缓存优先)

适用场景:静态资源(图片、字体、CSS、JS)

工作流程

1. 检查缓存
2. 命中 → 直接返回
3. 未命中 → 请求网络 → 缓存并返回

配置示例

{ urlPattern: 'https://cdn\\.example\\.com/.*', handler: 'CacheFirst', options: { cacheName: 'static-cache', expiration: { maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 30 // 30 天 } } }

3. StaleWhileRevalidate(缓存更新)

适用场景:需要即时显示但允许后台更新的资源

工作流程

1. 立即返回缓存内容
2. 后台发起网络请求
3. 响应返回后更新缓存

配置示例

{ urlPattern: '/images/.*', handler: 'StaleWhileRevalidate', options: { cacheName: 'images-cache' } }

4. NetworkOnly(仅网络)

适用场景:永远不缓存的请求(如登录、支付)

配置示例

{ urlPattern: '/api/(login|logout|payment)/.*', handler: 'NetworkOnly' }

5. CacheOnly(仅缓存)

适用场景:离线回退场景

配置示例

{ urlPattern: '/fallback.*', handler: 'CacheOnly' }

缓存策略对比

策略 优先级 适用场景 网络失败
NetworkFirst 网络 API、动态数据 返回缓存
CacheFirst 缓存 静态资源 网络请求
StaleWhileRevalidate 缓存 需要快速更新的内容 返回缓存
NetworkOnly 网络 登录、支付 失败
CacheOnly 缓存 离线回退 返回缓存

预缓存配置

基本用法

workbox: { preCaching: [ { url: '/', revision: process.env.BUILD_ID || Date.now() }, { url: '/offline', revision: process.env.BUILD_ID || Date.now() } ] }

重要说明

  1. 使用 url 而不是 urlPattern
// ✅ 正确 preCaching: [{ url: '/', ... }] // ❌ 错误 preCaching: [{ urlPattern: '/', ... }]
  1. revision 用于版本控制
// 每次构建生成新版本号 revision: process.env.BUILD_ID || Date.now() // 或者固定版本号 revision: 'v1.0.0'
  1. /offline 页面自动添加

@nuxtjs/pwa 会自动将 offlinePage 配置的页面添加到预缓存列表,无需手动添加

// ❌ 错误 - 重复配置 workbox: { offlinePage: '/offline', preCaching: [ { url: '/', ... }, { url: '/offline', ... } // 删除 ] } // ✅ 正确 workbox: { offlinePage: '/offline', preCaching: [ { url: '/', ... } ] }

版本控制

自动更新机制

每次构建时,revision 变化会触发 Service Worker 更新:

// 配置 preCaching: [ { url: '/', revision: process.env.BUILD_ID || Date.now() } ]

更新流程

1. 修改代码 → 新的 revision
2. 重新构建 → 新的 Service Worker
3. 用户访问 → 浏览器检测到新版本
4. Service Worker 更新 → 新版本激活
5. 清理旧缓存 → 新缓存生效

手动更新

方法 1:浏览器开发者工具

  1. 打开 DevTools → Application → Service Workers
  2. 点击 “Update on reload”(重新加载时更新)
  3. 刷新页面

方法 2:强制更新

// 在 Service Worker 中 self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { return caches.delete(cacheName) }) ) }) ) })

跳过等待

默认情况下,新 Service Worker 需要所有页面关闭后才会激活。可以启用 skipWaiting 立即激活:

workbox: { skipWaiting: true, clientsClaim: true }

部署流程

1. 开发环境

# 启动开发服务器 yarn dev # PWA 默认在生产环境启用 # 如需在开发环境测试,去掉 disable 配置

2. 生产构建

# 构建 yarn build # 启动 yarn start

构建后会生成:

.nuxt/dist/
├── sw.js                    # Service Worker 文件
└── manifest.{hash}.json      # PWA Manifest

3. Docker 部署

FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build EXPOSE 3000 CMD ["npm", "start"]
# docker-compose.yml services: app: build: . ports: - "3000:3000" environment: - NODE_ENV=production

4. Nginx 配置

# Service Worker 需要 HTTPS server { listen 443 ssl http2; server_name example.com; # SSL 证书配置 ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; # Service Worker 文件缓存 location /sw.js { add_header Cache-Control "no-cache"; } # 其他资源正常代理 location / { proxy_pass http://localhost:3000; } }

测试验证

1. 检查 Service Worker 注册

1. 打开浏览器开发者工具(F12)
2. 进入 Application → Service Workers
3. 检查是否已注册 /sw.js
4. 状态应为 "activated"(已激活)

2. 检查缓存内容

1. 进入 Application → Cache Storage
2. 查看缓存名称和内容
3. 验证预期资源是否被缓存

3. 测试离线功能

1. 浏览一些页面(触发缓存)
2. 进入 Network 面板
3. 勾选 "Offline"(离线模式)
4. 刷新页面
5. 验证:
   - 已缓存页面正常显示
   - 未缓存页面显示离线提示
6. 取消勾选 "Offline" 恢复网络

4. 测试添加到主屏幕

1. 使用移动浏览器访问
2. 点击浏览器菜单
3. 选择 "添加到主屏幕" 或 "安装应用"
4. 确认安装后,应用以独立窗口运行

常见问题

Q1: Service Worker 注册失败

错误add-to-cache-list-unexpected-type

原因:preCaching 使用了 urlPattern 而不是 url

解决

// 错误 preCaching: [{ urlPattern: '/', ... }] // 正确 preCaching: [{ url: '/', ... }]

Q2: 缓存条目冲突

错误add-to-cache-list-conflicting-entries

原因:同一 URL 在 preCaching 中重复配置

解决

// 错误 - /offline 重复 preCaching: [ { url: '/' }, { url: '/offline' } // 删除,自动添加 ] // 正确 preCaching: [ { url: '/' } ]

Q3: Service Worker 不更新

原因:旧 Service Worker 仍在等待激活

解决

workbox: { skipWaiting: true, clientsClaim: true }

或在浏览器中手动更新:

Application → Service Workers → Update on reload

Q4: 离线页面不显示

原因

  1. offlinePage 配置错误
  2. 页面路径不存在

解决

workbox: { offlinePage: '/offline' // 确保页面存在 }

确保 /offline 路由存在:

pages/
└── offline.vue  ✅

Q5: HTTPS 要求

问题:Service Worker 只能在 HTTPS 环境下运行

例外

  • localhost 可以使用 HTTP
  • 127.0.0.1 可以使用 HTTP

解决:生产环境必须配置 HTTPS


Q6: 缓存占用过多空间

配置缓存清理

{ urlPattern: '/api/.*', handler: 'NetworkFirst', options: { cacheName: 'api-cache', expiration: { maxEntries: 100, // 限制条目数 maxAgeSeconds: 86400 // 限制时间 } } }

Q7: 跨域资源缓存失败

原因:跨域资源的 CORS 配置问题

解决

// 确保 CORS 头正确 headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS' }

Q8: package.json 名称错误

错误Name contains illegal characters

原因:name 字段包含中文字符或不符合规范

解决

// 错误 { "name": "我的应用" } // 正确 { "name": "my-app" }

命名规范

  • 只能包含:小写字母、数字、连字符、下划线、点
  • 必须以字母开头

性能优化

1. 精简预缓存列表

// 只预缓存关键页面 preCaching: [ { url: '/' }, { url: '/offline' } ] // 不需要预缓存: // - 深层页面(用户可能不访问) // - 大文件(影响首次加载) // - 频繁更新的内容

2. 合理设置缓存有效期

// API 数据:短期缓存 { maxAgeSeconds: 60 * 60 * 24 // 24 小时 } // 静态资源:长期缓存 { maxAgeSeconds: 60 * 60 * 24 * 30 // 30 天 } // 永不失效:不设置 maxAgeSeconds { // 无过期时间 }

3. 按资源类型分组缓存

runtimeCaching: [ { urlPattern: '/api/.*', cacheName: 'api-cache', // API 独立缓存 ... }, { urlPattern: '/images/.*', cacheName: 'images-cache', // 图片独立缓存 ... }, { urlPattern: '\\.js$', cacheName: 'js-cache', // JS 独立缓存 ... } ]

4. 启用压缩

location / { gzip on; gzip_types text/plain text/css application/json application/javascript; proxy_pass http://localhost:3000; }

安全建议

1. 敏感数据不缓存

// 不缓存登录、支付等敏感接口 { urlPattern: '/api/(login|logout|payment)/.*', handler: 'NetworkOnly' // 不缓存 }

2. 私有数据清理

// 用户登出时清理缓存 self.addEventListener('message', event => { if (event.data === 'CLEAR_CACHE') { caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => caches.delete(cacheName)) ) }) } })

3. 验证来源

// 验证请求来源 self.addEventListener('fetch', event => { const url = new URL(event.request.url) // 只处理同源请求 if (url.origin !== location.origin) { event.respondWith(fetch(event.request)) return } // 正常缓存逻辑... })

参考资料


附录:完整配置示例

// nuxt.config.js module.exports = { modules: [ '@nuxtjs/pwa' ], pwa: { name: '示例应用', shortName: '示例', description: '这是一个示例 PWA 应用', themeColor: '#35a373', backgroundColor: '#ffffff', display: 'standalone', lang: 'zh-CN', icon: { source: '/icon.png', sizes: [72, 96, 128, 144, 152, 192, 384, 512], purpose: 'any maskable' }, workbox: { // 运行时缓存 runtimeCaching: [ { urlPattern: '/api/.*', handler: 'NetworkFirst', options: { cacheName: 'api-cache', expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 }, networkTimeoutSeconds: 10 } }, { urlPattern: 'https://cdn\\.example\\.com/.*', handler: 'CacheFirst', options: { cacheName: 'cdn-cache', expiration: { maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 30 } } }, { urlPattern: '\\.js$|\\.css$', handler: 'CacheFirst', options: { cacheName: 'static-cache' } } ], // 预缓存 preCaching: [ { url: '/', revision: process.env.BUILD_ID || Date.now() } ], // 离线页面 offlinePage: '/offline', // 更新策略 skipWaiting: true, clientsClaim: true, cleanupOutdatedCaches: true } } }