Vue PWA开发踩坑
21.1 Vue PWA配置的常见错误
核心知识点
在配置Vue PWA时,常见的错误包括:
- manifest.json配置错误:Web App Manifest配置不当
- service-worker.js配置错误:Service Worker配置不当
- PWA插件配置错误:Vue PWA插件配置错误
- HTTPS配置:缺少HTTPS配置
实用案例分析
错误场景:manifest.json配置错误
// 错误示例:manifest.json配置不当
{
"name": "Vue PWA App",
"short_name": "PWA App",
"description": "A Vue PWA application",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4CAF50",
"icons": [
{
"src": "img/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
// 错误:缺少必要的图标尺寸和方向配置
}正确实现:
// 正确示例:manifest.json配置
{
"name": "Vue PWA App",
"short_name": "PWA App",
"description": "A Vue PWA application",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#ffffff",
"theme_color": "#4CAF50",
"icons": [
{
"src": "img/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "img/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "img/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "img/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "img/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "img/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "img/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "img/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"shortcuts": [
{
"name": "首页",
"short_name": "首页",
"description": "跳转到首页",
"url": "/",
"icons": [{ "src": "img/icons/icon-192x192.png", "sizes": "192x192" }]
},
{
"name": "关于",
"short_name": "关于",
"description": "跳转到关于页面",
"url": "/about",
"icons": [{ "src": "img/icons/icon-192x192.png", "sizes": "192x192" }]
}
]
}错误场景:PWA插件配置错误
// 错误示例:Vue PWA插件配置错误
// vue.config.js
module.exports = {
pwa: {
// 错误:配置不完整
name: 'Vue PWA App',
themeColor: '#4CAF50'
}
}正确实现:
// 正确示例:Vue PWA插件配置
// vue.config.js
module.exports = {
pwa: {
name: 'Vue PWA App',
shortName: 'PWA App',
description: 'A Vue PWA application',
themeColor: '#4CAF50',
msTileColor: '#4CAF50',
appleMobileWebAppCapable: 'yes',
appleMobileWebAppStatusBarStyle: 'black-translucent',
// 配置manifest.json
manifestOptions: {
name: 'Vue PWA App',
short_name: 'PWA App',
description: 'A Vue PWA application',
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#4CAF50',
icons: [
{
src: './img/icons/icon-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: './img/icons/icon-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
},
// 配置Service Worker
workboxOptions: {
cleanupOutdatedCaches: true,
skipWaiting: true,
clientsClaim: true,
runtimeCaching: [
{
urlPattern: new RegExp('^https://api\.example\.com/'),
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
networkTimeoutSeconds: 10,
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 7 // 7天
},
cacheableResponse: {
statuses: [0, 200]
}
}
}
]
}
}
}21.2 Vue PWA缓存策略的陷阱
核心知识点
在配置Vue PWA缓存策略时,常见的陷阱包括:
- 缓存策略选择:选择不适合的缓存策略
- 缓存大小限制:未设置合理的缓存大小限制
- 缓存过期:缓存过期策略不当
- 缓存清理:未正确清理过期缓存
实用案例分析
错误场景:缓存策略选择错误
// 错误示例:缓存策略选择不当
// vue.config.js
module.exports = {
pwa: {
workboxOptions: {
// 错误:对所有资源使用相同的缓存策略
runtimeCaching: [
{
urlPattern: new RegExp('^https://'),
handler: 'CacheFirst', // 错误:对API请求使用CacheFirst
options: {
cacheName: 'all-cache'
}
}
]
}
}
}正确实现:
// 正确示例:选择合适的缓存策略
// vue.config.js
module.exports = {
pwa: {
workboxOptions: {
runtimeCaching: [
// API请求:NetworkFirst策略
{
urlPattern: new RegExp('^https://api\.example\.com/'),
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
networkTimeoutSeconds: 10,
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 7 // 7天
}
}
},
// 静态资源:CacheFirst策略
{
urlPattern: new RegExp('\\.(?:png|jpg|jpeg|svg|gif)$'),
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
expiration: {
maxEntries: 60,
maxAgeSeconds: 60 * 60 * 24 * 30 // 30天
}
}
},
// 字体:StaleWhileRevalidate策略
{
urlPattern: new RegExp('\\.(?:woff|woff2|ttf|otf|eot)$'),
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'font-cache',
expiration: {
maxEntries: 30,
maxAgeSeconds: 60 * 60 * 24 * 365 // 1年
}
}
}
]
}
}
}错误场景:缓存大小限制和过期策略不当
// 错误示例:缓存大小和过期策略不当
// service-worker.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('app-cache').then((cache) => {
// 错误:缓存过多资源,无大小限制
return cache.addAll([
'/',
'/index.html',
'/css/app.css',
'/js/app.js',
'/img/large-image.jpg',
'/font/large-font.woff2'
])
})
)
})正确实现:
// 正确示例:合理设置缓存大小和过期策略
// service-worker.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('app-cache').then((cache) => {
// 正确:只缓存必要的资源
return cache.addAll([
'/',
'/index.html',
'/css/app.css',
'/js/app.js'
])
})
)
})
// 正确:清理过期缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== 'app-cache' && cacheName !== 'api-cache') {
return caches.delete(cacheName)
}
})
)
})
)
})
// 正确:实现缓存过期检查
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
// 检查缓存是否过期
const cacheControl = response.headers.get('Cache-Control')
const maxAge = cacheControl ? parseInt(cacheControl.split('max-age=')[1]) : 86400
const date = new Date(response.headers.get('Date'))
const now = new Date()
const age = (now - date) / 1000
if (age > maxAge) {
// 缓存过期,从网络获取
return fetch(event.request).then((networkResponse) => {
caches.open('app-cache').then((cache) => {
cache.put(event.request, networkResponse.clone())
})
return networkResponse
})
}
return response
}
return fetch(event.request).then((networkResponse) => {
caches.open('app-cache').then((cache) => {
cache.put(event.request, networkResponse.clone())
})
return networkResponse
})
})
)
})21.3 Vue PWA安装体验的使用误区
核心知识点
在优化Vue PWA安装体验时,常见的误区包括:
- 安装提示时机:安装提示时机不当
- 安装提示频率:安装提示频率过高
- 安装体验优化:未优化安装体验
- 安装状态管理:安装状态管理不当
实用案例分析
错误场景:安装提示时机不当
// 错误示例:安装提示时机不当
// 组件中
<template>
<div>
<button v-if="deferredPrompt" @click="installApp">安装应用</button>
</div>
</template>
<script>
export default {
data() {
return {
deferredPrompt: null
}
},
mounted() {
// 错误:在组件挂载时立即提示安装
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
this.deferredPrompt = e
// 错误:立即显示安装按钮,不考虑用户体验
this.showInstallButton = true
})
},
methods: {
async installApp() {
if (this.deferredPrompt) {
this.deferredPrompt.prompt()
const { outcome } = await this.deferredPrompt.userChoice
console.log(`用户选择: ${outcome}`)
this.deferredPrompt = null
}
}
}
}
</script>正确实现:
// 正确示例:优化安装提示时机
// 组件中
<template>
<div>
<button v-if="showInstallButton" @click="installApp">安装应用</button>
</div>
</template>
<script>
export default {
data() {
return {
deferredPrompt: null,
showInstallButton: false,
installAttempts: 0,
maxInstallAttempts: 3
}
},
mounted() {
// 正确:监听beforeinstallprompt事件
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
this.deferredPrompt = e
// 正确:在合适的时机显示安装按钮
// 例如,用户访问多次后或完成某些操作后
this.checkInstallPrompt()
})
// 监听安装完成事件
window.addEventListener('appinstalled', (evt) => {
console.log('应用已安装')
this.deferredPrompt = null
this.showInstallButton = false
})
},
methods: {
checkInstallPrompt() {
// 检查用户访问次数
const visitCount = parseInt(localStorage.getItem('visitCount') || '0') + 1
localStorage.setItem('visitCount', visitCount.toString())
// 检查是否已安装
const isInstalled = window.matchMedia('(display-mode: standalone)').matches
// 正确:在用户访问多次且未安装时显示安装提示
if (visitCount >= 3 && !isInstalled && this.installAttempts < this.maxInstallAttempts) {
this.showInstallButton = true
}
},
async installApp() {
if (this.deferredPrompt) {
this.installAttempts++
this.deferredPrompt.prompt()
const { outcome } = await this.deferredPrompt.userChoice
console.log(`用户选择: ${outcome}`)
this.deferredPrompt = null
this.showInstallButton = false
}
}
}
}
</script>
// 或使用Intersection Observer在用户滚动到特定位置时显示
<template>
<div>
<div ref="installTrigger"></div>
<button v-if="showInstallButton" @click="installApp">安装应用</button>
</div>
</template>
<script>
export default {
data() {
return {
deferredPrompt: null,
showInstallButton: false
}
},
mounted() {
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
this.deferredPrompt = e
})
// 使用Intersection Observer
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && this.deferredPrompt) {
this.showInstallButton = true
observer.unobserve(entry.target)
}
})
})
if (this.$refs.installTrigger) {
observer.observe(this.$refs.installTrigger)
}
}
}
</script>错误场景:安装体验优化不当
// 错误示例:安装体验优化不当
// 组件中
<template>
<button @click="installApp">安装应用</button>
</template>
<script>
export default {
methods: {
installApp() {
// 错误:直接提示安装,无引导和说明
if (this.deferredPrompt) {
this.deferredPrompt.prompt()
}
}
}
}
</script>正确实现:
// 正确示例:优化安装体验
// 组件中
<template>
<div>
<!-- 安装提示模态框 -->
<div v-if="showInstallModal" class="install-modal">
<div class="install-modal-content">
<h3>安装我们的应用</h3>
<p>将应用添加到主屏幕,获得更好的使用体验:</p>
<ul>
<li>离线访问</li>
<li>更快的加载速度</li>
<li>类似原生应用的体验</li>
</ul>
<div class="install-modal-buttons">
<button @click="installApp">立即安装</button>
<button @click="cancelInstall">稍后再说</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
deferredPrompt: null,
showInstallModal: false
}
},
mounted() {
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
this.deferredPrompt = e
})
},
methods: {
showInstallPrompt() {
if (this.deferredPrompt) {
this.showInstallModal = true
}
},
async installApp() {
if (this.deferredPrompt) {
this.showInstallModal = false
this.deferredPrompt.prompt()
const { outcome } = await this.deferredPrompt.userChoice
console.log(`用户选择: ${outcome}`)
this.deferredPrompt = null
}
},
cancelInstall() {
this.showInstallModal = false
// 记录用户取消安装,一段时间内不再提示
localStorage.setItem('installCancelled', Date.now().toString())
}
}
}
</script>
<style scoped>
.install-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.install-modal-content {
background-color: white;
padding: 20px;
border-radius: 8px;
max-width: 400px;
width: 90%;
}
.install-modal-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
justify-content: flex-end;
}
</style>21.3 Vue PWA安装体验的使用误区
核心知识点
在优化Vue PWA安装体验时,常见的误区包括:
- 安装提示时机:安装提示时机不当
- 安装提示频率:安装提示频率过高
- 安装体验优化:未优化安装体验
- 安装状态管理:安装状态管理不当
实用案例分析
错误场景:安装提示时机不当
// 错误示例:安装提示时机不当
// 组件中
<template>
<div>
<button v-if="deferredPrompt" @click="installApp">安装应用</button>
</div>
</template>
<script>
export default {
data() {
return {
deferredPrompt: null
}
},
mounted() {
// 错误:在组件挂载时立即提示安装
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
this.deferredPrompt = e
// 错误:立即显示安装按钮,不考虑用户体验
this.showInstallButton = true
})
},
methods: {
async installApp() {
if (this.deferredPrompt) {
this.deferredPrompt.prompt()
const { outcome } = await this.deferredPrompt.userChoice
console.log(`用户选择: ${outcome}`)
this.deferredPrompt = null
}
}
}
}
</script>正确实现:
// 正确示例:优化安装提示时机
// 组件中
<template>
<div>
<button v-if="showInstallButton" @click="installApp">安装应用</button>
</div>
</template>
<script>
export default {
data() {
return {
deferredPrompt: null,
showInstallButton: false,
installAttempts: 0,
maxInstallAttempts: 3
}
},
mounted() {
// 正确:监听beforeinstallprompt事件
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
this.deferredPrompt = e
// 正确:在合适的时机显示安装按钮
// 例如,用户访问多次后或完成某些操作后
this.checkInstallPrompt()
})
// 监听安装完成事件
window.addEventListener('appinstalled', (evt) => {
console.log('应用已安装')
this.deferredPrompt = null
this.showInstallButton = false
})
},
methods: {
checkInstallPrompt() {
// 检查用户访问次数
const visitCount = parseInt(localStorage.getItem('visitCount') || '0') + 1
localStorage.setItem('visitCount', visitCount.toString())
// 检查是否已安装
const isInstalled = window.matchMedia('(display-mode: standalone)').matches
// 正确:在用户访问多次且未安装时显示安装提示
if (visitCount >= 3 && !isInstalled && this.installAttempts < this.maxInstallAttempts) {
this.showInstallButton = true
}
},
async installApp() {
if (this.deferredPrompt) {
this.installAttempts++
this.deferredPrompt.prompt()
const { outcome } = await this.deferredPrompt.userChoice
console.log(`用户选择: ${outcome}`)
this.deferredPrompt = null
this.showInstallButton = false
}
}
}
}
</script>
// 或使用Intersection Observer在用户滚动到特定位置时显示
<template>
<div>
<div ref="installTrigger"></div>
<button v-if="showInstallButton" @click="installApp">安装应用</button>
</div>
</template>
<script>
export default {
data() {
return {
deferredPrompt: null,
showInstallButton: false
}
},
mounted() {
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
this.deferredPrompt = e
})
// 使用Intersection Observer
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && this.deferredPrompt) {
this.showInstallButton = true
observer.unobserve(entry.target)
}
})
})
if (this.$refs.installTrigger) {
observer.observe(this.$refs.installTrigger)
}
}
}
</script>错误场景:安装体验优化不当
// 错误示例:安装体验优化不当
// 组件中
<template>
<button @click="installApp">安装应用</button>
</template>
<script>
export default {
methods: {
installApp() {
// 错误:直接提示安装,无引导和说明
if (this.deferredPrompt) {
this.deferredPrompt.prompt()
}
}
}
}
</script>正确实现:
// 正确示例:优化安装体验
// 组件中
<template>
<div>
<!-- 安装提示模态框 -->
<div v-if="showInstallModal" class="install-modal">
<div class="install-modal-content">
<h3>安装我们的应用</h3>
<p>将应用添加到主屏幕,获得更好的使用体验:</p>
<ul>
<li>离线访问</li>
<li>更快的加载速度</li>
<li>类似原生应用的体验</li>
</ul>
<div class="install-modal-buttons">
<button @click="installApp">立即安装</button>
<button @click="cancelInstall">稍后再说</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
deferredPrompt: null,
showInstallModal: false
}
},
mounted() {
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
this.deferredPrompt = e
})
},
methods: {
showInstallPrompt() {
if (this.deferredPrompt) {
this.showInstallModal = true
}
},
async installApp() {
if (this.deferredPrompt) {
this.showInstallModal = false
this.deferredPrompt.prompt()
const { outcome } = await this.deferredPrompt.userChoice
console.log(`用户选择: ${outcome}`)
this.deferredPrompt = null
}
},
cancelInstall() {
this.showInstallModal = false
// 记录用户取消安装,一段时间内不再提示
localStorage.setItem('installCancelled', Date.now().toString())
}
}
}
</script>
<style scoped>
.install-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.install-modal-content {
background-color: white;
padding: 20px;
border-radius: 8px;
max-width: 400px;
width: 90%;
}
.install-modal-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
justify-content: flex-end;
}
</style>21.4 Vue PWA推送通知的常见问题
核心知识点
在实现Vue PWA推送通知时,常见的问题包括:
- 推送通知权限:权限申请不当
- 推送服务配置:推送服务配置错误
- 消息处理:推送消息处理不当
- 离线推送:离线状态下的推送处理
实用案例分析
错误场景:推送通知权限申请不当
// 错误示例:推送通知权限申请不当
// 组件中
<template>
<button @click="requestNotificationPermission">启用通知</button>
</template>
<script>
export default {
methods: {
async requestNotificationPermission() {
// 错误:直接请求权限,不解释用途
const permission = await Notification.requestPermission()
console.log(`通知权限: ${permission}`)
}
}
}
</script>正确实现:
// 正确示例:优化推送通知权限申请
// 组件中
<template>
<div>
<button v-if="!notificationPermission" @click="requestNotificationPermission">启用通知</button>
<p v-if="notificationPermission === 'granted'">通知已启用</p>
<p v-if="notificationPermission === 'denied'">通知已被拒绝</p>
</div>
</template>
<script>
export default {
data() {
return {
notificationPermission: null
}
},
mounted() {
// 检查现有权限
this.checkNotificationPermission()
},
methods: {
checkNotificationPermission() {
if ('Notification' in window) {
this.notificationPermission = Notification.permission
}
},
async requestNotificationPermission() {
// 正确:先解释通知用途
if ('Notification' in window) {
const shouldRequest = confirm('启用通知后,您将收到以下类型的通知:\n- 新消息提醒\n- 活动更新\n- 重要通知')
if (shouldRequest) {
const permission = await Notification.requestPermission()
this.notificationPermission = permission
console.log(`通知权限: ${permission}`)
if (permission === 'granted') {
// 订阅推送服务
this.subscribeToPush()
}
}
} else {
alert('您的浏览器不支持通知功能')
}
},
async subscribeToPush() {
if ('serviceWorker' in navigator && 'PushManager' in window) {
const registration = await navigator.serviceWorker.ready
try {
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY')
})
console.log('推送订阅成功:', subscription)
// 将订阅信息发送到服务器
this.sendSubscriptionToServer(subscription)
} catch (error) {
console.error('推送订阅失败:', error)
}
}
},
urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const rawData = window.atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
},
sendSubscriptionToServer(subscription) {
// 发送订阅信息到服务器
fetch('/api/push-subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
})
}
}
}
</script>错误场景:推送消息处理不当
// 错误示例:推送消息处理不当
// service-worker.js
self.addEventListener('push', (event) => {
// 错误:未处理推送消息
console.log('收到推送消息:', event)
})正确实现:
// 正确示例:处理推送消息
// service-worker.js
self.addEventListener('push', (event) => {
if (!event.data) return
try {
const data = event.data.json()
const options = {
body: data.body,
icon: '/img/icons/icon-192x192.png',
badge: '/img/icons/badge-72x72.png',
vibrate: [100, 50, 100],
data: {
url: data.url || '/'
},
actions: [
{
action: 'view',
title: '查看详情'
},
{
action: 'close',
title: '关闭'
}
]
}
event.waitUntil(
self.registration.showNotification(data.title, options)
)
} catch (error) {
console.error('处理推送消息失败:', error)
}
})
// 处理通知点击
self.addEventListener('notificationclick', (event) => {
event.notification.close()
if (event.action === 'view') {
const urlToOpen = event.notification.data.url || '/'
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => {
// 检查是否已有打开的窗口
for (let client of windowClients) {
if (client.url === urlToOpen && 'focus' in client) {
return client.focus()
}
}
// 打开新窗口
if (clients.openWindow) {
return clients.openWindow(urlToOpen)
}
})
)
}
})
// 处理通知关闭
self.addEventListener('notificationclose', (event) => {
console.log('通知已关闭')
})21.5 Vue PWA离线功能的陷阱
核心知识点
在实现Vue PWA离线功能时,常见的陷阱包括:
- 离线缓存策略:离线缓存策略不当
- 离线页面:未设置离线页面
- 离线数据处理:离线状态下的数据处理不当
- 网络状态检测:网络状态检测错误
实用案例分析
错误场景:离线缓存策略不当
// 错误示例:离线缓存策略不当
// service-worker.js
self.addEventListener('fetch', (event) => {
// 错误:对所有请求使用CacheFirst策略,可能导致数据过期
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request)
})
)
})正确实现:
// 正确示例:实现合理的离线缓存策略
// service-worker.js
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url)
// 对API请求使用NetworkFirst策略
if (url.origin === 'https://api.example.com') {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(event.request)
})
)
}
// 对静态资源使用CacheFirst策略
else if (url.pathname.match(/\\.(js|css|png|jpg|jpeg|svg|gif)$/)) {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request).then((networkResponse) => {
if (networkResponse && networkResponse.ok) {
const clonedResponse = networkResponse.clone()
caches.open('static-cache').then((cache) => {
cache.put(event.request, clonedResponse)
})
}
return networkResponse
})
})
)
}
// 对HTML页面使用NetworkFirst策略
else {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match('/offline.html')
})
)
}
})
// 缓存离线页面
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('app-cache').then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/offline.html',
'/css/app.css',
'/js/app.js'
])
})
)
})错误场景:未设置离线页面和网络状态检测
// 错误示例:未设置离线页面
// 组件中
<template>
<div>
<h1>应用内容</h1>
</div>
</template>
<script>
export default {
// 错误:未检测网络状态
}
</script>正确实现:
// 正确示例:设置离线页面和网络状态检测
// 1. 创建离线页面
// offline.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网络连接已断开</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
padding: 20px;
text-align: center;
}
h1 {
color: #333;
}
p {
color: #666;
max-width: 400px;
}
button {
margin-top: 20px;
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>网络连接已断开</h1>
<p>您当前处于离线状态,无法访问在线内容。请检查网络连接后重试。</p>
<button onclick="window.location.reload()">重新连接</button>
</body>
</html>
// 2. 组件中检测网络状态
// 组件中
<template>
<div>
<!-- 网络状态提示 -->
<div v-if="!isOnline" class="offline-indicator">
您当前处于离线状态,正在使用缓存内容
</div>
<h1>应用内容</h1>
</div>
</template>
<script>
export default {
data() {
return {
isOnline: navigator.onLine
}
},
mounted() {
// 监听网络状态变化
window.addEventListener('online', this.updateOnlineStatus)
window.addEventListener('offline', this.updateOnlineStatus)
},
beforeUnmount() {
window.removeEventListener('online', this.updateOnlineStatus)
window.removeEventListener('offline', this.updateOnlineStatus)
},
methods: {
updateOnlineStatus() {
this.isOnline = navigator.onLine
if (this.isOnline) {
// 网络恢复,刷新数据
this.refreshData()
}
},
refreshData() {
// 刷新应用数据
console.log('网络已恢复,刷新数据')
}
}
}
</script>
<style scoped>
.offline-indicator {
background-color: #ff9800;
color: white;
padding: 10px;
text-align: center;
margin-bottom: 20px;
}
</style>21.6 Vue PWA更新机制的使用误区
核心知识点
在实现Vue PWA更新机制时,常见的误区包括:
- 更新检测:更新检测机制不当
- 更新提示:更新提示时机和方式不当
- 更新安装:更新安装过程处理不当
- 缓存清理:更新后未正确清理缓存
实用案例分析
错误场景:更新检测机制不当
// 错误示例:更新检测机制不当
// 组件中
<template>
<div>
<button @click="checkForUpdates">检查更新</button>
</div>
</template>
<script>
export default {
methods: {
checkForUpdates() {
// 错误:手动检查更新,用户体验差
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then((registration) => {
registration.update()
console.log('检查更新')
})
}
}
}
}
</script>正确实现:
// 正确示例:实现自动更新检测
// 组件中
<template>
<div>
<!-- 更新提示 -->
<div v-if="updateAvailable" class="update-notification">
<p>发现新版本</p>
<button @click="installUpdate">立即更新</button>
<button @click="dismissUpdate">稍后再说</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
updateAvailable: false,
refreshing: false,
updateRegistration: null
}
},
mounted() {
// 监听Service Worker更新
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (this.refreshing) return
this.refreshing = true
// 重新加载页面
window.location.reload()
})
navigator.serviceWorker.ready.then((registration) => {
// 检查是否有等待中的更新
if (registration.waiting) {
this.updateAvailable = true
this.updateRegistration = registration
}
// 监听更新事件
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing
if (newWorker) {
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// 有新版本可用
this.updateAvailable = true
this.updateRegistration = registration
}
})
}
})
})
}
},
methods: {
installUpdate() {
if (this.updateRegistration && this.updateRegistration.waiting) {
// 发送消息给Service Worker,触发更新
this.updateRegistration.waiting.postMessage({ type: 'SKIP_WAITING' })
}
},
dismissUpdate() {
this.updateAvailable = false
// 记录用户忽略更新的时间
localStorage.setItem('lastUpdateDismissed', Date.now().toString())
}
}
}
</script>
<style scoped>
.update-notification {
background-color: #4CAF50;
color: white;
padding: 15px;
margin-bottom: 20px;
border-radius: 5px;
}
.update-notification button {
margin: 5px;
padding: 5px 10px;
border: none;
border-radius: 3px;
cursor: pointer;
}
</style>
// service-worker.js中处理更新
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting()
}
})
// 激活时立即控制所有客户端
self.addEventListener('activate', (event) => {
event.waitUntil(
self.clients.claim()
)
})错误场景:更新后未正确清理缓存
// 错误示例:更新后未清理缓存
// service-worker.js
self.addEventListener('activate', (event) => {
event.waitUntil(
// 错误:未清理过期缓存
self.clients.claim()
)
})正确实现:
// 正确示例:更新后清理过期缓存
// service-worker.js
self.addEventListener('activate', (event) => {
event.waitUntil(
Promise.all([
// 清理过期缓存
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
// 清理旧版本的缓存
if (cacheName !== 'app-cache-v2' && cacheName !== 'static-cache-v2') {
return caches.delete(cacheName)
}
})
)
}),
// 立即控制所有客户端
self.clients.claim()
])
)
})
// 版本化缓存名称
const CACHE_NAME = 'app-cache-v2'
const STATIC_CACHE_NAME = 'static-cache-v2'
// 安装时缓存必要资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/offline.html'
])
})
)
})21.7 Vue PWA性能优化的陷阱
核心知识点
在优化Vue PWA性能时,常见的陷阱包括:
- 资源优化:静态资源优化不当
- 缓存策略:缓存策略导致的性能问题
- Service Worker性能:Service Worker导致的性能问题
- 安装包大小:PWA安装包过大
实用案例分析
错误场景:静态资源优化不当
// 错误示例:静态资源优化不当
// vue.config.js
module.exports = {
// 错误:未配置资源优化
pwa: {
// 缺少资源优化配置
}
}正确实现:
// 正确示例:优化静态资源
// vue.config.js
module.exports = {
// 优化构建
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 10
},
common: {
name: 'common',
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
},
// PWA配置
pwa: {
workboxOptions: {
// 优化缓存
cleanupOutdatedCaches: true,
skipWaiting: true,
clientsClaim: true,
// 预缓存必要资源
preCaching: [
'/',
'/index.html'
],
// 动态缓存策略
runtimeCaching: [
{
urlPattern: new RegExp('^https://api\.example\.com/'),
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
networkTimeoutSeconds: 10
}
},
{
urlPattern: new RegExp('\\.(js|css)$'),
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-cache'
}
},
{
urlPattern: new RegExp('\\.(png|jpg|jpeg|svg|gif)$'),
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
expiration: {
maxEntries: 60,
maxAgeSeconds: 60 * 60 * 24 * 30 // 30天
}
}
}
]
}
}
}错误场景:Service Worker性能问题
// 错误示例:Service Worker性能问题
// service-worker.js
self.addEventListener('fetch', (event) => {
// 错误:对所有请求都进行缓存处理,影响性能
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response
}
return fetch(event.request).then((networkResponse) => {
if (networkResponse && networkResponse.ok) {
// 错误:对所有响应都进行缓存,包括大型文件
const clonedResponse = networkResponse.clone()
caches.open('all-cache').then((cache) => {
cache.put(event.request, clonedResponse)
})
}
return networkResponse
})
})
)
})正确实现:
// 正确示例:优化Service Worker性能
// service-worker.js
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url)
// 跳过浏览器扩展请求
if (url.origin !== self.location.origin) return
// 跳过非GET请求
if (event.request.method !== 'GET') return
// 跳过大型文件
if (url.pathname.match(/\\.(mp4|mov|avi|zip|rar)$/)) return
// 对不同类型的资源使用不同的缓存策略
if (url.pathname.match(/\\.(js|css)$/)) {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request).then((networkResponse) => {
if (networkResponse && networkResponse.ok) {
const clonedResponse = networkResponse.clone()
caches.open('static-cache').then((cache) => {
cache.put(event.request, clonedResponse)
})
}
return networkResponse
})
})
)
} else if (url.pathname.match(/\\.(png|jpg|jpeg|svg|gif)$/)) {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request).then((networkResponse) => {
if (networkResponse && networkResponse.ok) {
const clonedResponse = networkResponse.clone()
caches.open('image-cache').then((cache) => {
cache.put(event.request, clonedResponse)
})
}
return networkResponse
})
})
)
} else {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match('/offline.html')
})
)
}
})
// 限制缓存大小
self.addEventListener('activate', (event) => {
event.waitUntil(
Promise.all([
// 清理过期缓存
caches.open('image-cache').then((cache) => {
cache.keys().then((keys) => {
if (keys.length > 60) {
// 删除最早的缓存
return Promise.all(
keys.slice(0, keys.length - 60).map((key) => {
return cache.delete(key)
})
)
}
})
}),
self.clients.claim()
])
)
})21.8 Vue PWA兼容性的常见问题
核心知识点
在处理Vue PWA兼容性时,常见的问题包括:
- 浏览器兼容性:不同浏览器的PWA支持差异
- 设备兼容性:不同设备的PWA支持差异
- 功能降级:PWA功能在不支持的环境中的降级处理
- 测试覆盖:PWA兼容性测试覆盖不足
实用案例分析
错误场景:未处理浏览器兼容性
// 错误示例:未处理浏览器兼容性
// 组件中
<template>
<div>
<button @click="installApp">安装应用</button>
</div>
</template>
<script>
export default {
methods: {
installApp() {
// 错误:直接调用PWA相关API,不检查浏览器支持
if (this.deferredPrompt) {
this.deferredPrompt.prompt()
}
}
}
}
</script>正确实现:
// 正确示例:处理浏览器兼容性
// 组件中
<template>
<div>
<div v-if="!isPwaSupported" class="pwa-not-supported">
<p>您的浏览器不支持PWA功能</p>
<p>推荐使用Chrome、Firefox或Safari最新版本</p>
</div>
<button v-else-if="showInstallButton" @click="installApp">安装应用</button>
</div>
</template>
<script>
export default {
data() {
return {
isPwaSupported: false,
showInstallButton: false,
deferredPrompt: null
}
},
mounted() {
// 检查PWA支持
this.checkPwaSupport()
},
methods: {
checkPwaSupport() {
// 检查必要的PWA API支持
this.isPwaSupported = (
'serviceWorker' in navigator &&
'Notification' in window &&
'PushManager' in window &&
'beforeinstallprompt' in window
)
if (this.isPwaSupported) {
// 监听安装提示
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
this.deferredPrompt = e
this.showInstallButton = true
})
}
},
async installApp() {
if (this.deferredPrompt) {
this.deferredPrompt.prompt()
const { outcome } = await this.deferredPrompt.userChoice
console.log(`用户选择: ${outcome}`)
this.deferredPrompt = null
this.showInstallButton = false
}
}
}
}
</script>
<style scoped>
.pwa-not-supported {
background-color: #f44336;
color: white;
padding: 15px;
margin-bottom: 20px;
border-radius: 5px;
}
</style>错误场景:未实现功能降级
// 错误示例:未实现功能降级
// 组件中
<template>
<div>
<h1>{{ data.title }}</h1>
<p>{{ data.content }}</p>
</div>
</template>
<script>
export default {
data() {
return {
data: {}
}
},
mounted() {
// 错误:假设PWA功能可用,无降级方案
this.fetchData()
},
methods: {
async fetchData() {
// 错误:直接使用fetch,无离线降级
const response = await fetch('/api/data')
this.data = await response.json()
}
}
}
</script>正确实现:
// 正确示例:实现功能降级
// 组件中
<template>
<div>
<h1>{{ data.title }}</h1>
<p>{{ data.content }}</p>
<div v-if="!isOnline" class="offline-warning">
您当前处于离线状态,显示的是缓存内容
</div>
</div>
</template>
<script>
export default {
data() {
return {
data: {},
isOnline: navigator.onLine
}
},
mounted() {
// 监听网络状态
window.addEventListener('online', this.updateOnlineStatus)
window.addEventListener('offline', this.updateOnlineStatus)
// 获取数据
this.fetchData()
},
beforeUnmount() {
window.removeEventListener('online', this.updateOnlineStatus)
window.removeEventListener('offline', this.updateOnlineStatus)
},
methods: {
updateOnlineStatus() {
this.isOnline = navigator.onLine
if (this.isOnline) {
this.fetchData()
}
},
async fetchData() {
try {
// 尝试从网络获取
const response = await fetch('/api/data')
if (response.ok) {
this.data = await response.json()
// 缓存数据
localStorage.setItem('cachedData', JSON.stringify(this.data))
} else {
// 网络请求失败,使用缓存
this.loadCachedData()
}
} catch (error) {
// 网络错误,使用缓存
this.loadCachedData()
}
},
loadCachedData() {
// 加载缓存数据
const cachedData = localStorage.getItem('cachedData')
if (cachedData) {
this.data = JSON.parse(cachedData)
} else {
// 无缓存数据,使用默认数据
this.data = {
title: '默认标题',
content: '您当前处于离线状态,无可用数据'
}
}
}
}
}
</script>
<style scoped>
.offline-warning {
background-color: #ff9800;
color: white;
padding: 10px;
margin-top: 20px;
border-radius: 5px;
}
</style>