⚡ PWA 渐进式 Web 应用
PWA(Progressive Web App)让 Web 应用具备类似原生 App 的能力:离线访问、桌面图标、推送通知、后台同步。在移动端,iOS 从 11.3 开始支持 Service Worker。
PWA 核心技术栈
| 技术 | 作用 | 兼容性 |
|---|---|---|
| Service Worker | 离线缓存、后台同步、推送通知 | iOS 11.3+, Android Chrome 全支持 |
| Web App Manifest | 添加到桌面、图标、启动画面、全屏模式 | Android Chrome 完整支持,iOS Safari 部分支持 |
| HTTPS | PWA 强制要求 | Service Worker 只能在 HTTPS 或 localhost 下注册 |
| Cache API | 编程式缓存管理 | 与 Service Worker 兼容性相同 |
| IndexedDB | 结构化离线数据存储 | iOS 8+, Android 全支持 |
1. Web App Manifest(添加到桌面)
<!-- ===== manifest.json(放在项目根目录)===== -->
{
"name": "我的应用",
"short_name": "MyApp",
"description": "一个移动端 Web 应用",
"start_url": "/?utm_source=pwa",
"scope": "/",
"display": "standalone", // standalone: 隐藏浏览器UI
// fullscreen: 完全全屏
// minimal-ui: 最小浏览器UI
// browser: 普通浏览器
"orientation": "portrait", // 锁定屏幕方向
"background_color": "#ffffff",
"theme_color": "#4A90D9",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable" // maskable: 适配 Android 自适应图标
}
],
"screenshots": [ // 应用商店展示图
{
"src": "/screenshots/home.png",
"sizes": "750x1334",
"type": "image/png"
}
],
"categories": ["utilities"],
"lang": "zh-CN"
}
<!-- HTML 中引入 -->
<link rel="manifest" href="/manifest.json">
<!-- iOS 特殊 meta 标签(iOS 不读取 manifest.json) -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="我的应用">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-180.png">
<link rel="apple-touch-startup-image" href="/splash/splash-750x1334.png">
2. Service Worker(离线缓存)
// ===== sw.js(Service Worker 文件)=====
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
'/',
'/css/style.css',
'/js/main.js',
'/offline.html', // 离线兜底页
];
// ① install: 预缓存关键资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(urlsToCache);
})
);
// 立即激活,不等待旧 SW 释放
self.skipWaiting();
});
// ② activate: 清理旧版本缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
})
);
// 立即接管所有页面
self.clients.claim();
});
// ③ fetch: 缓存策略 - 网络优先,缓存兜底
self.addEventListener('fetch', (event) => {
// 只处理 GET 请求
if (event.request.method !== 'GET') return;
event.respondWith(
fetch(event.request)
.then((response) => {
// 网络成功:缓存响应副本
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => {
// 网络失败:返回缓存
return caches.match(event.request).then((cachedResponse) => {
return cachedResponse || caches.match('/offline.html');
});
})
);
});
// ===== 注册 Service Worker(在主 JS 中)=====
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then((registration) => {
console.log('SW 注册成功:', registration.scope);
// 监听更新
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// 有新版本可用,提示用户刷新
if (confirm('有新版本可用,是否刷新?')) {
window.location.reload();
}
}
});
});
}).catch((err) => {
console.error('SW 注册失败:', err);
});
});
}
3. 缓存策略对比
| 策略 | 模式 | 适用场景 |
|---|---|---|
| Cache First(缓存优先) | 先查缓存,未命中再网络 | 静态资源(CSS/JS/图片/字体),版本号控制更新 |
| Network First(网络优先) | 先请求网络,失败用缓存 | API 数据、需要实时性的内容 |
| Stale While Revalidate | 返回缓存,后台更新 | 不要求即时性的内容,用户体验最好 |
| Network Only | 仅网络请求 | 不需要缓存的请求(如上报、统计) |
| Cache Only | 仅从缓存获取 | 离线应用的纯本地资源 |
Stale While Revalidate 策略实现
// ===== 最佳用户体验策略:先返回缓存,后台更新 =====
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then((cache) => {
return cache.match(event.request).then((cachedResponse) => {
// 后台发起网络请求更新缓存
const fetchPromise = fetch(event.request).then((networkResponse) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
// 立即返回缓存(如果有),否则等网络
return cachedResponse || fetchPromise;
});
})
);
});
4. iOS 添加到桌面引导
// ===== iOS Safari 添加到桌面引导 =====
// iOS 不支持自动弹出安装提示,需要手动引导用户
function showInstallGuide() {
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const isStandalone = window.navigator.standalone; // iOS 已添加到桌面
if (isIOS && !isStandalone) {
// 显示引导浮层
const guide = document.createElement('div');
guide.className = 'ios-install-guide';
guide.innerHTML = `
<div class="guide-content">
<p>点击下方 <strong>分享按钮</strong></p>
<p>选择 <strong>添加到主屏幕</strong></p>
<span class="guide-arrow">↓</span>
<button onclick="this.parentElement.parentElement.remove()">知道了</button>
</div>
`;
document.body.appendChild(guide);
}
}
// CSS 样式
.ios-install-guide {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.85);
color: #fff;
padding: 16px 24px;
border-radius: 12px;
z-index: 9999;
text-align: center;
}
.guide-arrow {
font-size: 24px;
display: block;
animation: bounce 1s infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(8px); }
}
5. PWA 离线检测与提示
// ===== 网络状态监听 =====
function initNetworkListener() {
const updateOnlineStatus = () => {
const isOnline = navigator.onLine;
const toast = document.getElementById('network-toast');
if (!isOnline) {
toast.textContent = '📡 当前处于离线状态,部分功能可能不可用';
toast.classList.add('show');
} else {
toast.textContent = '✅ 网络已恢复';
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2000);
}
};
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
}
// HTML 结构
<div id="network-toast" style="
position:fixed; top:0; left:0; right:0;
background:#ff6b6b; color:#fff; text-align:center;
padding:8px; transform:translateY(-100%);
transition:transform 0.3s; z-index:9999;
"></div>
<style>
#network-toast.show { transform: translateY(0); }
</style>
💡 PWA 移动端实践建议
- iOS 限制:iOS 的 Service Worker 缓存上限约 50MB,超出会被系统清理
- 缓存策略分层:HTML 用 Network First,CSS/JS/字体用 Cache First(带版本号),API 用 Stale While Revalidate
- App 壳方案:PWA 在 iOS 体验不如 Android,可考虑 Trusted Web Activity(TWA)上架 Google Play
- 渐进增强:先确保无 SW 时页面正常工作,PWA 能力作为增强项
- 推送通知:iOS 16.4+ 才开始支持 Web Push,Android 完整支持
- Workbox:生产环境推荐使用 Google Workbox 库管理 SW,避免手动处理缓存细节