PWA(Progressive Web App,渐进式 Web 应用)是一种利用现代 Web 技术构建的应用形态,它兼具网页的可达性与原生应用的交互体验。通过 Service Worker、Cache Storage 和 Web App Manifest 三大核心技术,PWA 能够在弱网甚至无网环境下保持可用,并支持添加至主屏幕、后台同步和推送通知等功能。
PWA 的核心价值
PWA 并非框架或库,而是一套设计范式与技术标准的组合。它的目标是在不依赖应用商店分发的前提下,让用户获得接近原生应用的体验。其核心优势包括:离线可用、即时加载、可安装、可推送、安全且响应迅速。对于内容型产品或工具类站点,PWA 能以极低的开发成本覆盖移动端用户,同时避免 iOS 与 Android 双端维护的负担。
Twitter Lite 是 PWA 领域的标杆案例。通过 Service Worker 缓存静态资源与推文数据,其页面加载时间减少了 30%,数据流量消耗降低了 70%,用户平均会话时长反而显著提升。这证明了 PWA 在真实业务场景中的可行性。
Service Worker 生命周期
Service Worker 是 PWA 的基石,它是一个运行于浏览器后台的独立线程,能够拦截网络请求、管理缓存、处理推送和后台同步。理解其生命周期是掌握 PWA 开发的第一步。
Service Worker 的生命周期包含四个阶段:
installing:当浏览器未发现当前作用域下已激活的 Service Worker,或检测到脚本内容发生字节级变化时,会触发安装事件。此阶段适合预缓存核心静态资源。如果在 install 事件回调中调用了 event.waitUntil() 并且传入的 Promise 被拒绝,安装将失败,Service Worker 进入 redundant 状态。
waiting:安装成功后,如果当前已有一个激活的 Service Worker 在运行,新安装的版本会进入等待状态。这是为了避免同一时刻存在两个不同版本的 Service Worker 同时处理请求,导致页面状态不一致。当所有受控页面关闭后,新的 Service Worker 才会激活。
activating:旧 Service Worker 退出后,新版本进入激活阶段。此时可以清理旧版本的缓存,确保后续请求使用最新的资源。激活完成后,Service Worker 开始接管其注册作用域内的 fetch 请求。
redundant:当安装失败、被新版本取代,或注册被撤销时,Service Worker 进入废弃状态,不再接收任何事件。
开发者可通过 self.skipWaiting() 在 install 事件中强制新版本立即激活,同时配合 clients.claim() 在 activate 阶段立即接管已有页面。这在开发环境中极为常见,但生产环境需谨慎使用,以免导致正在浏览的页面出现资源版本不匹配的问题。
缓存策略详解
Service Worker 拦截 fetch 请求后,可依据业务场景选择不同的缓存策略。以下是五种经典模式:
Cache First(缓存优先):请求发生时先查询 Cache Storage。若命中则直接返回缓存内容;若未命中再发起网络请求,并将成功响应写入缓存。此策略适用于不经常变动的静态资源,如 Logo、字体文件和框架代码。缺点是一旦缓存命中,用户在缓存过期前始终看不到更新。
Network First(网络优先):优先发起网络请求。若成功则返回响应并更新缓存;若失败则回退到缓存。此策略适合对实时性要求较高的内容,如新闻列表、社交动态。缺点是弱网环境下请求延迟较高,用户体验可能受损。
Stale-While-Revalidate(过时重验证):先返回缓存中的旧内容以保证即时响应,同时在后台发起网络请求更新缓存。下一次访问时用户即可获得最新内容。此策略平衡了加载速度与内容新鲜度,是 PWA 中最推荐的通用策略。
Cache Only:所有请求仅从缓存中响应。适用于需要完全离线运行的应用,或某些明确不需要网络资源的场景。
Network Only: bypass 缓存,所有请求直接走网络。适用于隐私敏感或严格依赖实时数据的接口。
在实际项目中,极少只使用单一策略。通常的做法是为主文档、接口数据和静态资源分别配置不同的策略,并通过路由匹配实现精细化控制。
Cache Storage API 的精细管理
Cache Storage 是 Service Worker 中专门用于缓存请求/响应对的存储空间,与浏览器 HTTP 缓存相互独立。通过 caches.open() 打开具名缓存,开发者可以实现多版本共存与按需清理。
缓存版本控制是生产环境必须面对的问题。推荐在缓存名称中加入版本号,如 static-v2、api-v1。当 Service Worker 新版本激活时,在 activate 事件中遍历所有缓存键名,删除不再属于当前版本的缓存空间,避免磁盘空间无限增长。
清理策略需要考虑两个维度:过期时间与容量上限。Cache Storage API 本身不提供 TTL 机制,需要开发者自行实现。一种常见做法是在缓存条目中加入时间戳元数据,每次读取时判断是否过期;另一种方案是限制缓存总条目数,采用 LRU 算法淘汰最久未使用的资源。
对于 Cache Storage 的调试,Chrome DevTools 的 Application 面板提供了可视化的缓存浏览和删除功能,极大降低了排错成本。
Web App Manifest
Web App Manifest 是一份 JSON 配置文件,定义了应用的名称、图标、主题色、显示模式等元信息。浏览器依据这份清单生成添加到主屏幕的入口,并决定应用以何种形态启动。
关键字段包括:
name与short_name:分别为完整名称与短名称,后者用于主屏幕图标下方等空间有限的场景。icons:提供多分辨率 PNG 或 SVG 图标,系统会自动选择最合适的尺寸。display:取值包括standalone、fullscreen、minimal-ui和browser。standalone最为常用,会隐藏浏览器地址栏,呈现类似原生应用的沉浸感。start_url:定义从主屏幕启动时打开的入口路径,通常设置为/?source=pwa以便分析 PWA 用户来源。theme_color与background_color:控制浏览器工具栏和启动画面的颜色,应与品牌形象保持一致。shortcuts:定义长按图标时弹出的快捷菜单,可直达应用的常用功能页。
Manifest 文件必须在 HTML 的 <head> 中通过 <link rel="manifest" href="/manifest.json"> 引入。
Background Sync 后台同步
用户在离线状态下提交的表单、发送的消息或执行的操作,往往需要在网络恢复后自动补发。Background Sync API 正是为此而生。
基本流程是:页面主线程调用 navigator.serviceWorker.ready,获取 Service Worker 注册实例,然后执行 registration.sync.register('sync-tag')。浏览器在检测到网络恢复后,会触发 Service Worker 的 sync 事件,开发者取出待发送队列中的数据,逐一完成网络请求。
此 API 并非定时任务调度器,它只保证在浏览器认为"合适的时候"触发一次。对于需要精确周期执行的后台任务,应使用 Periodic Background Sync,但其兼容性目前仍较有限。
推送通知
Push API 与 Notification API 的组合使 PWA 具备了类似原生应用的消息推送能力。与 WebSocket 等长连接方案不同,浏览器厂商提供统一的推送服务端,即使应用页面完全关闭,只要 Service Worker 存活,依然可以接收推送并弹出通知。
技术流程分为三步:客户端订阅推送服务,获得包含 endpoint 和密钥的 PushSubscription 对象;服务端通过 Web Push 协议向该 endpoint 发送加密消息;Service Worker 收到 push 事件后调用 self.registration.showNotification() 展示系统级通知。
由于推送消息本身不包含可读正文,Service Worker 通常需要在此事件内发一个网络请求,拉取完整的通知内容或跳转目标 URL。
需要注意的是,iOS 直到 Safari 16.4 才开始支持 Web Push,这是 PWA 生态长期受限的重要瓶颈。
添加至主屏幕(A2HS)
当站点满足可安装性条件时,浏览器会在地址栏显示安装图标,或触发 beforeinstallprompt 事件。开发者可以监听该事件,保存事件引用,然后在合适的时机(如用户点击自定义的"安装"按钮)调用 prompt() 方法弹出系统级安装对话框。
用户完成安装后,应用图标出现在主屏幕或开始菜单中,启动时以全屏或独立窗口模式运行,不再显示浏览器 UI。
可安装性要求清单
并非所有站点都能被浏览器识别为可安装应用。必须同时满足以下条件:
- 站点必须部署在 HTTPS 协议下,Service Worker 和 Push API 均要求安全上下文。
- 提供有效的 Web App Manifest 文件,且包含至少一组符合规范的图标。
- 注册一个 Service Worker,并且其代码中包含对 fetch 事件的监听逻辑。
- Manifest 中的
start_url、name等核心字段必须合法且非空。
开发者可通过 Chrome DevTools 的 Lighthouse 面板运行"PWA"审计,快速排查不满足的条目。
完整代码示例
以下是一个可直接用于生产环境的 Service Worker 模板,采用 Stale-While-Revalidate 作为默认策略,并为静态资源和 API 请求分别配置了独立缓存。
// /sw.js
const STATIC_CACHE = 'static-v2';
const API_CACHE = 'api-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/icon-192x192.png',
'/offline.html'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => cache.addAll(STATIC_ASSETS))
.then(() => self.skipWaiting())
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys
.filter((key) => key !== STATIC_CACHE && key !== API_CACHE)
.map((key) => caches.delete(key))
)
).then(() => self.clients.claim())
);
});
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request, API_CACHE));
} else if (request.destination === 'image' || request.destination === 'script') {
event.respondWith(staleWhileRevalidate(request, STATIC_CACHE));
} else {
event.respondWith(
caches.match(request).then((cached) => cached || fetch(request))
);
}
});
async function staleWhileRevalidate(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
const fetchPromise = fetch(request).then((response) => {
if (response.ok) cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}
async function networkFirst(request, cacheName) {
const cache = await caches.open(cacheName);
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) cache.put(request, networkResponse.clone());
return networkResponse;
} catch (error) {
const cached = await cache.match(request);
if (cached) return cached;
throw error;
}
}
self.addEventListener('push', (event) => {
const data = event.data ? event.data.json() : {};
event.waitUntil(
self.registration.showNotification(data.title || '新消息', {
body: data.body || '',
icon: '/icon-192x192.png',
badge: '/badge-72x72.png',
data: data.url || '/'
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data));
});
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-outbox') {
event.waitUntil(syncOutbox());
}
});
async function syncOutbox() {
const requests = await getPendingRequests();
return Promise.all(requests.map((req) => fetch(req)));
}
页面主线程注册 Service Worker 的代码:
// /app.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((reg) => console.log('SW registered:', reg.scope))
.catch((err) => console.error('SW registration failed:', err));
});
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
// 显示自定义安装按钮
document.getElementById('install-btn').style.display = 'block';
});
document.getElementById('install-btn')?.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(outcome === 'accepted' ? 'User installed PWA' : 'User dismissed install');
deferredPrompt = null;
});
}
Web App Manifest 文件:
{
"name": "My PWA App",
"short_name": "MyPWA",
"start_url": "/?source=pwa",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#1a73e8",
"icons": [
{
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"shortcuts": [
{
"name": "新建笔记",
"short_name": "笔记",
"description": "快速创建一条新笔记",
"url": "/note/new",
"icons": [{ "src": "/shortcut-note.png", "sizes": "192x192" }]
}
]
}
总结
PWA 通过 Service Worker 赋予 Web 应用离线能力和交互深度,通过 Cache Storage 实现精细化的资源管理,通过 Manifest 完成从网页到"应用"的身份转变。它不是要取代原生应用,而是在 Web 技术的边界内提供最优的用户体验。对于希望快速覆盖多端用户、同时又不愿承受应用商店审核与分发成本的产品团队来说,PWA 仍然是一条值得深入投入的技术路线。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。