PWA (Progressive Web App)
渐进式 Web 应用开发相关工具和框架。
Vite PWA
基本信息
- 简介: Vite 的零配置 PWA 插件
- 链接: https://vite-pwa-org.netlify.app/
- GitHub: https://github.com/vite-pwa/vite-plugin-pwa
特点
- ✅ 零配置,开箱即用
- ✅ 自动生成 Service Worker
- ✅ 自动生成 Web App Manifest
- ✅ 支持离线访问
- ✅ 支持预缓存
- ✅ 支持 Workbox
- ✅ TypeScript 支持
安装
bash
pnpm add -D vite-plugin-pwabash
bun add -D vite-plugin-pwabash
npm install -D vite-plugin-pwabash
yarn add -D vite-plugin-pwa基础配置
typescript
// vite.config.ts
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
manifest: {
name: '我的应用',
short_name: '应用',
description: '我的 PWA 应用',
theme_color: '#ffffff',
background_color: '#ffffff',
display: 'standalone',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 // 24 hours
},
cacheableResponse: {
statuses: [0, 200]
}
}
}
]
}
})
]
})Vue 3 集成
vue
<script setup lang="ts">
import { useRegisterSW } from 'virtual:pwa-register/vue'
const {
needRefresh,
updateServiceWorker,
} = useRegisterSW({
onRegistered(r) {
console.log('SW Registered:', r)
},
onRegisterError(error) {
console.log('SW registration error', error)
},
})
const close = () => {
needRefresh.value = false
}
const update = () => {
updateServiceWorker(true)
}
</script>
<template>
<div v-if="needRefresh" class="pwa-toast">
<div class="message">
<span>发现新版本,点击更新</span>
</div>
<div class="buttons">
<button @click="update">更新</button>
<button @click="close">关闭</button>
</div>
</div>
</template>
<style scoped>
.pwa-toast {
position: fixed;
right: 0;
bottom: 0;
margin: 16px;
padding: 12px;
border: 1px solid #8885;
border-radius: 4px;
z-index: 1000;
background: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.buttons {
margin-top: 8px;
display: flex;
gap: 8px;
}
button {
padding: 4px 12px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
}
</style>离线页面
typescript
// vite.config.ts
VitePWA({
workbox: {
navigateFallback: '/offline.html',
navigateFallbackDenylist: [/^\/api/]
}
})html
<!-- public/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 {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
font-family: system-ui, -apple-system, sans-serif;
background: #f5f5f5;
}
.container {
text-align: center;
padding: 2rem;
}
h1 {
color: #333;
margin-bottom: 1rem;
}
p {
color: #666;
}
</style>
</head>
<body>
<div class="container">
<h1>📡 当前离线</h1>
<p>请检查网络连接后重试</p>
</div>
</body>
</html>缓存策略
typescript
// vite.config.ts
VitePWA({
workbox: {
runtimeCaching: [
// 网络优先
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
networkTimeoutSeconds: 10,
expiration: {
maxEntries: 50,
maxAgeSeconds: 60 * 60 * 24 // 1 day
}
}
},
// 缓存优先
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days
}
}
},
// 仅网络
{
urlPattern: /^https:\/\/analytics\.example\.com\/.*/i,
handler: 'NetworkOnly'
},
// 仅缓存
{
urlPattern: /^https:\/\/cdn\.example\.com\/static\/.*/i,
handler: 'CacheOnly'
},
// 优先缓存,网络回退
{
urlPattern: /\.(?:js|css)$/,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-resources'
}
}
]
}
})Workbox
基本信息
- 简介: Google 的 Service Worker 工具库
- 链接: https://developer.chrome.com/docs/workbox/
- GitHub: https://github.com/GoogleChrome/workbox
安装
bash
pnpm add workbox-windowbash
bun add workbox-windowbash
npm install workbox-windowbash
yarn add workbox-window基础用法
typescript
import { Workbox } from 'workbox-window'
if ('serviceWorker' in navigator) {
const wb = new Workbox('/sw.js')
wb.addEventListener('installed', (event) => {
if (event.isUpdate) {
console.log('New service worker installed')
}
})
wb.addEventListener('activated', (event) => {
console.log('Service worker activated')
})
wb.register()
}PWA Asset Generator
基本信息
- 简介: 自动生成 PWA 图标和启动画面
- GitHub: https://github.com/elegantapp/pwa-asset-generator
安装
bash
npm install -g pwa-asset-generator使用
bash
# 从单个图标生成所有尺寸
pwa-asset-generator logo.svg ./public/icons
# 生成带背景的图标
pwa-asset-generator logo.svg ./public/icons --background "#ffffff"
# 生成 iOS 启动画面
pwa-asset-generator logo.svg ./public/icons --splash-only通知推送
Web Push
typescript
// 请求通知权限
async function requestNotificationPermission() {
const permission = await Notification.requestPermission()
if (permission === 'granted') {
console.log('Notification permission granted')
// 订阅推送
const registration = await navigator.serviceWorker.ready
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: 'YOUR_PUBLIC_VAPID_KEY'
})
// 发送订阅信息到服务器
await fetch('/api/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
})
}
}
// 显示通知
function showNotification(title: string, options?: NotificationOptions) {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then((registration) => {
registration.showNotification(title, {
body: options?.body || '',
icon: options?.icon || '/icon.png',
badge: options?.badge || '/badge.png',
vibrate: [200, 100, 200],
tag: options?.tag || 'notification',
requireInteraction: false,
...options
})
})
}
}Service Worker 中处理推送
typescript
// sw.js
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? {}
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon || '/icon.png',
badge: '/badge.png',
data: data.url
})
)
})
self.addEventListener('notificationclick', (event) => {
event.notification.close()
event.waitUntil(
clients.openWindow(event.notification.data || '/')
)
})安装提示
自定义安装提示
typescript
let deferredPrompt: any
window.addEventListener('beforeinstallprompt', (e) => {
// 阻止默认提示
e.preventDefault()
// 保存事件
deferredPrompt = e
// 显示自定义安装按钮
showInstallButton()
})
async function installApp() {
if (!deferredPrompt) return
// 显示安装提示
deferredPrompt.prompt()
// 等待用户响应
const { outcome } = await deferredPrompt.userChoice
console.log(`User response: ${outcome}`)
// 清除保存的事件
deferredPrompt = null
// 隐藏安装按钮
hideInstallButton()
}
// 监听安装完成
window.addEventListener('appinstalled', () => {
console.log('PWA installed successfully')
})Vue 3 安装组件
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const showInstall = ref(false)
let deferredPrompt: any = null
onMounted(() => {
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault()
deferredPrompt = e
showInstall.value = true
})
window.addEventListener('appinstalled', () => {
showInstall.value = false
console.log('PWA installed')
})
})
const install = async () => {
if (!deferredPrompt) return
deferredPrompt.prompt()
const { outcome } = await deferredPrompt.userChoice
if (outcome === 'accepted') {
console.log('User accepted the install prompt')
}
deferredPrompt = null
showInstall.value = false
}
const dismiss = () => {
showInstall.value = false
}
</script>
<template>
<div v-if="showInstall" class="install-banner">
<div class="content">
<p>安装应用到主屏幕,获得更好的体验</p>
<div class="buttons">
<button @click="install" class="install-btn">安装</button>
<button @click="dismiss" class="dismiss-btn">稍后</button>
</div>
</div>
</div>
</template>
<style scoped>
.install-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
padding: 1rem;
z-index: 1000;
}
.content {
max-width: 600px;
margin: 0 auto;
}
.buttons {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
}
.install-btn {
flex: 1;
padding: 0.5rem 1rem;
background: #0abab5;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.dismiss-btn {
padding: 0.5rem 1rem;
background: transparent;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
}
</style>最佳实践
1. Manifest 配置
json
{
"name": "我的应用",
"short_name": "应用",
"description": "应用描述",
"start_url": "/",
"display": "standalone",
"theme_color": "#0abab5",
"background_color": "#ffffff",
"orientation": "portrait",
"icons": [
{
"src": "/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}2. 性能优化
- 预缓存关键资源
- 使用合适的缓存策略
- 压缩静态资源
- 懒加载非关键资源
- 使用 CDN
3. 离线体验
- 提供离线页面
- 缓存 API 响应
- 显示离线状态
- 同步离线数据
4. 更新策略
- 自动更新 Service Worker
- 提示用户刷新
- 优雅降级
检测工具
Lighthouse
Chrome DevTools 内置的 PWA 审计工具。
- 打开 Chrome DevTools
- 切换到 Lighthouse 标签
- 选择 "Progressive Web App"
- 点击 "Generate report"
PWA Builder
在线 PWA 检测和构建工具。