PWA 渐进式Web应用开发指南

PWA核心特性

  • 可安装:可以添加到主屏幕,像原生应用一样使用
  • 离线可用:通过Service Worker提供离线功能
  • 响应式设计:适配各种屏幕尺寸
  • 应用外壳:快速加载的界面框架
  • 推送通知:向用户发送通知消息
  • 安全连接:必须使用HTTPS

Service Worker实现

注册Service Worker

// 主线程注册Service Worker
if ('serviceWorker' in navigator) {
    window.addEventListener('load', async () => {
        try {
            const registration = await navigator.serviceWorker.register('/sw.js', {
                scope: '/'
            });
            
            console.log('Service Worker注册成功:', registration);
            
            // 检查更新
            registration.addEventListener('updatefound', () => {
                const newWorker = registration.installing;
                console.log('发现新Service Worker版本');
                
                newWorker.addEventListener('statechange', () => {
                    if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
                        // 新版本已安装,提示用户刷新
                        showUpdateNotification();
                    }
                });
            });
        } catch (error) {
            console.error('Service Worker注册失败:', error);
        }
    });
}

Service Worker文件

// sw.js - Service Worker主文件
const CACHE_NAME = 'pwa-cache-v1';
const urlsToCache = [
    '/',
    '/index.html',
    '/styles/main.css',
    '/scripts/app.js',
    '/images/icon-192.png',
    '/images/icon-512.png'
];

// 安装事件
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => {
                console.log('缓存文件:', urlsToCache);
                return cache.addAll(urlsToCache);
            })
            .then(() => self.skipWaiting())
    );
});

// 激活事件
self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cacheName => {
                    if (cacheName !== CACHE_NAME) {
                        console.log('删除旧缓存:', cacheName);
                        return caches.delete(cacheName);
                    }
                })
            );
        }).then(() => self.clients.claim())
    );
});

// 获取请求
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;
                }).catch(() => {
                    // 网络失败,尝试返回离线页面
                    if (event.request.mode === 'navigate') {
                        return caches.match('/offline.html');
                    }
                });
            })
    );
});

Web App Manifest

// manifest.json
{
    "name": "我的PWA应用",
    "short_name": "MyPWA",
    "description": "一个渐进式Web应用示例",
    "start_url": "/",
    "display": "standalone",
    "background_color": "#ffffff",
    "theme_color": "#4a90e2",
    "orientation": "portrait",
    "scope": "/",
    "lang": "zh-CN",
    "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"
        }
    ],
    "screenshots": [
        {
            "src": "/screenshots/desktop.png",
            "sizes": "1280x720",
            "type": "image/png",
            "form_factor": "wide"
        },
        {
            "src": "/screenshots/mobile.png",
            "sizes": "750x1334",
            "type": "image/png",
            "form_factor": "narrow"
        }
    ],
    "categories": ["productivity", "utilities"],
    "shortcuts": [
        {
            "name": "新建项目",
            "short_name": "新建",
            "description": "创建一个新项目",
            "url": "/new-project",
            "icons": [{ "src": "/icons/new.png", "sizes": "96x96" }]
        },
        {
            "name": "查看报告",
            "short_name": "报告",
            "description": "查看统计报告",
            "url": "/reports",
            "icons": [{ "src": "/icons/report.png", "sizes": "96x96" }]
        }
    ]
}

推送通知

// 请求通知权限
async function requestNotificationPermission() {
    if (!('Notification' in window)) {
        console.log('当前浏览器不支持通知');
        return false;
    }
    
    if (Notification.permission === 'granted') {
        return true;
    }
    
    const permission = await Notification.requestPermission();
    return permission === 'granted';
}

// 订阅推送
async function subscribeToPush() {
    if (!('serviceWorker' in navigator)) {
        console.log('Service Worker不支持');
        return null;
    }
    
    if (!('PushManager' in window)) {
        console.log('推送通知不支持');
        return null;
    }
    
    const registration = await navigator.serviceWorker.ready;
    const subscription = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array('你的公钥')
    });
    
    // 将subscription发送到服务器
    await sendSubscriptionToServer(subscription);
    
    return subscription;
}

// 显示通知
function showNotification(title, options = {}) {
    if (!('Notification' in window) || Notification.permission !== 'granted') {
        return;
    }
    
    const notification = new Notification(title, {
        icon: '/icons/icon-192.png',
        badge: '/icons/icon-96.png',
        vibrate: [200, 100, 200],
        ...options
    });
    
    notification.onclick = () => {
        window.focus();
        notification.close();
    };
}

// Service Worker中接收推送
self.addEventListener('push', event => {
    const data = event.data?.json() || { title: '新通知' };
    
    const options = {
        body: data.body || '您有一条新消息',
        icon: data.icon || '/icons/icon-192.png',
        badge: '/icons/icon-96.png',
        vibrate: [200, 100, 200],
        data: {
            url: data.url || '/'
        },
        actions: data.actions || [
            {
                action: 'view',
                title: '查看'
            },
            {
                action: 'close',
                title: '关闭'
            }
        ]
    };
    
    event.waitUntil(
        self.registration.showNotification(data.title, options)
    );
});

// 处理通知点击
self.addEventListener('notificationclick', event => {
    event.notification.close();
    
    if (event.action === 'close') {
        return;
    }
    
    event.waitUntil(
        clients.matchAll({ type: 'window' })
            .then(clientList => {
                // 如果有打开的窗口,聚焦它
                for (const client of clientList) {
                    if (client.url === event.notification.data.url && 'focus' in client) {
                        return client.focus();
                    }
                }
                
                // 否则打开新窗口
                if (clients.openWindow) {
                    return clients.openWindow(event.notification.data.url);
                }
            })
    );
});

PWA最佳实践

  1. 渐进增强:确保基础功能在不支持PWA的浏览器中也能工作
  2. 快速加载:应用外壳应在1秒内加载完成
  3. 离线优先:设计应用在离线状态下也能提供基本功能
  4. 响应式设计:确保在所有设备上都有良好的体验
  5. 定期更新:通过Service Worker自动更新缓存
  6. 性能监控:使用Lighthouse等工具评估PWA质量
  7. 用户引导:引导用户将应用添加到主屏幕

Lighthouse性能测试

# 安装Lighthouse
npm install -g lighthouse

# 运行测试
lighthouse https://your-pwa-app.com --output html --output-path ./report.html

# 或者使用Chrome DevTools
# 1. 打开Chrome DevTools
# 2. 转到Lighthouse标签页
# 3. 选择PWA审计类别
# 4. 点击"生成报告"

PWA技术让Web应用具备了原生应用的体验,同时保持了Web的开放性和可访问性。通过合理使用Service Worker、Web App Manifest和推送通知等特性,可以创建出功能丰富、性能优异的渐进式Web应用。