

PWA(Progressive Web App)是结合Web和原生应用优点的技术方案,通过Service Worker实现离线功能,Web App Manifest提供安装能力,HTTPS确保安全。具备离线可用、可安装、推送通知三大特性,开发成本低,更新便捷。
技术 | 作用 | 关键点 |
|---|---|---|
Service Worker | 离线缓存、推送 | 拦截网络请求 |
Web App Manifest | 安装配置 | 定义应用外观 |
HTTPS | 安全传输 | 必须协议 |
Cache API | 资源缓存 | 智能缓存策略 |
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的PWA应用</title>
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#2196F3">
</head>
<body>
<h1>欢迎使用PWA</h1>
<p>网络状态:<span id="status">在线</span></p>
<button id="installBtn" style="display:none">安装应用</button>
<script src="app.js"></script>
</body>
</html>{
"name": "我的PWA应用",
"short_name": "PWA应用",
"description": "简洁的PWA示例应用",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2196F3",
"icons": [
{
"src": "icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}// service-worker.js
const CACHE_NAME = 'pwa-v1';
const urlsToCache = [
'/',
'/app.js',
'/style.css'
];
// 安装事件 - 缓存资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
// 激活事件 - 清理旧缓存
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
// 拦截请求 - 缓存优先策略
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 缓存命中直接返回
if (response) {
return response;
}
// 克隆请求
const fetchRequest = event.request.clone();
return fetch(fetchRequest).then(response => {
// 检查是否有效响应
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 克隆响应
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});// app.js
class PWAApp {
constructor() {
this.init();
}
async init() {
// 注册Service Worker
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register('/service-worker.js');
console.log('Service Worker注册成功');
// 监听更新
registration.addEventListener('updatefound', () => {
console.log('发现新版本');
});
} catch (error) {
console.log('Service Worker注册失败:', error);
}
}
// 监听网络状态
this.setupNetworkListener();
// 设置安装提示
this.setupInstallPrompt();
}
setupNetworkListener() {
const statusElement = document.getElementById('status');
const updateOnlineStatus = () => {
statusElement.textContent = navigator.onLine ? '在线' : '离线';
statusElement.style.color = navigator.onLine ? 'green' : 'red';
};
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
updateOnlineStatus();
}
setupInstallPrompt() {
let deferredPrompt;
const installBtn = document.getElementById('installBtn');
window.addEventListener('beforeinstallprompt', (e) => {
// 阻止默认行为
e.preventDefault();
// 保存事件
deferredPrompt = e;
// 显示安装按钮
installBtn.style.display = 'block';
});
installBtn.addEventListener('click', async () => {
if (deferredPrompt) {
// 显示安装提示
deferredPrompt.prompt();
// 等待用户选择
const { outcome } = await deferredPrompt.userChoice;
console.log(`用户选择: ${outcome}`);
// 清空保存的事件
deferredPrompt = null;
installBtn.style.display = 'none';
}
});
// 监听应用安装
window.addEventListener('appinstalled', () => {
console.log('PWA已安装');
});
}
}
// 初始化应用
new PWAApp();// 推送通知管理器
class PushNotificationManager {
constructor() {
this.subscription = null;
}
async init() {
if (!('Notification' in window)) {
console.log('浏览器不支持通知');
return;
}
// 请求通知权限
const permission = await Notification.requestPermission();
console.log('通知权限:', permission);
if (permission === 'granted') {
this.setupPushSubscription();
}
}
async setupPushSubscription() {
if (!('serviceWorker' in navigator)) return;
const registration = await navigator.serviceWorker.ready;
// 获取现有订阅
this.subscription = await registration.pushManager.getSubscription();
if (!this.subscription) {
// 创建新订阅
this.subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlBase64ToUint8Array('YOUR_PUBLIC_KEY')
});
console.log('推送订阅已创建');
// 发送订阅到服务器
this.sendSubscriptionToServer(this.subscription);
}
}
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;
}
async sendSubscriptionToServer(subscription) {
// 发送到后端服务器
await fetch('/api/save-subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(subscription)
});
}
// 显示本地通知
showNotification(title, options = {}) {
if (Notification.permission === 'granted') {
navigator.serviceWorker.ready.then(registration => {
registration.showNotification(title, {
body: options.body || '',
icon: options.icon || '/icon-192.png',
badge: options.badge || '/icon-72.png',
vibrate: options.vibrate || [200, 100, 200],
data: options.data || {}
});
});
}
}
}server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# 安全头设置
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
# Service Worker允许范围
add_header Service-Worker-Allowed "/" always;
root /var/www/pwa-app;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# 缓存静态资源
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API代理
location /api/ {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}问题 | 解决方案 |
|---|---|
HTTPS混合内容 | 升级所有HTTP资源到HTTPS |
Service Worker不生效 | 检查文件路径和HTTPS协议 |
安装按钮不显示 | 确认Manifest配置正确 |
推送通知失败 | 检查VAPID密钥配置 |
缓存不更新 | 修改Service Worker版本号 |
iOS Safari兼容性问题 | 添加必要的polyfill |
PWA是Web应用的未来趋势,合理运用能显著提升用户体验和应用价值。