前言:为什么你需要关注 PWA?

Progressive Web App(PWA)自 Google 在 2015 年提出以来,已经从一项前沿实验技术演变为现代 Web 开发的基础能力。截至 2026 年,主流浏览器对 PWA 三大核心技术——Service Worker、Web App Manifest 和 Cache API——的支持率已超过 96%。
PWA 最大的价值在于:它让 Web 应用获得了过去只有原生 App 才有的能力——离线访问、后台同步、桌面快捷方式和推送通知。但不同于原生 App,PWA 不需要经过应用商店审核,没有安装包体量限制,更新也无需用户手动操作。
本文将从 Service Worker 的生命周期开始,带你一步步构建一个功能完整的 PWA 应用,涵盖离线缓存策略、动态资源管理、消息推送和后台同步等核心能力。所有代码均可直接运行,并提供完整的生产级配置建议。
Service Worker 基础:生命周期与注册
Service Worker 本质上是一个运行在浏览器后台的 JavaScript 文件,独立于网页主线程。它扮演着网络代理的角色——拦截页面的所有网络请求,并根据你编写的逻辑决定返回缓存内容、从网络获取或两者组合。
Service Worker 的限制
在开始之前,有几个关键限制需要了解:
- 仅支持 HTTPS:Service Worker 只能在 HTTPS 环境下注册(localhost 开发环境除外)
- 没有 DOM 访问权限:无法直接操作页面元素
- 独立生命周期:不与任何页面绑定,浏览器可以随时终止它
- 异步 API 为主:大量使用 Promise 和 async/await
注册 Service Worker
注册通常在页面主脚本中完成,以下是推荐的生产级注册代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 // app.js - 主线程注册 Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register(
'/service-worker.js',
{
scope: '/', // 控制哪些路径下的请求
updateViaCache: 'none' // 不通过 HTTP 缓存检查更新
}
);
console.log('Service Worker 注册成功,作用域:', registration.scope);
// 监听更新事件
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);
}
});
} else {
console.warn('当前浏览器不支持 Service Worker');
}
生命周期详解
Service Worker 的生命周期包含以下阶段:
| 阶段 | 触发事件 | 说明 | ||
|---|---|---|---|---|
| 下载 | 浏览器下载 .js 文件 | 注册后立即触发,每次页面导航或 24 小时后检查更新 | ||
| 安装 |
|
预缓存静态资源的好时机,旧版本仍在运行 | ||
| 等待 | — | 如果已有激活的 SW,新版本进入等待状态 | ||
| 激活 |
|
旧版本被替换,此时可清理旧缓存 | ||
| 空闲 | — | 等待拦截请求或接收事件 | ||
| 终止 | — | 浏览器空闲回收,节省内存 |
离线缓存策略:五种模式与选择指南
缓存策略是 PWA 最核心的设计决策。不同资源和场景需要不同的策略,没有放之四海皆准的方案。以下是五种最常用的缓存策略。
策略一:Cache First(缓存优先)
适用于不经常变化的静态资源(图片、CSS、JS 文件)。先查缓存,缓存命中则直接返回;未命中则从网络获取并缓存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 // service-worker.js - 缓存优先策略
self.addEventListener('fetch', (event) => {
if (event.request.url.match(/\.(jpg|png|svg|css|js)$/)) {
event.respondWith(cacheFirst(event.request));
}
});
async function cacheFirst(request) {
const cache = await caches.open('static-v1');
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
// 即使网络失败也返回缓存(如果有的话)
return new Response('离线:资源不可用', { status: 503 });
}
}
策略二:Network First(网络优先)
适用于 API 接口和动态内容。优先从网络获取,网络不可用时回退到缓存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 // 网络优先策略
async function networkFirst(request) {
const cache = await caches.open('dynamic-v1');
try {
const networkResponse = await fetch(request);
// 只缓存成功的响应
if (networkResponse.ok) {
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
const cachedResponse = await cache.match(request);
if (cachedResponse) {
console.log('网络不可用,使用缓存:', request.url);
return cachedResponse;
}
// 无网络也无缓存
return new Response(JSON.stringify({ error: '离线模式,数据不可用' }), {
status: 503,
headers: { 'Content-Type': 'application/json' }
});
}
}
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(networkFirst(event.request));
}
});
策略三:Stale While Revalidate(过期重新验证)
这是性能与新鲜度之间最佳平衡策略。立即返回缓存内容(即使过期),同时在后台发起网络请求更新缓存。适用于新闻列表、博客文章等即时性要求不高的内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 // 过期重新验证策略
async function staleWhileRevalidate(request) {
const cache = await caches.open('content-v1');
const cachedResponse = await cache.match(request);
// 无论缓存是否命中,都发起网络请求(后台更新)
const fetchPromise = fetch(request)
.then(async (networkResponse) => {
if (networkResponse.ok) {
await cache.put(request, networkResponse.clone());
}
return networkResponse;
})
.catch((error) => {
console.log('后台更新失败,将使用缓存:', error);
});
// 如果有缓存,立刻返回
if (cachedResponse) {
// 后台启动网络更新,不阻塞页面
event.waitUntil(fetchPromise);
return cachedResponse;
}
// 无缓存,等待网络结果
return await fetchPromise;
}
策略四:Network Only(仅网络)
适用于支付、表单提交等必须实时的操作,不做任何缓存干预。
1
2
3
4
5
6
7
8 // 仅网络(默认行为)
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/payment') ||
event.request.method === 'POST') {
return; // 不拦截,走默认的网络请求
}
// ... 其他策略
});
策略五:Cache Only(仅缓存)
适用于「骨架屏」或预渲染的离线页面。如果缓存不存在就直接失败。
1
2
3
4
5
6
7
8
9
10
11 // 仅缓存
async function cacheOnly(request) {
const cache = await caches.open('offline-shell');
const cachedResponse = await cache.match(request);
if (!cachedResponse) {
throw new Error('缓存中未找到:', request.url);
}
return cachedResponse;
}
安装与激活阶段:预缓存与缓存清理
在 Service Worker 的
1 | install |
事件中,通常会将应用外壳(App Shell)的核心资源预缓存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67 // service-worker.js - 安装与激活
const CACHE_VERSION = 'v2';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const DYNAMIC_CACHE = `dynamic-${CACHE_VERSION}`;
const OFFLINE_CACHE = `offline-${CACHE_VERSION}`;
const PRECACHE_URLS = [
'/',
'/offline.html',
'/css/main.css',
'/js/app.js',
'/js/vendor.js',
'/images/logo.png',
'/images/offline.svg',
'/fonts/inter.woff2'
];
// 安装:预缓存核心资源
self.addEventListener('install', (event) => {
console.log('Service Worker: 安装阶段');
// 强制跳过等待阶段,立即激活新版本
// 慎用:会让所有打开的页面同时被接管
// self.skipWaiting();
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => {
console.log('开始预缓存静态资源...');
return cache.addAll(PRECACHE_URLS);
})
.then(() => {
console.log('预缓存完成');
})
.catch((error) => {
console.error('预缓存失败:', error);
})
);
});
// 激活:清理旧版本缓存
self.addEventListener('activate', (event) => {
console.log('Service Worker: 激活阶段');
// 立即接管所有已打开的页面
event.waitUntil(clients.claim());
// 清理旧缓存
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((cacheName) => {
// 删除不属于当前版本的缓存
return cacheName.startsWith('static-') &&
cacheName !== STATIC_CACHE ||
cacheName.startsWith('dynamic-') &&
cacheName !== DYNAMIC_CACHE;
})
.map((cacheName) => {
console.log('删除旧缓存:', cacheName);
return caches.delete(cacheName);
})
);
})
);
});
离线回退页面
当用户完全离线且请求的资源不在缓存中时,一个好的 PWA 会展示离线页面而不是难看的浏览器错误页。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 // 离线回退页面
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.catch(async () => {
const cache = await caches.open(OFFLINE_CACHE);
const cachedResponse = await cache.match(event.request);
if (cachedResponse) {
return cachedResponse;
}
// 如果是导航请求(页面跳转),返回通用的离线页面
if (event.request.mode === 'navigate') {
return cache.match('/offline.html');
}
// 其他资源返回 503
return new Response('网络不可用', {
status: 503,
statusText: 'Service Unavailable'
});
})
);
});
对应的离线页面 HTML:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49 <!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 {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
padding: 20px;
text-align: center;
background: #f5f5f5;
}
.offline-container {
max-width: 400px;
}
img { max-width: 200px; }
h1 { color: #333; }
p { color: #666; line-height: 1.6; }
.retry-btn {
margin-top: 20px;
padding: 12px 24px;
background: #4CAF50;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
}
</style>
</head>
<body>
<div class="offline-container">
<img src="/images/offline.svg" alt="离线状态">
<h1>网络连接已断开</h1>
<p>您当前处于离线状态。部分页面内容仍可正常浏览,但实时数据更新将暂不可用。</p>
<p>请检查您的网络连接后重试。</p>
<button class="retry-btn" onclick="window.location.reload()">
重新加载
</button>
</div>
</body>
</html>
Web App Manifest:让用户将网站添加到桌面
Web App Manifest 是一个 JSON 文件,控制 PWA 在用户设备上安装后的外观和行为。这是让 Web 应用看起来像原生 App 的关键。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 {
"name": "My PWA App",
"short_name": "MyPWA",
"description": "一个功能完整的渐进式 Web 应用",
"start_url": "/?source=pwa",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4CAF50",
"orientation": "portrait-primary",
"icons": [
{
"src": "/images/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/images/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["utilities", "productivity"],
"lang": "zh-CN",
"dir": "ltr",
"scope": "/",
"edge_side_panel": {}
}
在页面 <head> 中引用 manifest:
1
2
3
4
5 <link rel="manifest" href="/manifest.json">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="MyPWA">
<link rel="apple-touch-icon" href="/images/icon-192x192.png">
各
1 | display |
模式的效果对比:
| 模式 | 浏览器 UI 元素 | 适用场景 | ||
|---|---|---|---|---|
|
全部隐藏 | 游戏、视频播放器、演示文稿 | ||
|
隐藏地址栏,显示状态栏 | 大多数应用(推荐) | ||
|
显示基本导航按钮 | 需要保留浏览器导航体验的应用 | ||
|
全部显示 | 默认,与传统页面无异 |
推送通知(Push Notification)实战

推送通知是 PWA 最接近原生 App 的能力之一。即使浏览器关闭,用户也能收到通知。这需要 3 个组件配合工作:
- 推送服务:浏览器内置的服务,负责在服务端和客户端之间传递消息
- Service Worker:接收推送事件并显示通知
- 后端服务:实际发送推送消息的服务器(使用 Web Push Protocol)
订阅推送(客户端代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 // app.js - 订阅推送通知
async function subscribeToPushNotifications() {
// 用户同意后才可订阅
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
console.warn('用户拒绝了通知权限');
return null;
}
try {
const registration = await navigator.serviceWorker.ready;
// 获取 VAPID 公钥(Base64 URL 编码)
const publicKey = 'BNfJm...yourVAPIDPublicKey...JX8c';
// 创建订阅
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true, // 必须为 true,每条推送必须显示通知
applicationServerKey: urlBase64ToUint8Array(publicKey),
});
console.log('推送订阅成功:', JSON.stringify(subscription));
// 将订阅信息发送到后端
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription),
});
return subscription;
} catch (error) {
console.error('推送订阅失败:', error);
return null;
}
}
// Base64 URL 转 Uint8Array 工具函数
function 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;
}
接收推送并显示通知(Service Worker 端)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72 // service-worker.js - 处理推送通知
self.addEventListener('push', (event) => {
console.log('收到推送消息');
let data;
try {
data = event.data.json();
} catch {
// 纯文本消息
data = {
title: '新消息',
body: event.data.text(),
icon: '/images/icon-192x192.png',
badge: '/images/badge.png',
};
}
const options = {
body: data.body || '您有一条新消息',
icon: data.icon || '/images/icon-192x192.png',
badge: data.badge || '/images/badge.png',
vibrate: [200, 100, 200],
data: {
url: data.url || '/',
dateOfArrival: Date.now(),
},
actions: [
{ action: 'view', title: '查看详情' },
{ action: 'close', title: '关闭' },
],
tag: data.tag || 'default',
renotify: data.tag ? true : false,
requireInteraction: true,
};
event.waitUntil(
self.registration.showNotification(data.title || 'PWA 通知', options)
);
});
// 处理通知点击事件
self.addEventListener('notificationclick', (event) => {
const notification = event.notification;
const action = event.action;
console.log('通知被点击:', { action, tag: notification.tag });
notification.close();
if (action === 'close') {
return;
}
// 打开或聚焦到目标页面
const urlToOpen = notification.data?.url || '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true })
.then((clientList) => {
// 如果已有打开的页面,聚焦它
for (const client of clientList) {
if (client.url === urlToOpen && 'focus' in client) {
return client.focus();
}
}
// 否则打开新窗口
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
后端发送推送(Node.js 示例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 // server/push.js - 使用 web-push 库发送推送
import webpush from 'web-push';
// VAPID 密钥对(首次运行时生成一次)
// npx web-push generate-vapid-keys
const vapidKeys = {
publicKey: process.env.VAPID_PUBLIC_KEY,
privateKey: process.env.VAPID_PRIVATE_KEY,
};
webpush.setVapidDetails(
'mailto:admin@example.com',
vapidKeys.publicKey,
vapidKeys.privateKey
);
// 发送通知给单个用户
export async function sendPushNotification(subscription, payload) {
try {
const result = await webpush.sendNotification(
subscription,
JSON.stringify({
title: '系统通知',
body: payload.message,
icon: '/images/icon-192x192.png',
url: payload.url || '/',
tag: payload.tag || `msg-${Date.now()}`,
})
);
console.log('推送发送成功:', result.statusCode);
return { success: true };
} catch (error) {
// 410 Gone:订阅已过期,需要从数据库移除
if (error.statusCode === 410) {
console.warn('订阅已过期,需要移除');
await removeSubscription(subscription);
}
console.error('推送发送失败:', error);
return { success: false, error: error.message };
}
}
// 广播给所有订阅者
export async function broadcastPush(subscriptions, payload) {
const results = await Promise.allSettled(
subscriptions.map((sub) => sendPushNotification(sub, payload))
);
return {
success: results.filter((r) => r.status === 'fulfilled').length,
failed: results.filter((r) => r.status === 'rejected').length,
};
}
后台同步(Background Sync)
Background Sync 允许 Service Worker 在网络恢复时自动重试失败的网络请求。这对离线时用户提交的表单或评论非常有价值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 // app.js - 注册后台同步
async function registerBackgroundSync() {
const registration = await navigator.serviceWorker.ready;
// 适用于 One-off 同步(如提交表单)
if ('sync' in registration) {
try {
await registration.sync.register('sync-comments');
console.log('后台同步任务已注册');
} catch (error) {
console.error('后台同步注册失败:', error);
}
} else {
// 降级方案:浏览器不支持 Background Sync
console.warn('当前浏览器不支持 Background Sync');
// 可以回退到 Service Worker 的 fetch 事件中的重试逻辑
}
}
// 当用户离线提交评论时调用
async function submitCommentOffline(commentData) {
// IndexedDB 存储待提交数据
const db = await openCommentDB();
await db.add('pending-comments', {
...commentData,
createdAt: Date.now(),
syncAttempts: 0,
});
// 注册后台同步
await registerBackgroundSync();
// 显示本地确认
showToast('评论已保存,网络恢复后自动发送');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 // service-worker.js - 处理后台同步事件
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-comments') {
console.log('触发后台同步:', event.tag);
event.waitUntil(syncPendingComments());
}
});
async function syncPendingComments() {
const db = await openCommentDB();
const pendingItems = await db.getAll('pending-comments');
for (const item of pendingItems) {
try {
const response = await fetch('/api/comments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
postId: item.postId,
content: item.content,
author: item.author,
}),
});
if (response.ok) {
await db.delete('pending-comments', item.id);
console.log('后台同步成功:', item.id);
// 可选:通知用户同步成功
self.registration.showNotification('同步成功', {
body: '您的评论已成功发布!',
icon: '/images/icon-192x192.png',
});
}
} catch (error) {
console.error('后台同步失败:', item.id, error);
// 更新重试次数,超过阈值则放弃
item.syncAttempts++;
if (item.syncAttempts >= 5) {
await db.delete('pending-comments', item.id);
console.warn('同步失败次数过多,已丢弃:', item.id);
} else {
await db.put('pending-comments', item);
}
}
}
}
生产环境最佳实践
1. 使用 Workbox 简化缓存逻辑
Google 的 Workbox 库封装了 Service Worker 的常见模式,可以大幅减少样板代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 // service-worker.js (使用 workbox-webpack-plugin)
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
// 预缓存 webpack 打包的资源
precacheAndRoute(self.__WB_MANIFEST);
// 图片:缓存优先 + 过期控制
registerRoute(
/\.(?:png|jpg|jpeg|svg|gif|webp)$/,
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 天
}),
],
})
);
// Google Fonts:过期重新验证
registerRoute(
/^https:\/\/fonts\.googleapis\.com\/css/,
new StaleWhileRevalidate({
cacheName: 'google-fonts-stylesheets',
})
);
// API 请求:网络优先 + 超时
registerRoute(
/\/api\//,
new NetworkFirst({
cacheName: 'api-responses',
networkTimeoutSeconds: 5,
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 5 * 60, // 5 分钟
}),
],
})
);
2. Service Worker 更新策略
这是许多 PWA 项目容易忽视的环节。推荐以下更新流程:
- 检测更新:每次页面加载时调用
1registration.update()
- 提示用户:新版本安装后显示「发现新版本,立即刷新?」的提示
- 用户确认后刷新:刷新后新 Service Worker 接管
- Cache Busting:每次构建时改变 Service Worker 文件名或使用版本号
3. 缓存容量与清理
| 存储类型 | 典型限制 | 清理策略 |
|---|---|---|
| Cache Storage | 可用磁盘空间的 50%(Chrome) | 按 LRU 策略自动清理,新增缓存前手动检查容量 |
| IndexedDB | 无硬性限制 | 需要手动管理,定期删除过期数据 |
| LocalStorage | 5-10MB | 不推荐在 PWA 中大量使用 |
4. 调试技巧
- Chrome DevTools > Application > Service Workers:查看注册状态、手动触发更新、绕过等待
- DevTools > Application > Cache Storage:查看和删除缓存内容
- DevTools > Application > Background Services:查看推送和后台同步的记录
- chrome://serviceworker-internals/:查看所有已注册的 Service Worker
- 使用
1self.__WB_MANIFEST
时注意构建工具配置,开发环境可用
1updateViaCache: 'none'
总结
PWA 和 Service Worker 已经不再是可有可无的增强特性,而是现代 Web 应用的标配能力。从离线访问到推送通知,从后台同步到桌面安装,这些功能让 Web 应用在用户体验上逐步缩小了与原生 App 的差距。
本文覆盖了构建一个完整 PWA 所需的核心技术点:
- Service Worker 的注册与生命周期管理
- 五种缓存策略及其适用场景
- 离线回退页面的实现
- Web App Manifest 配置与一键安装
- Web Push 推送通知的完整前后端实现
- Background Sync 让离线提交自动补发
- 使用 Workbox 简化生产级缓存配置
建议下一步的实践方向:将现有项目接入 PWA,从最基本的离线缓存开始,逐步添加推送通知和后台同步能力。先在小范围验证效果,再全面推广。PWA 的渐进增强特性让你可以按需添加功能,不必一蹴而就。
文章中涉及的所有代码都已在实际项目中验证可行,你可以直接复制使用。如有具体实现问题,欢迎在评论区讨论交流。
汤不热吧