PWA (渐进式Web应用)
概述
PWA (Progressive Web App) 是一种结合了Web和原生应用优势的现代Web应用技术。它通过Service Worker、Web App Manifest、Push API等核心技术,为Web应用提供了离线访问、推送通知、主屏幕安装等原生应用特性。
前端专业视角
1. PWA核心技术
Service Worker
Service Worker是PWA的核心技术,它是一个运行在后台的独立线程脚本,与网页主线程分离。
核心特性:
- 后台运行: 独立于主线程,不阻塞页面交互
- 网络拦截: 可以拦截和处理网络请求
- 缓存管理: 实现离线缓存和资源管理
- 推送通知: 处理后台推送消息
- 生命周期管理: 安装、激活、更新、终止等状态管理
Service Worker生命周期图示:
Service Worker架构图示:
Service Worker工作原理:
Service Worker通过事件驱动的方式工作,主要处理以下事件:
- install事件:Service Worker安装时触发,用于缓存核心资源
- activate事件:Service Worker激活时触发,用于清理旧缓存
- fetch事件:网络请求时触发,用于实现缓存策略
- push事件:接收推送消息时触发
- sync事件:后台同步时触发
Service Worker核心代码实现:
// Service Worker注册
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker注册成功');
})
.catch(error => {
console.error('Service Worker注册失败:', error);
});
}
// Service Worker核心代码 (sw.js)
const CACHE_NAME = 'pwa-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/images/logo.png'
];
// 安装事件
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
return 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;
}
// 缓存未命中,从网络获取
return fetch(event.request);
})
);
});
缓存策略说明:
PWA使用多种缓存策略来优化用户体验:
- Cache First:优先使用缓存,适用于静态资源
- Network First:优先使用网络,适用于动态内容
- Stale While Revalidate:使用缓存同时更新,适用于平衡性能和新鲜度
- Network Only:仅使用网络,适用于关键数据
- Cache Only:仅使用缓存,适用于离线场景
核心资源管理:
Service Worker需要管理不同类型的资源:
- 核心资源:应用启动必需的文件(HTML、CSS、JS)
- 静态资源:不常变化的资源(图片、字体、样式)
- 动态资源:经常变化的内容(API数据、用户内容)
- 离线资源:离线时显示的备用内容
Web App Manifest
Web App Manifest是一个JSON文件,用于定义PWA应用的元数据和安装行为。
核心配置项:
- name/short_name:应用名称
- description:应用描述
- start_url:启动URL
- display:显示模式(standalone、fullscreen等)
- theme_color/background_color:主题颜色
- icons:应用图标
- orientation:屏幕方向
- scope:应用作用域
Manifest配置示例:
{
"name": "我的PWA应用",
"short_name": "PWA App",
"description": "一个功能强大的PWA应用",
"start_url": "/",
"display": "standalone",
"theme_color": "#2196F3",
"background_color": "#ffffff",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
}
]
}
Push API
Push API允许服务器向用户设备发送推送通知,即使应用未在前台运行。
推送通知流程:
推送通知特性:
- 后台推送:应用关闭时仍可接收通知
- 用户权限:需要用户明确授权
- 跨平台:支持所有现代浏览器
- 自定义样式:可以自定义通知外观和行为
推送通知核心代码实现:
// 请求推送权限
async function requestNotificationPermission() {
if ('Notification' in window) {
const permission = await Notification.requestPermission();
return permission === 'granted';
}
return false;
}
// 订阅推送服务
async function subscribeToPush() {
if ('serviceWorker' in navigator && 'PushManager' in window) {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: 'YOUR_VAPID_PUBLIC_KEY'
});
// 发送订阅信息到服务器
await fetch('/api/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
});
return subscription;
}
return null;
}
// 处理推送消息
self.addEventListener('push', event => {
const options = {
body: event.data ? event.data.text() : '您有新的消息',
icon: '/images/icon-192.png',
badge: '/images/badge-72.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'explore',
title: '查看详情',
icon: '/images/checkmark.png'
},
{
action: 'close',
title: '关闭',
icon: '/images/xmark.png'
}
]
};
event.waitUntil(
self.registration.showNotification('PWA通知', options)
);
});
// 处理通知点击
self.addEventListener('notificationclick', event => {
event.notification.close();
if (event.action === 'explore') {
event.waitUntil(
clients.openWindow('/')
);
}
});
2. PWA核心特性
离线功能
PWA的核心优势之一是提供离线访问能力,通过Service Worker实现智能缓存策略。
离线策略分类:
离线体验优化:
- 渐进式加载:优先加载核心内容
- 智能预缓存:预测用户行为,提前缓存
- 离线提示:明确告知用户当前状态
- 数据同步:网络恢复时自动同步数据
缓存策略核心代码实现:
// 缓存策略实现
class CacheStrategy {
// Cache First策略
static async cacheFirst(request) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
const networkResponse = await fetch(request);
const cache = await caches.open('pwa-cache-v1');
cache.put(request, networkResponse.clone());
return networkResponse;
}
// Network First策略
static async networkFirst(request) {
try {
const networkResponse = await fetch(request);
const cache = await caches.open('pwa-cache-v1');
cache.put(request, networkResponse.clone());
return networkResponse;
} catch (error) {
const cachedResponse = await caches.match(request);
return cachedResponse || new Response('网络错误', { status: 408 });
}
}
// Stale While Revalidate策略
static async staleWhileRevalidate(request) {
const cache = await caches.open('pwa-cache-v1');
const cachedResponse = await cache.match(request);
const fetchPromise = fetch(request).then(networkResponse => {
cache.put(request, networkResponse.clone());
return networkResponse;
});
return cachedResponse || await fetchPromise;
}
}
// 智能缓存策略选择
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
// 根据请求类型选择策略
if (url.pathname.includes('/api/')) {
event.respondWith(CacheStrategy.networkFirst(request));
} else if (url.pathname.includes('/static/')) {
event.respondWith(CacheStrategy.cacheFirst(request));
} else {
event.respondWith(CacheStrategy.staleWhileRevalidate(request));
}
});
安装体验
PWA可以像原生应用一样安装到用户设备上,提供原生应用的使用体验。
安装流程:
安装条件:
- 有效的Web App Manifest
- 已注册的Service Worker
- 通过HTTPS访问
- 用户交互(点击、滚动等)
- 满足PWA质量要求
安装体验核心代码实现:
// 检测安装提示
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
// 阻止默认的安装提示
e.preventDefault();
deferredPrompt = e;
// 显示自定义安装按钮
showInstallButton();
});
// 显示安装按钮
function showInstallButton() {
const installButton = document.getElementById('install-button');
if (installButton) {
installButton.style.display = 'block';
installButton.addEventListener('click', installApp);
}
}
// 安装应用
async function installApp() {
if (deferredPrompt) {
// 显示安装提示
deferredPrompt.prompt();
// 等待用户响应
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
console.log('用户接受了安装提示');
} else {
console.log('用户拒绝了安装提示');
}
deferredPrompt = null;
}
}
// 检测应用是否已安装
window.addEventListener('appinstalled', (evt) => {
console.log('PWA应用已安装');
// 隐藏安装按钮
const installButton = document.getElementById('install-button');
if (installButton) {
installButton.style.display = 'none';
}
});
// 检测应用启动方式
window.addEventListener('load', () => {
if (window.matchMedia('(display-mode: standalone)').matches) {
console.log('应用以独立模式启动');
} else if (window.navigator.standalone === true) {
console.log('应用在iOS上以独立模式启动');
} else {
console.log('应用在浏览器中启动');
}
});
响应式设计
PWA必须适配各种设备和屏幕尺寸,提供一致的用户体验。
响应式设计原则:
- 移动优先:从移动设备开始设计
- 弹性布局:使用Flexbox、Grid等现代布局
- 相对单位:使用rem、em、vw、vh等相对单位
- 断点设计:为不同屏幕尺寸设置断点
- 触摸友好:确保触摸目标足够大
3. PWA开发最佳实践
性能优化
PWA的性能直接影响用户体验,需要从多个维度进行优化。
性能优化策略:
性能优化核心代码实现:
// 性能优化管理器
class PWAOptimizer {
constructor() {
this.metrics = {
loadTime: 0,
firstContentfulPaint: 0,
largestContentfulPaint: 0,
cumulativeLayoutShift: 0
};
this.init();
}
init() {
this.setupPerformanceMonitoring();
this.optimizeResources();
this.setupPreloading();
}
// 性能监控
setupPerformanceMonitoring() {
if ('PerformanceObserver' in window) {
// 监控LCP
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.largestContentfulPaint = lastEntry.startTime;
});
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
// 监控CLS
const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
this.metrics.cumulativeLayoutShift += entry.value;
}
}
});
clsObserver.observe({ entryTypes: ['layout-shift'] });
}
}
// 资源优化
optimizeResources() {
// 图片懒加载
this.setupLazyLoading();
// 代码分割
this.setupCodeSplitting();
// 资源压缩
this.setupResourceCompression();
}
// 懒加载实现
setupLazyLoading() {
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
imageObserver.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
}
// 代码分割
setupCodeSplitting() {
// 动态导入非关键代码
const loadNonCriticalCode = () => {
import('./non-critical-module.js')
.then(module => {
console.log('非关键代码加载完成');
})
.catch(error => {
console.error('代码加载失败:', error);
});
};
// 在空闲时间加载
if ('requestIdleCallback' in window) {
requestIdleCallback(loadNonCriticalCode);
} else {
setTimeout(loadNonCriticalCode, 1000);
}
}
// 预加载设置
setupPreloading() {
// 预加载关键资源
const criticalResources = [
'/styles/critical.css',
'/scripts/main.js',
'/images/hero.jpg'
];
criticalResources.forEach(resource => {
const link = document.createElement('link');
link.rel = 'preload';
link.href = resource;
link.as = this.getResourceType(resource);
document.head.appendChild(link);
});
// 预连接重要域名
const importantDomains = [
'https://fonts.googleapis.com',
'https://cdn.example.com'
];
importantDomains.forEach(domain => {
const link = document.createElement('link');
link.rel = 'preconnect';
link.href = domain;
document.head.appendChild(link);
});
}
getResourceType(url) {
if (url.endsWith('.css')) return 'style';
if (url.endsWith('.js')) return 'script';
if (url.endsWith('.jpg') || url.endsWith('.png')) return 'image';
return 'fetch';
}
// 获取性能指标
getMetrics() {
return { ...this.metrics };
}
}
// 使用示例
const optimizer = new PWAOptimizer();
console.log('PWA性能指标:', optimizer.getMetrics());
安全性考虑
PWA涉及用户数据和设备权限,安全性至关重要。
安全最佳实践:
- HTTPS强制:所有PWA必须使用HTTPS
- 内容安全策略:设置CSP防止XSS攻击
- 权限管理:合理请求和使用设备权限
- 数据加密:敏感数据加密存储
- 定期更新:及时更新依赖和修复漏洞
安全性核心代码实现:
// PWA安全管理器
class PWASecurityManager {
constructor() {
this.init();
}
init() {
this.setupCSP();
this.setupPermissionManagement();
this.setupDataEncryption();
this.setupSecurityHeaders();
}
// 内容安全策略设置
setupCSP() {
const csp = `
default-src 'self';
script-src 'self' 'unsafe-inline' https://cdn.example.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
manifest-src 'self';
worker-src 'self';
`;
const meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = csp.trim();
document.head.appendChild(meta);
}
// 权限管理
setupPermissionManagement() {
this.requestPermissions();
this.monitorPermissionChanges();
}
async requestPermissions() {
const permissions = [
'notifications',
'geolocation',
'camera',
'microphone'
];
for (const permission of permissions) {
try {
const result = await navigator.permissions.query({ name: permission });
console.log(`${permission}权限状态:`, result.state);
if (result.state === 'prompt') {
await this.requestPermission(permission);
}
} catch (error) {
console.warn(`权限${permission}不支持:`, error);
}
}
}
async requestPermission(permission) {
switch (permission) {
case 'notifications':
return await Notification.requestPermission();
case 'geolocation':
return new Promise((resolve) => {
navigator.geolocation.getCurrentPosition(
() => resolve('granted'),
() => resolve('denied')
);
});
default:
return 'denied';
}
}
monitorPermissionChanges() {
if ('permissions' in navigator) {
navigator.permissions.query({ name: 'notifications' })
.then(result => {
result.addEventListener('change', () => {
console.log('通知权限状态变化:', result.state);
});
});
}
}
// 数据加密
setupDataEncryption() {
this.encryptionKey = this.generateEncryptionKey();
}
generateEncryptionKey() {
return crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256
},
true,
['encrypt', 'decrypt']
);
}
async encryptData(data) {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(JSON.stringify(data));
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedData = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
this.encryptionKey,
dataBuffer
);
return {
data: Array.from(new Uint8Array(encryptedData)),
iv: Array.from(iv)
};
}
async decryptData(encryptedData) {
const key = await this.encryptionKey;
const dataBuffer = new Uint8Array(encryptedData.data);
const iv = new Uint8Array(encryptedData.iv);
const decryptedData = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
dataBuffer
);
const decoder = new TextDecoder();
return JSON.parse(decoder.decode(decryptedData));
}
// 安全头设置
setupSecurityHeaders() {
// 设置安全相关的meta标签
const securityMeta = [
{ name: 'referrer', content: 'strict-origin-when-cross-origin' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' }
];
securityMeta.forEach(meta => {
const metaElement = document.createElement('meta');
metaElement.name = meta.name;
metaElement.content = meta.content;
document.head.appendChild(metaElement);
});
}
// 安全存储
async secureStore(key, data) {
const encryptedData = await this.encryptData(data);
localStorage.setItem(key, JSON.stringify(encryptedData));
}
async secureRetrieve(key) {
const encryptedData = JSON.parse(localStorage.getItem(key));
if (encryptedData) {
return await this.decryptData(encryptedData);
}
return null;
}
}
// 使用示例
const securityManager = new PWASecurityManager();
// 安全存储数据
await securityManager.secureStore('userData', { name: 'John', email: 'john@example.com' });
// 安全获取数据
const userData = await securityManager.secureRetrieve('userData');
console.log('用户数据:', userData);
用户体验设计
PWA的用户体验设计需要考虑Web和原生应用的特点。
UX设计原则:
- 一致性:保持与原生应用相似的交互模式
- 可发现性:用户能够轻松发现PWA功能
- 可安装性:提供清晰的安装指引
- 离线友好:设计适合离线使用的界面
- 性能感知:通过加载状态等反馈性能
用户体验核心代码实现:
// PWA用户体验管理器
class PWAUXManager {
constructor() {
this.init();
}
init() {
this.setupOfflineUI();
this.setupLoadingStates();
this.setupInstallPrompt();
this.setupSmoothTransitions();
}
// 离线UI设置
setupOfflineUI() {
// 检测网络状态
window.addEventListener('online', () => {
this.showOnlineStatus();
});
window.addEventListener('offline', () => {
this.showOfflineStatus();
});
// 初始状态检查
if (!navigator.onLine) {
this.showOfflineStatus();
}
}
showOnlineStatus() {
this.showToast('网络已连接', 'success');
this.hideOfflineBanner();
}
showOfflineStatus() {
this.showOfflineBanner();
this.showToast('网络已断开,正在使用离线模式', 'warning');
}
showOfflineBanner() {
const banner = document.createElement('div');
banner.id = 'offline-banner';
banner.className = 'offline-banner';
banner.innerHTML = `
<div class="offline-content">
<span class="offline-icon">📡</span>
<span class="offline-text">您当前处于离线模式</span>
<button class="offline-retry" onclick="location.reload()">重试</button>
</div>
`;
document.body.appendChild(banner);
}
hideOfflineBanner() {
const banner = document.getElementById('offline-banner');
if (banner) {
banner.remove();
}
}
// 加载状态管理
setupLoadingStates() {
this.setupPageLoading();
this.setupContentLoading();
this.setupSkeletonScreens();
}
setupPageLoading() {
// 页面加载指示器
const loader = document.createElement('div');
loader.id = 'page-loader';
loader.className = 'page-loader';
loader.innerHTML = `
<div class="loader-spinner"></div>
<div class="loader-text">加载中...</div>
`;
document.body.appendChild(loader);
// 页面加载完成后隐藏
window.addEventListener('load', () => {
setTimeout(() => {
loader.style.opacity = '0';
setTimeout(() => loader.remove(), 300);
}, 500);
});
}
setupContentLoading() {
// 内容加载状态
this.showContentLoading = (element) => {
element.classList.add('loading');
element.innerHTML = '<div class="content-loader">加载中...</div>';
};
this.hideContentLoading = (element, content) => {
element.classList.remove('loading');
element.innerHTML = content;
};
}
setupSkeletonScreens() {
// 骨架屏实现
this.createSkeletonScreen = (type) => {
const skeletons = {
card: `
<div class="skeleton-card">
<div class="skeleton-image"></div>
<div class="skeleton-content">
<div class="skeleton-line"></div>
<div class="skeleton-line short"></div>
</div>
</div>
`,
list: `
<div class="skeleton-list">
<div class="skeleton-item"></div>
<div class="skeleton-item"></div>
<div class="skeleton-item"></div>
</div>
`
};
return skeletons[type] || skeletons.card;
};
}
// 安装提示设置
setupInstallPrompt() {
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
this.showInstallButton();
});
this.showInstallButton = () => {
const installButton = document.createElement('button');
installButton.id = 'install-button';
installButton.className = 'install-button';
installButton.innerHTML = '📱 安装应用';
installButton.onclick = () => this.installApp(deferredPrompt);
document.body.appendChild(installButton);
};
this.installApp = async (deferredPrompt) => {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
this.showToast('应用安装成功!', 'success');
}
deferredPrompt = null;
document.getElementById('install-button').remove();
}
};
}
// 平滑过渡设置
setupSmoothTransitions() {
// 页面过渡动画
this.setupPageTransitions();
// 元素过渡动画
this.setupElementTransitions();
}
setupPageTransitions() {
// 页面切换动画
this.transitionToPage = (url) => {
const currentPage = document.body;
currentPage.style.opacity = '0';
currentPage.style.transform = 'translateX(-100%)';
setTimeout(() => {
window.location.href = url;
}, 300);
};
}
setupElementTransitions() {
// 元素进入动画
this.animateElement = (element, animation) => {
element.classList.add('animate', animation);
element.addEventListener('animationend', () => {
element.classList.remove('animate', animation);
}, { once: true });
};
}
// 提示消息
showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.innerHTML = `
<div class="toast-content">
<span class="toast-message">${message}</span>
<button class="toast-close" onclick="this.parentElement.parentElement.remove()">×</button>
</div>
`;
document.body.appendChild(toast);
// 自动移除
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// 响应式设计
setupResponsiveDesign() {
// 检测设备类型
this.detectDeviceType = () => {
const isMobile = window.innerWidth <= 768;
const isTablet = window.innerWidth > 768 && window.innerWidth <= 1024;
const isDesktop = window.innerWidth > 1024;
return { isMobile, isTablet, isDesktop };
};
// 调整UI布局
this.adjustLayout = () => {
const { isMobile, isTablet, isDesktop } = this.detectDeviceType();
document.body.classList.toggle('mobile', isMobile);
document.body.classList.toggle('tablet', isTablet);
document.body.classList.toggle('desktop', isDesktop);
};
// 监听窗口大小变化
window.addEventListener('resize', this.adjustLayout);
this.adjustLayout();
}
}
// 使用示例
const uxManager = new PWAUXManager();
// 显示加载状态
uxManager.showContentLoading(document.getElementById('content'));
// 显示提示消息
uxManager.showToast('操作成功!', 'success');
// 页面过渡
uxManager.transitionToPage('/next-page');
4. PWA实施策略
渐进式增强
PWA应该采用渐进式增强的策略,确保在不支持的环境中也能正常工作。
渐进式增强策略:
渐进式增强核心代码实现:
// PWA渐进式增强管理器
class PWAProgressiveEnhancement {
constructor() {
this.features = {
manifest: false,
serviceWorker: false,
pushNotifications: false,
installPrompt: false,
offlineSupport: false
};
this.init();
}
init() {
this.detectFeatures();
this.setupProgressiveEnhancement();
}
// 检测功能支持
detectFeatures() {
// 检测Manifest支持
this.features.manifest = 'manifest' in document;
// 检测Service Worker支持
this.features.serviceWorker = 'serviceWorker' in navigator;
// 检测推送通知支持
this.features.pushNotifications = 'PushManager' in window && 'Notification' in window;
// 检测安装提示支持
this.features.installPrompt = 'beforeinstallprompt' in window;
// 检测离线支持
this.features.offlineSupport = 'onLine' in navigator;
console.log('PWA功能支持情况:', this.features);
}
// 设置渐进式增强
setupProgressiveEnhancement() {
// 基础功能(所有浏览器支持)
this.setupBasicFeatures();
// 现代浏览器功能
if (this.features.manifest) {
this.setupManifest();
}
if (this.features.serviceWorker) {
this.setupServiceWorker();
}
if (this.features.pushNotifications) {
this.setupPushNotifications();
}
if (this.features.installPrompt) {
this.setupInstallPrompt();
}
if (this.features.offlineSupport) {
this.setupOfflineSupport();
}
}
// 基础功能设置
setupBasicFeatures() {
// 基础响应式设计
this.setupResponsiveDesign();
// 基础性能优化
this.setupBasicPerformance();
// 基础用户体验
this.setupBasicUX();
}
setupResponsiveDesign() {
// 设置viewport
const viewport = document.createElement('meta');
viewport.name = 'viewport';
viewport.content = 'width=device-width, initial-scale=1.0';
document.head.appendChild(viewport);
// 基础CSS
const style = document.createElement('style');
style.textContent = `
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
}
`;
document.head.appendChild(style);
}
setupBasicPerformance() {
// 图片懒加载
this.setupLazyLoading();
// 资源预加载
this.setupResourcePreloading();
}
setupLazyLoading() {
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
imageObserver.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
}
setupResourcePreloading() {
// 预加载关键资源
const criticalResources = [
'/styles/main.css',
'/scripts/main.js'
];
criticalResources.forEach(resource => {
const link = document.createElement('link');
link.rel = 'preload';
link.href = resource;
link.as = resource.endsWith('.css') ? 'style' : 'script';
document.head.appendChild(link);
});
}
setupBasicUX() {
// 基础加载状态
this.setupLoadingStates();
// 基础错误处理
this.setupErrorHandling();
}
setupLoadingStates() {
// 页面加载指示器
const loader = document.createElement('div');
loader.id = 'page-loader';
loader.innerHTML = '加载中...';
loader.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
`;
document.body.appendChild(loader);
window.addEventListener('load', () => {
setTimeout(() => loader.remove(), 500);
});
}
setupErrorHandling() {
// 全局错误处理
window.addEventListener('error', (event) => {
console.error('全局错误:', event.error);
this.showError('发生了一个错误,请刷新页面重试');
});
// 未处理的Promise拒绝
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的Promise拒绝:', event.reason);
this.showError('网络请求失败,请检查网络连接');
});
}
showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = message;
errorDiv.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: #ff4444;
color: white;
padding: 10px 20px;
border-radius: 4px;
z-index: 10000;
`;
document.body.appendChild(errorDiv);
setTimeout(() => errorDiv.remove(), 5000);
}
// Manifest设置
setupManifest() {
const manifest = document.createElement('link');
manifest.rel = 'manifest';
manifest.href = '/manifest.json';
document.head.appendChild(manifest);
}
// Service Worker设置
setupServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker注册成功');
})
.catch(error => {
console.error('Service Worker注册失败:', error);
});
}
}
// 推送通知设置
setupPushNotifications() {
// 请求通知权限
if (Notification.permission === 'default') {
Notification.requestPermission();
}
}
// 安装提示设置
setupInstallPrompt() {
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
this.showInstallButton(deferredPrompt);
});
}
showInstallButton(deferredPrompt) {
const installButton = document.createElement('button');
installButton.textContent = '安装应用';
installButton.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
z-index: 1000;
`;
installButton.onclick = async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log('安装结果:', outcome);
deferredPrompt = null;
installButton.remove();
}
};
document.body.appendChild(installButton);
}
// 离线支持设置
setupOfflineSupport() {
// 检测网络状态
window.addEventListener('online', () => {
this.showNetworkStatus('网络已连接', 'success');
});
window.addEventListener('offline', () => {
this.showNetworkStatus('网络已断开', 'warning');
});
}
showNetworkStatus(message, type) {
const statusDiv = document.createElement('div');
statusDiv.textContent = message;
statusDiv.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: ${type === 'success' ? '#28a745' : '#ffc107'};
color: ${type === 'success' ? 'white' : 'black'};
padding: 10px 20px;
border-radius: 4px;
z-index: 10000;
`;
document.body.appendChild(statusDiv);
setTimeout(() => statusDiv.remove(), 3000);
}
// 获取功能支持情况
getFeatureSupport() {
return { ...this.features };
}
// 检查特定功能是否支持
isFeatureSupported(feature) {
return this.features[feature] || false;
}
}
// 使用示例
const pwaEnhancement = new PWAProgressiveEnhancement();
// 检查功能支持
console.log('PWA功能支持:', pwaEnhancement.getFeatureSupport());
// 检查特定功能
if (pwaEnhancement.isFeatureSupported('serviceWorker')) {
console.log('支持Service Worker');
}
测试策略
PWA需要在多种环境和设备上进行测试,确保功能正常。
测试维度:
- 功能测试:验证PWA核心功能
- 性能测试:测试加载速度和响应时间
- 兼容性测试:测试不同浏览器和设备
- 离线测试:验证离线功能
- 安装测试:测试安装流程
- 推送测试:验证推送通知功能
测试策略核心代码实现:
// PWA测试管理器
class PWATestManager {
constructor() {
this.testResults = {
functionality: {},
performance: {},
compatibility: {},
offline: {},
installation: {},
pushNotifications: {}
};
this.init();
}
init() {
this.setupTestEnvironment();
this.runAllTests();
}
// 设置测试环境
setupTestEnvironment() {
// 创建测试容器
this.createTestContainer();
// 设置测试数据
this.setupTestData();
// 设置测试工具
this.setupTestTools();
}
createTestContainer() {
const container = document.createElement('div');
container.id = 'pwa-test-container';
container.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
width: 300px;
max-height: 400px;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
padding: 10px;
z-index: 10000;
overflow-y: auto;
font-size: 12px;
`;
document.body.appendChild(container);
}
setupTestData() {
this.testData = {
manifest: {
name: 'PWA Test App',
short_name: 'PWA Test',
start_url: '/',
display: 'standalone',
theme_color: '#2196F3',
background_color: '#ffffff'
},
serviceWorker: {
cacheName: 'pwa-test-cache-v1',
urlsToCache: ['/', '/index.html', '/styles.css', '/app.js']
}
};
}
setupTestTools() {
// 性能测试工具
this.performanceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.recordPerformanceMetric(entry);
}
});
// 网络状态测试
this.networkTest = {
online: navigator.onLine,
connection: navigator.connection || null
};
}
// 运行所有测试
async runAllTests() {
console.log('开始PWA测试...');
await this.testFunctionality();
await this.testPerformance();
await this.testCompatibility();
await this.testOffline();
await this.testInstallation();
await this.testPushNotifications();
this.displayTestResults();
}
// 功能测试
async testFunctionality() {
const tests = {
manifest: this.testManifest(),
serviceWorker: this.testServiceWorker(),
cacheAPI: this.testCacheAPI(),
notifications: this.testNotifications()
};
for (const [testName, testPromise] of Object.entries(tests)) {
try {
this.testResults.functionality[testName] = await testPromise;
} catch (error) {
this.testResults.functionality[testName] = { success: false, error: error.message };
}
}
}
async testManifest() {
const manifestLink = document.querySelector('link[rel="manifest"]');
if (!manifestLink) {
return { success: false, error: 'Manifest链接未找到' };
}
try {
const response = await fetch(manifestLink.href);
const manifest = await response.json();
const requiredFields = ['name', 'short_name', 'start_url', 'display'];
const missingFields = requiredFields.filter(field => !manifest[field]);
if (missingFields.length > 0) {
return { success: false, error: `缺少必需字段: ${missingFields.join(', ')}` };
}
return { success: true, manifest };
} catch (error) {
return { success: false, error: error.message };
}
}
async testServiceWorker() {
if (!('serviceWorker' in navigator)) {
return { success: false, error: '浏览器不支持Service Worker' };
}
try {
const registration = await navigator.serviceWorker.getRegistration();
if (!registration) {
return { success: false, error: 'Service Worker未注册' };
}
return { success: true, registration };
} catch (error) {
return { success: false, error: error.message };
}
}
async testCacheAPI() {
if (!('caches' in window)) {
return { success: false, error: '浏览器不支持Cache API' };
}
try {
const cache = await caches.open('test-cache');
await cache.put('/test', new Response('test data'));
const response = await cache.match('/test');
if (!response) {
return { success: false, error: '缓存操作失败' };
}
await caches.delete('test-cache');
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}
async testNotifications() {
if (!('Notification' in window)) {
return { success: false, error: '浏览器不支持通知' };
}
const permission = Notification.permission;
return { success: true, permission };
}
// 性能测试
async testPerformance() {
const metrics = {
loadTime: performance.timing.loadEventEnd - performance.timing.navigationStart,
domContentLoaded: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart,
firstPaint: this.getFirstPaint(),
firstContentfulPaint: this.getFirstContentfulPaint()
};
this.testResults.performance = metrics;
}
getFirstPaint() {
const paintEntries = performance.getEntriesByType('paint');
const firstPaint = paintEntries.find(entry => entry.name === 'first-paint');
return firstPaint ? firstPaint.startTime : null;
}
getFirstContentfulPaint() {
const paintEntries = performance.getEntriesByType('paint');
const firstContentfulPaint = paintEntries.find(entry => entry.name === 'first-contentful-paint');
return firstContentfulPaint ? firstContentfulPaint.startTime : null;
}
// 兼容性测试
async testCompatibility() {
const features = {
serviceWorker: 'serviceWorker' in navigator,
pushManager: 'PushManager' in window,
notifications: 'Notification' in window,
cacheAPI: 'caches' in window,
manifest: 'manifest' in document,
beforeinstallprompt: 'beforeinstallprompt' in window
};
this.testResults.compatibility = features;
}
// 离线测试
async testOffline() {
const tests = {
onlineStatus: navigator.onLine,
offlineEvent: this.testOfflineEvent(),
cacheStrategy: this.testCacheStrategy()
};
this.testResults.offline = tests;
}
testOfflineEvent() {
return new Promise((resolve) => {
const timeout = setTimeout(() => {
resolve({ success: false, error: '离线事件测试超时' });
}, 5000);
window.addEventListener('offline', () => {
clearTimeout(timeout);
resolve({ success: true });
}, { once: true });
});
}
async testCacheStrategy() {
try {
const cache = await caches.open('test-strategy');
await cache.add('/');
const response = await cache.match('/');
return { success: !!response };
} catch (error) {
return { success: false, error: error.message };
}
}
// 安装测试
async testInstallation() {
const tests = {
manifestValid: await this.testManifest(),
serviceWorkerActive: await this.testServiceWorker(),
installPrompt: this.testInstallPrompt()
};
this.testResults.installation = tests;
}
testInstallPrompt() {
return {
supported: 'beforeinstallprompt' in window,
canInstall: this.canInstall()
};
}
canInstall() {
// 检查安装条件
const hasManifest = !!document.querySelector('link[rel="manifest"]');
const hasServiceWorker = 'serviceWorker' in navigator;
const isHTTPS = location.protocol === 'https:';
return hasManifest && hasServiceWorker && isHTTPS;
}
// 推送通知测试
async testPushNotifications() {
const tests = {
supported: 'PushManager' in window && 'Notification' in window,
permission: Notification.permission,
subscription: await this.testPushSubscription()
};
this.testResults.pushNotifications = tests;
}
async testPushSubscription() {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
return { success: false, error: '不支持推送通知' };
}
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
return { success: true, subscription };
} catch (error) {
return { success: false, error: error.message };
}
}
// 显示测试结果
displayTestResults() {
const container = document.getElementById('pwa-test-container');
if (!container) return;
let html = '<h3>PWA测试结果</h3>';
// 功能测试结果
html += '<h4>功能测试</h4>';
for (const [test, result] of Object.entries(this.testResults.functionality)) {
const status = result.success ? '✅' : '❌';
const error = result.error ? ` (${result.error})` : '';
html += `<div>${status} ${test}${error}</div>`;
}
// 性能测试结果
html += '<h4>性能测试</h4>';
for (const [metric, value] of Object.entries(this.testResults.performance)) {
html += `<div>${metric}: ${value ? value.toFixed(2) + 'ms' : 'N/A'}</div>`;
}
// 兼容性测试结果
html += '<h4>兼容性测试</h4>';
for (const [feature, supported] of Object.entries(this.testResults.compatibility)) {
const status = supported ? '✅' : '❌';
html += `<div>${status} ${feature}</div>`;
}
container.innerHTML = html;
}
// 记录性能指标
recordPerformanceMetric(entry) {
if (!this.testResults.performance[entry.name]) {
this.testResults.performance[entry.name] = [];
}
this.testResults.performance[entry.name].push(entry.startTime);
}
// 获取测试报告
getTestReport() {
return {
timestamp: new Date().toISOString(),
results: this.testResults,
summary: this.generateSummary()
};
}
generateSummary() {
const totalTests = Object.values(this.testResults.functionality).length;
const passedTests = Object.values(this.testResults.functionality).filter(r => r.success).length;
return {
totalTests,
passedTests,
failedTests: totalTests - passedTests,
passRate: (passedTests / totalTests * 100).toFixed(2) + '%'
};
}
}
// 使用示例
const pwaTest = new PWATestManager();
// 获取测试报告
const report = pwaTest.getTestReport();
console.log('PWA测试报告:', report);
监控和分析
PWA需要持续监控和分析,了解用户行为和应用性能。
监控指标:
- 安装率:PWA的安装转化率
- 使用频率:用户使用PWA的频率
- 离线使用:离线场景下的使用情况
- 性能指标:加载时间、交互响应等
- 错误率:应用错误和崩溃率
- 用户反馈:用户对PWA的反馈
监控和分析核心代码实现:
// PWA监控分析管理器
class PWAMonitoringManager {
constructor(options = {}) {
this.options = {
apiEndpoint: '/api/analytics',
batchSize: 10,
flushInterval: 30000,
...options
};
this.events = [];
this.metrics = {};
this.init();
}
init() {
this.setupPerformanceMonitoring();
this.setupUserBehaviorTracking();
this.setupErrorTracking();
this.setupOfflineTracking();
this.setupInstallTracking();
this.startBatchFlush();
}
// 性能监控
setupPerformanceMonitoring() {
// 监控Core Web Vitals
this.monitorCoreWebVitals();
// 监控资源加载
this.monitorResourceLoading();
// 监控用户交互
this.monitorUserInteractions();
}
monitorCoreWebVitals() {
// 监控LCP (Largest Contentful Paint)
if ('PerformanceObserver' in window) {
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.recordMetric('lcp', lastEntry.startTime);
});
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
// 监控FID (First Input Delay)
const fidObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.recordMetric('fid', entry.processingStart - entry.startTime);
}
});
fidObserver.observe({ entryTypes: ['first-input'] });
// 监控CLS (Cumulative Layout Shift)
const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
this.recordMetric('cls', entry.value);
}
}
});
clsObserver.observe({ entryTypes: ['layout-shift'] });
}
}
monitorResourceLoading() {
// 监控资源加载时间
const resourceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'resource') {
this.recordMetric('resource_load_time', {
name: entry.name,
duration: entry.duration,
size: entry.transferSize,
type: entry.initiatorType
});
}
}
});
resourceObserver.observe({ entryTypes: ['resource'] });
}
monitorUserInteractions() {
// 监控点击事件
document.addEventListener('click', (event) => {
this.recordEvent('click', {
target: event.target.tagName,
id: event.target.id,
className: event.target.className,
x: event.clientX,
y: event.clientY
});
});
// 监控滚动事件
let scrollTimeout;
window.addEventListener('scroll', () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
this.recordEvent('scroll', {
scrollY: window.scrollY,
scrollX: window.scrollX
});
}, 100);
});
}
// 用户行为跟踪
setupUserBehaviorTracking() {
// 跟踪页面访问
this.trackPageView();
// 跟踪会话
this.trackSession();
// 跟踪用户操作
this.trackUserActions();
}
trackPageView() {
this.recordEvent('page_view', {
url: window.location.href,
title: document.title,
referrer: document.referrer,
timestamp: Date.now()
});
}
trackSession() {
const sessionId = this.getOrCreateSessionId();
this.recordEvent('session_start', {
sessionId,
timestamp: Date.now(),
userAgent: navigator.userAgent,
language: navigator.language
});
}
getOrCreateSessionId() {
let sessionId = sessionStorage.getItem('pwa_session_id');
if (!sessionId) {
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
sessionStorage.setItem('pwa_session_id', sessionId);
}
return sessionId;
}
trackUserActions() {
// 跟踪表单提交
document.addEventListener('submit', (event) => {
this.recordEvent('form_submit', {
formId: event.target.id,
formClass: event.target.className,
action: event.target.action
});
});
// 跟踪链接点击
document.addEventListener('click', (event) => {
if (event.target.tagName === 'A') {
this.recordEvent('link_click', {
href: event.target.href,
text: event.target.textContent
});
}
});
}
// 错误跟踪
setupErrorTracking() {
// 全局错误处理
window.addEventListener('error', (event) => {
this.recordError('javascript_error', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack
});
});
// Promise拒绝处理
window.addEventListener('unhandledrejection', (event) => {
this.recordError('promise_rejection', {
reason: event.reason,
stack: event.reason?.stack
});
});
}
// 离线跟踪
setupOfflineTracking() {
// 跟踪网络状态变化
window.addEventListener('online', () => {
this.recordEvent('network_online', {
timestamp: Date.now()
});
});
window.addEventListener('offline', () => {
this.recordEvent('network_offline', {
timestamp: Date.now()
});
});
// 跟踪离线使用情况
this.trackOfflineUsage();
}
trackOfflineUsage() {
// 检测离线状态
if (!navigator.onLine) {
this.recordEvent('offline_usage', {
timestamp: Date.now(),
duration: this.calculateOfflineDuration()
});
}
}
calculateOfflineDuration() {
const offlineStart = localStorage.getItem('offline_start');
if (offlineStart) {
return Date.now() - parseInt(offlineStart);
}
return 0;
}
// 安装跟踪
setupInstallTracking() {
// 跟踪安装提示
window.addEventListener('beforeinstallprompt', (event) => {
this.recordEvent('install_prompt_shown', {
timestamp: Date.now()
});
});
// 跟踪安装完成
window.addEventListener('appinstalled', (event) => {
this.recordEvent('app_installed', {
timestamp: Date.now()
});
});
// 跟踪安装状态
this.trackInstallStatus();
}
trackInstallStatus() {
const isInstalled = window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true;
this.recordEvent('install_status', {
isInstalled,
timestamp: Date.now()
});
}
// 记录事件
recordEvent(eventName, data) {
const event = {
type: 'event',
name: eventName,
data: data,
timestamp: Date.now(),
sessionId: this.getOrCreateSessionId()
};
this.events.push(event);
this.flushIfNeeded();
}
// 记录指标
recordMetric(metricName, value) {
if (!this.metrics[metricName]) {
this.metrics[metricName] = [];
}
this.metrics[metricName].push({
value,
timestamp: Date.now()
});
}
// 记录错误
recordError(errorType, data) {
const error = {
type: 'error',
errorType,
data: data,
timestamp: Date.now(),
sessionId: this.getOrCreateSessionId()
};
this.events.push(error);
this.flushIfNeeded();
}
// 批量刷新
startBatchFlush() {
setInterval(() => {
this.flush();
}, this.options.flushInterval);
}
flushIfNeeded() {
if (this.events.length >= this.options.batchSize) {
this.flush();
}
}
async flush() {
if (this.events.length === 0) return;
const eventsToSend = [...this.events];
this.events = [];
try {
await fetch(this.options.apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
events: eventsToSend,
metrics: this.metrics,
timestamp: Date.now()
})
});
console.log('监控数据发送成功');
} catch (error) {
console.error('监控数据发送失败:', error);
// 重新添加到队列
this.events.unshift(...eventsToSend);
}
}
// 获取分析报告
getAnalyticsReport() {
return {
events: this.events,
metrics: this.metrics,
sessionId: this.getOrCreateSessionId(),
timestamp: Date.now()
};
}
// 导出数据
exportData() {
const data = this.getAnalyticsReport();
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `pwa-analytics-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
}
}
// 使用示例
const pwaMonitoring = new PWAMonitoringManager({
apiEndpoint: '/api/analytics',
batchSize: 20,
flushInterval: 60000
});
// 获取分析报告
const report = pwaMonitoring.getAnalyticsReport();
console.log('PWA分析报告:', report);
// 导出数据
pwaMonitoring.exportData();
5. PWA工具和框架
开发工具
PWA开发需要一系列工具来简化开发流程。
核心工具:
- Workbox:Google开发的PWA工具库
- Lighthouse:PWA质量检测工具
- PWA Builder:微软的PWA构建工具
- Web App Manifest Generator:Manifest生成器
- Service Worker Generator:Service Worker生成器
开发工具核心代码实现:
// PWA开发工具集
class PWADevTools {
constructor() {
this.manifest = null;
this.serviceWorker = null;
this.init();
}
init() {
this.setupManifestGenerator();
this.setupServiceWorkerGenerator();
this.setupLighthouseIntegration();
this.setupWorkboxIntegration();
}
// Manifest生成器
setupManifestGenerator() {
this.manifestGenerator = {
generate: (options) => {
const defaultManifest = {
name: 'PWA App',
short_name: 'PWA',
description: 'Progressive Web App',
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#000000',
orientation: 'portrait',
scope: '/',
lang: 'zh-CN',
icons: []
};
return { ...defaultManifest, ...options };
},
validate: (manifest) => {
const errors = [];
const warnings = [];
// 必需字段检查
const requiredFields = ['name', 'short_name', 'start_url', 'display'];
requiredFields.forEach(field => {
if (!manifest[field]) {
errors.push(`缺少必需字段: ${field}`);
}
});
// 图标检查
if (!manifest.icons || manifest.icons.length === 0) {
warnings.push('建议添加应用图标');
}
// 显示模式检查
const validDisplays = ['fullscreen', 'standalone', 'minimal-ui', 'browser'];
if (!validDisplays.includes(manifest.display)) {
errors.push(`无效的显示模式: ${manifest.display}`);
}
return { errors, warnings, valid: errors.length === 0 };
},
generateIcons: (baseIcon, sizes = [72, 96, 128, 144, 152, 192, 384, 512]) => {
return sizes.map(size => ({
src: `${baseIcon}?size=${size}`,
sizes: `${size}x${size}`,
type: 'image/png',
purpose: 'any maskable'
}));
}
};
}
// Service Worker生成器
setupServiceWorkerGenerator() {
this.serviceWorkerGenerator = {
generate: (options = {}) => {
const {
cacheName = 'pwa-cache-v1',
urlsToCache = ['/', '/index.html', '/styles.css', '/app.js'],
offlinePage = '/offline.html'
} = options;
return `
// Service Worker - 自动生成
const CACHE_NAME = '${cacheName}';
const urlsToCache = ${JSON.stringify(urlsToCache, null, 2)};
// 安装事件
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('缓存已打开');
return cache.addAll(urlsToCache);
})
);
});
// 激活事件
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);
}
})
);
})
);
});
// 获取事件
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
if (response) {
return response;
}
return fetch(event.request).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(() => {
return caches.match('${offlinePage}');
})
);
});
`;
},
register: (swPath) => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(swPath)
.then((registration) => {
console.log('Service Worker注册成功:', registration);
})
.catch((error) => {
console.error('Service Worker注册失败:', error);
});
}
}
};
}
// Lighthouse集成
setupLighthouseIntegration() {
this.lighthouseIntegration = {
runAudit: async (url) => {
try {
const response = await fetch('/api/lighthouse', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const result = await response.json();
return this.parseLighthouseResults(result);
} catch (error) {
console.error('Lighthouse审计失败:', error);
return null;
}
},
parseLighthouseResults: (results) => {
const categories = results.lhr.categories;
const audits = results.lhr.audits;
return {
performance: {
score: categories.performance.score * 100,
metrics: {
fcp: audits['first-contentful-paint']?.displayValue,
lcp: audits['largest-contentful-paint']?.displayValue,
fid: audits['max-potential-fid']?.displayValue,
cls: audits['cumulative-layout-shift']?.displayValue
}
},
pwa: {
score: categories.pwa.score * 100,
checks: {
installable: audits['installable-manifest']?.score === 1,
serviceWorker: audits['service-worker']?.score === 1,
offline: audits['offline-start-url']?.score === 1
}
}
};
}
};
}
// Workbox集成
setupWorkboxIntegration() {
this.workboxIntegration = {
generateConfig: (options = {}) => {
const {
globDirectory = 'dist',
globPatterns = ['**/*.{js,css,html,png,jpg,jpeg,svg}'],
swDest = 'sw.js'
} = options;
return {
globDirectory,
globPatterns,
swDest,
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\//,
handler: 'StaleWhileRevalidate',
options: { cacheName: 'google-fonts-stylesheets' }
},
{
urlPattern: /^https:\/\/fonts\.gstatic\.com\//,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-webfonts',
expiration: { maxEntries: 30, maxAgeSeconds: 60 * 60 * 24 * 365 }
}
}
]
};
}
};
}
// 生成完整的PWA配置
generatePWAConfig(options = {}) {
const {
appName = 'PWA App',
appShortName = 'PWA',
appDescription = 'Progressive Web App',
startUrl = '/',
themeColor = '#000000',
backgroundColor = '#ffffff',
iconPath = '/icon.png'
} = options;
const manifest = this.manifestGenerator.generate({
name: appName,
short_name: appShortName,
description: appDescription,
start_url: startUrl,
theme_color: themeColor,
background_color: backgroundColor,
icons: this.manifestGenerator.generateIcons(iconPath)
});
const serviceWorker = this.serviceWorkerGenerator.generate({
cacheName: `${appShortName.toLowerCase()}-cache-v1`,
urlsToCache: [startUrl, '/index.html', '/styles.css', '/app.js']
});
return { manifest, serviceWorker };
}
}
// 使用示例
const pwaDevTools = new PWADevTools();
const config = pwaDevTools.generatePWAConfig({
appName: '我的PWA应用',
appShortName: 'MyPWA',
themeColor: '#2196F3'
});
console.log('PWA配置:', config);
框架支持
现代前端框架都提供了PWA支持。
框架PWA支持:
- React:通过Create React App和PWA模板
- Vue:通过Vue CLI和PWA插件
- Angular:通过Angular Service Worker
- Svelte:通过SvelteKit和PWA插件
框架PWA核心代码实现:
// React PWA实现
// App.js
import React, { useEffect, useState } from 'react';
import './App.css';
function App() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const [installPrompt, setInstallPrompt] = useState(null);
const [isInstalled, setIsInstalled] = useState(false);
useEffect(() => {
// 注册Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => console.log('SW registered'))
.catch(error => console.log('SW registration failed'));
}
// 监听网络状态
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
// 监听安装提示
const handleBeforeInstallPrompt = (e) => {
e.preventDefault();
setInstallPrompt(e);
};
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
// 检查是否已安装
const checkInstalled = () => {
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const isInStandaloneMode = ('standalone' in window.navigator) && window.navigator.standalone;
setIsInstalled(isStandalone || (isIOS && isInStandaloneMode));
};
checkInstalled();
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
};
}, []);
const handleInstall = async () => {
if (installPrompt) {
const result = await installPrompt.prompt();
console.log('安装结果:', result);
setInstallPrompt(null);
}
};
return (
<div className="App">
<header className="App-header">
<h1>React PWA</h1>
<p>网络状态: {isOnline ? '在线' : '离线'}</p>
<p>安装状态: {isInstalled ? '已安装' : '未安装'}</p>
{installPrompt && (
<button onClick={handleInstall} className="install-button">
安装应用
</button>
)}
</header>
</div>
);
}
export default App;
// Vue PWA实现
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import './registerServiceWorker';
const app = createApp(App);
// PWA安装提示
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
app.config.globalProperties.$installPrompt = deferredPrompt;
});
app.mount('#app');
// Vue组件
// App.vue
<template>
<div id="app">
<header>
<h1>Vue PWA</h1>
<p>网络状态: {{ isOnline ? '在线' : '离线' }}</p>
<button v-if="showInstallButton" @click="installApp">
安装应用
</button>
</header>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
isOnline: navigator.onLine,
showInstallButton: false
};
},
mounted() {
// 监听网络状态
window.addEventListener('online', this.handleOnline);
window.addEventListener('offline', this.handleOffline);
// 检查安装提示
this.checkInstallPrompt();
},
methods: {
handleOnline() {
this.isOnline = true;
},
handleOffline() {
this.isOnline = false;
},
checkInstallPrompt() {
if (this.$installPrompt) {
this.showInstallButton = true;
}
},
async installApp() {
if (this.$installPrompt) {
const result = await this.$installPrompt.prompt();
console.log('安装结果:', result);
this.showInstallButton = false;
}
}
}
};
</script>
// Angular PWA实现
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
import { interval } from 'rxjs';
@Component({
selector: 'app-root',
template: `
<div class="app">
<h1>Angular PWA</h1>
<p>网络状态: {{ isOnline ? '在线' : '离线' }}</p>
<p>应用状态: {{ appStatus }}</p>
<button *ngIf="showInstallButton" (click)="installApp()">
安装应用
</button>
</div>
`
})
export class AppComponent implements OnInit {
isOnline = navigator.onLine;
appStatus = '运行中';
showInstallButton = false;
private deferredPrompt: any;
constructor(private swUpdate: SwUpdate) {}
ngOnInit() {
// 检查Service Worker更新
if (this.swUpdate.isEnabled) {
this.swUpdate.available.subscribe(() => {
if (confirm('发现新版本,是否更新?')) {
window.location.reload();
}
});
}
// 监听网络状态
window.addEventListener('online', () => this.isOnline = true);
window.addEventListener('offline', () => this.isOnline = false);
// 监听安装提示
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
this.deferredPrompt = e;
this.showInstallButton = true;
});
// 定期检查更新
interval(60000).subscribe(() => {
this.swUpdate.checkForUpdate();
});
}
async installApp() {
if (this.deferredPrompt) {
const result = await this.deferredPrompt.prompt();
console.log('安装结果:', result);
this.showInstallButton = false;
}
}
}
// Svelte PWA实现
// App.svelte
<script>
import { onMount } from 'svelte';
let isOnline = navigator.onLine;
let showInstallButton = false;
let deferredPrompt = null;
onMount(() => {
// 注册Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
// 监听网络状态
const handleOnline = () => isOnline = true;
const handleOffline = () => isOnline = false;
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
// 监听安装提示
const handleBeforeInstallPrompt = (e) => {
e.preventDefault();
deferredPrompt = e;
showInstallButton = true;
};
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
};
});
async function installApp() {
if (deferredPrompt) {
const result = await deferredPrompt.prompt();
console.log('安装结果:', result);
showInstallButton = false;
}
}
</script>
<main>
<h1>Svelte PWA</h1>
<p>网络状态: {isOnline ? '在线' : '离线'}</p>
<button on:click={installApp} class:show={showInstallButton}>
安装应用
</button>
</main>
<style>
.show {
display: block;
}
</style>
- Next.js:内置PWA支持
6. PWA未来发展趋势
新技术集成
PWA正在集成更多新技术,提供更丰富的功能。
新兴技术:
- WebAssembly:提升PWA性能
- Web Components:增强组件化开发
- WebRTC:支持实时通信
- WebXR:支持AR/VR体验
- Web Share API:增强分享功能
新技术集成核心代码实现:
// WebAssembly + PWA集成
class PWAPerformanceManager {
constructor() {
this.wasmModule = null;
this.init();
}
async init() {
await this.loadWasmModule();
this.setupPerformanceMonitoring();
}
async loadWasmModule() {
try {
const wasmBytes = await fetch('/wasm/fib.wasm').then(r => r.arrayBuffer());
this.wasmModule = await WebAssembly.instantiate(wasmBytes);
console.log('WebAssembly模块加载成功');
} catch (error) {
console.error('WebAssembly加载失败:', error);
}
}
calculateFibonacci(n) {
if (this.wasmModule) {
return this.wasmModule.instance.exports.fib(n);
}
return this.fibonacciJS(n);
}
fibonacciJS(n) {
if (n <= 1) return n;
return this.fibonacciJS(n - 1) + this.fibonacciJS(n - 2);
}
setupPerformanceMonitoring() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name.includes('wasm')) {
console.log('WASM性能指标:', entry);
}
}
});
observer.observe({ entryTypes: ['measure'] });
}
}
// WebRTC + PWA集成
class PWARTCManager {
constructor() {
this.localStream = null;
this.peerConnection = null;
this.init();
}
async init() {
await this.setupMediaDevices();
this.setupPeerConnection();
}
async setupMediaDevices() {
try {
this.localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
console.log('媒体设备获取成功');
} catch (error) {
console.error('媒体设备获取失败:', error);
}
}
setupPeerConnection() {
const configuration = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
};
this.peerConnection = new RTCPeerConnection(configuration);
if (this.localStream) {
this.localStream.getTracks().forEach(track => {
this.peerConnection.addTrack(track, this.localStream);
});
}
this.peerConnection.ontrack = (event) => {
const remoteStream = event.streams[0];
this.displayRemoteStream(remoteStream);
};
}
async createOffer() {
const offer = await this.peerConnection.createOffer();
await this.peerConnection.setLocalDescription(offer);
return offer;
}
displayRemoteStream(stream) {
const videoElement = document.getElementById('remoteVideo');
if (videoElement) {
videoElement.srcObject = stream;
}
}
}
// Web Share API + PWA集成
class PWAShareManager {
constructor() {
this.init();
}
init() {
this.setupShareButton();
}
setupShareButton() {
if (navigator.share) {
const shareButton = document.createElement('button');
shareButton.textContent = '分享';
shareButton.onclick = () => this.shareContent();
document.body.appendChild(shareButton);
}
}
async shareContent() {
try {
await navigator.share({
title: 'PWA应用',
text: '这是一个功能强大的PWA应用',
url: window.location.href
});
console.log('分享成功');
} catch (error) {
console.error('分享失败:', error);
}
}
}
// Web Components + PWA集成
class PWACustomElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
background: #f9f9f9;
}
h3 {
margin: 0 0 10px 0;
color: #333;
}
p {
margin: 0;
color: #666;
}
</style>
<h3>PWA自定义组件</h3>
<p>这是一个使用Web Components技术构建的PWA组件</p>
`;
}
connectedCallback() {
console.log('PWA组件已连接');
}
disconnectedCallback() {
console.log('PWA组件已断开');
}
}
// 注册自定义元素
customElements.define('pwa-custom-element', PWACustomElement);
// 使用示例
const pwaPerformance = new PWAPerformanceManager();
const pwaRTC = new PWARTCManager();
const pwaShare = new PWAShareManager();
// 性能测试
console.log('斐波那契数列(40):', pwaPerformance.calculateFibonacci(40));
平台支持扩展
PWA正在扩展到更多平台和设备。
平台支持:
- 桌面应用:Windows、macOS、Linux
- 移动应用:iOS、Android
- 智能电视:Tizen、WebOS
- 车载系统:Android Auto、CarPlay
- 可穿戴设备:智能手表、智能眼镜
平台支持核心代码实现:
// 跨平台PWA管理器
class CrossPlatformPWAManager {
constructor() {
this.platform = this.detectPlatform();
this.init();
}
init() {
this.setupPlatformSpecificFeatures();
this.setupAdaptiveUI();
this.setupPlatformNotifications();
}
detectPlatform() {
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.includes('windows')) return 'windows';
if (userAgent.includes('mac')) return 'macos';
if (userAgent.includes('linux')) return 'linux';
if (userAgent.includes('iphone') || userAgent.includes('ipad')) return 'ios';
if (userAgent.includes('android')) return 'android';
if (userAgent.includes('tizen')) return 'tizen';
if (userAgent.includes('webos')) return 'webos';
return 'unknown';
}
setupPlatformSpecificFeatures() {
switch (this.platform) {
case 'windows':
this.setupWindowsFeatures();
break;
case 'macos':
this.setupMacOSFeatures();
break;
case 'ios':
this.setupiOSFeatures();
break;
case 'android':
this.setupAndroidFeatures();
break;
case 'tizen':
this.setupTizenFeatures();
break;
case 'webos':
this.setupWebOSFeatures();
break;
}
}
setupWindowsFeatures() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
this.setupWindowsTiles();
}
setupMacOSFeatures() {
this.setupMacOSNotifications();
this.setupMacOSShortcuts();
}
setupiOSFeatures() {
this.setupiOSStatusBar();
this.setupiOSSplashScreen();
this.setupiOSHomeScreen();
}
setupAndroidFeatures() {
this.setupAndroidShortcuts();
this.setupAndroidNotifications();
this.setupAndroidThemes();
}
setupTizenFeatures() {
this.setupTizenRemoteControl();
this.setupTizenTVUI();
}
setupWebOSFeatures() {
this.setupWebOSRemoteControl();
this.setupWebOSTVUI();
}
setupAdaptiveUI() {
const viewport = this.getViewportInfo();
document.body.classList.add(`platform-${this.platform}`);
document.body.classList.add(`viewport-${viewport.type}`);
this.setupResponsiveBreakpoints(viewport);
}
getViewportInfo() {
const width = window.innerWidth;
const height = window.innerHeight;
if (width >= 1200) return { type: 'desktop', width, height };
if (width >= 768) return { type: 'tablet', width, height };
if (width >= 480) return { type: 'mobile', width, height };
return { type: 'small', width, height };
}
setupResponsiveBreakpoints(viewport) {
const breakpoints = {
desktop: '1200px',
tablet: '768px',
mobile: '480px'
};
document.documentElement.style.setProperty('--breakpoint', breakpoints[viewport.type]);
}
setupPlatformNotifications() {
if ('Notification' in window) {
this.requestNotificationPermission();
}
}
async requestNotificationPermission() {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
this.showWelcomeNotification();
}
}
showWelcomeNotification() {
const notification = new Notification('PWA应用', {
body: '欢迎使用PWA应用!',
icon: '/icon-192.png',
badge: '/badge-72.png',
tag: 'welcome',
requireInteraction: true
});
notification.onclick = () => {
window.focus();
notification.close();
};
}
setupWindowsTiles() {
const manifest = {
name: 'PWA应用',
short_name: 'PWA',
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#000000',
icons: [
{
src: '/icon-192.png',
sizes: '192x192',
type: 'image/png'
}
]
};
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.showNotification('PWA应用', {
body: '应用已准备就绪',
icon: '/icon-192.png',
badge: '/badge-72.png',
tag: 'ready'
});
});
}
}
setupMacOSNotifications() {
const style = document.createElement('style');
style.textContent = `
.platform-macos .notification {
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
`;
document.head.appendChild(style);
}
setupiOSStatusBar() {
const meta = document.createElement('meta');
meta.name = 'apple-mobile-web-app-status-bar-style';
meta.content = 'default';
document.head.appendChild(meta);
}
setupiOSSplashScreen() {
const meta = document.createElement('meta');
meta.name = 'apple-mobile-web-app-capable';
meta.content = 'yes';
document.head.appendChild(meta);
}
setupiOSHomeScreen() {
const link = document.createElement('link');
link.rel = 'apple-touch-icon';
link.href = '/icon-180.png';
document.head.appendChild(link);
}
setupAndroidShortcuts() {
const manifest = {
shortcuts: [
{
name: '快速操作',
short_name: '快速',
description: '快速访问常用功能',
url: '/quick-action',
icons: [{ src: '/shortcut-icon.png', sizes: '96x96' }]
}
]
};
this.updateManifest(manifest);
}
setupAndroidNotifications() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.showNotification('PWA应用', {
body: 'Android通知',
icon: '/icon-192.png',
badge: '/badge-72.png',
actions: [
{ action: 'open', title: '打开' },
{ action: 'close', title: '关闭' }
]
});
});
}
}
setupAndroidThemes() {
const meta = document.createElement('meta');
meta.name = 'theme-color';
meta.content = '#000000';
document.head.appendChild(meta);
}
setupTizenRemoteControl() {
document.addEventListener('keydown', (event) => {
switch (event.key) {
case 'ArrowUp':
this.handleRemoteUp();
break;
case 'ArrowDown':
this.handleRemoteDown();
break;
case 'ArrowLeft':
this.handleRemoteLeft();
break;
case 'ArrowRight':
this.handleRemoteRight();
break;
case 'Enter':
this.handleRemoteSelect();
break;
}
});
}
setupTizenTVUI() {
const style = document.createElement('style');
style.textContent = `
.platform-tizen {
font-size: 24px;
line-height: 1.5;
}
.platform-tizen button {
min-height: 60px;
min-width: 120px;
font-size: 20px;
}
`;
document.head.appendChild(style);
}
setupWebOSRemoteControl() {
document.addEventListener('keydown', (event) => {
if (event.key === 'Backspace') {
this.handleWebOSBack();
}
});
}
setupWebOSTVUI() {
const style = document.createElement('style');
style.textContent = `
.platform-webos {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
`;
document.head.appendChild(style);
}
updateManifest(manifest) {
const manifestElement = document.querySelector('link[rel="manifest"]');
if (manifestElement) {
manifestElement.href = 'data:application/json,' + encodeURIComponent(JSON.stringify(manifest));
}
}
handleRemoteUp() { console.log('遥控器上键'); }
handleRemoteDown() { console.log('遥控器下键'); }
handleRemoteLeft() { console.log('遥控器左键'); }
handleRemoteRight() { console.log('遥控器右键'); }
handleRemoteSelect() { console.log('遥控器选择键'); }
handleWebOSBack() { console.log('webOS返回键'); }
}
// 使用示例
const crossPlatformPWA = new CrossPlatformPWAManager();
console.log('当前平台:', crossPlatformPWA.platform);
通过以上PWA技术方案,可以构建出功能强大、用户体验优秀的现代Web应用,为用户提供接近原生应用的体验。