目录
概述
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()
}
]
}
重要说明
- 使用
url而不是urlPattern
// ✅ 正确
preCaching: [{ url: '/', ... }]
// ❌ 错误
preCaching: [{ urlPattern: '/', ... }]
revision用于版本控制
// 每次构建生成新版本号
revision: process.env.BUILD_ID || Date.now()
// 或者固定版本号
revision: 'v1.0.0'
/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:浏览器开发者工具
- 打开 DevTools → Application → Service Workers
- 点击 “Update on reload”(重新加载时更新)
- 刷新页面
方法 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: 离线页面不显示
原因:
offlinePage配置错误- 页面路径不存在
解决:
workbox: {
offlinePage: '/offline' // 确保页面存在
}
确保 /offline 路由存在:
pages/
└── offline.vue ✅
Q5: HTTPS 要求
问题:Service Worker 只能在 HTTPS 环境下运行
例外:
localhost可以使用 HTTP127.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
}
}
}