PWA 开发:Service Worker、离线缓存与安装
PWA 渐进式 Web 应用开发
PWA(Progressive Web App)让网页同时拥有网站和原生应用的优势:快速加载、离线访问、可安装到设备主屏。本教程将手把手带你掌握 PWA 的核心技术——Service Worker、离线缓存与应用安装。
1. 什么是 PWA
PWA 不是某种特定的框架或语言,而是一套现代化的 Web 能力组合,主要包括:
- 渐进式增强:在任何浏览器都能运行,现代化浏览器体验更佳。
- 响应式设计:适配各种屏幕。
- 离线或弱网可用:通过 Service Worker 缓存关键资源。
- 像原生应用一样交互:通过 Web App Manifest 实现全屏、图标及安装。
- 始终更新:Service Worker 更新机制确保内容新鲜。
- 安全:必须使用 HTTPS。
本教程聚焦于后三类:Manifest、Service Worker 与离线缓存。
2. 准备工作:一个简单的页面
首先创建一个最基本的网页项目。目录结构如下:
pwa-demo/
├── index.html
├── style.css
├── app.js
├── manifest.json
├── sw.js
└── images/
└── icon-192.png
└── icon-512.png
index.html 中引入必要文件:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#2196F3">
<title>我的 PWA 应用</title>
</head>
<body>
<h1>欢迎使用 PWA</h1>
<p>这是一个渐进式 Web 应用示例。</p>
<script src="app.js"></script>
</body>
</html>
注意:PWA 要求 HTTPS 环境,本地开发可使用
localhost或配合工具如ngrok。
3. Web App Manifest:让应用可安装
Manifest 是一个 JSON 文件,告诉浏览器你的应用应该以何种方式呈现给用户,并支持添加到主屏幕。
基础 manifest.json
{
"name": "我的 PWA 应用",
"short_name": "PWA Demo",
"start_url": "/index.html",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2196F3",
"icons": [
{
"src": "images/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "images/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
关键字段说明:
name/short_name:应用全称与短名称(用于主屏标题)。start_url:启动时加载的页面,通常为根路径。display:显示模式,standalone看起来更像独立应用(无浏览器 UI)。icons:至少提供 192px 和 512px 的图标,浏览器会按需缩放。theme_color:工具栏颜色。background_color:启动画面背景色。
在 HTML 中通过 <link rel="manifest" href="manifest.json"> 引入。此时用 Chrome 打开页面,DevTools 的 Application -> Manifest 面板可检查 Manifest 是否正确,符合条件时浏览器会弹出安装提示。
4. Service Worker:离线能力的基石
Service Worker 是运行在浏览器背后的独立脚本,拦截和处理网络请求,使丰富的离线体验成为可能。
4.1 注册 Service Worker
在 app.js 中添加注册逻辑:
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW 注册成功: ', registration.scope);
})
.catch(err => {
console.log('SW 注册失败: ', err);
});
});
}
注册在页面 load 后执行,避免与首屏资源加载竞争。
4.2 Service Worker 生命周期
- 安装 (install):适合预缓存关键文件。
- 激活 (activate):清理旧缓存。
- 激活后控制页面 (fetch):拦截请求,从缓存或网络响应。
4.3 编写 sw.js:离线缓存
下面创建一个完整的 sw.js,实现预缓存与运行时缓存。
// 缓存版本和资源清单
const CACHE_NAME = 'pwa-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/style.css',
'/app.js',
'/images/icon-192.png',
'/images/icon-512.png'
];
// 安装事件:预缓存关键资源
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.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
});
// 请求拦截:先查缓存,后走网络
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 缓存命中,返回缓存
if (response) {
return response;
}
// 否则发起网络请求并动态缓存
return fetch(event.request).then(networkResponse => {
// 确保响应有效
if (!networkResponse || networkResponse.status !== 200 || networkResponse.type !== 'basic') {
return networkResponse;
}
const responseToCache = networkResponse.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseToCache);
});
return networkResponse;
});
})
);
});
这段代码实现了 Cache First (缓存优先) 策略,适合稳定的静态资源。动态请求会被网络获取后自动缓存,下次离线时可用。
4.4 缓存策略进阶
不同的资源需要不同的缓存策略:
| 策略名称 | 描述 | 适用场景 |
|---|---|---|
| Cache First | 优先从缓存获取,缓存不存在才去网络 | 静态资源、应用外壳 |
| Network First | 优先网络请求,失败时使用缓存 | API 数据、需常更新的内容 |
| Stale-While-Revalidate | 立即返回缓存,同时后台更新缓存 | 平衡性能与新鲜度 |
| Network Only | 始终从网络获取 | 敏感信息或实时数据 |
| Cache Only | 仅从缓存获取,网络都不尝试 | 必须完全离线运行的资源 |
示例:为 API 请求使用 Network First 策略
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then(response => {
return caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, response.clone());
return response;
});
})
.catch(() => {
return caches.match(event.request);
})
);
}
// 其他请求沿用默认逻辑
});
5. 触发安装提示
当应用满足以下条件时,浏览器会自动触发安装横幅:
- 拥有有效的 Web App Manifest。
- 已注册 Service Worker。
- 网站通过 HTTPS 提供(或 localhost)。
- 用户与网站有一定交互(如停留一段时间)。
你还可以主动捕获 beforeinstallprompt 事件,自定义安装按钮:
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
// 阻止默认的横幅
e.preventDefault();
deferredPrompt = e;
// 显示自定义安装按钮
const installBtn = document.createElement('button');
installBtn.textContent = '安装应用';
installBtn.style.cssText = 'position:fixed;bottom:20px;right:20px;';
document.body.appendChild(installBtn);
installBtn.addEventListener('click', () => {
deferredPrompt.prompt();
deferredPrompt.userChoice.then(choiceResult => {
if (choiceResult.outcome === 'accepted') {
console.log('用户同意安装');
}
deferredPrompt = null;
});
});
});
6. 调试与测试
Chrome DevTools 是调试 PWA 的利器:
- Application 面板:
- Manifest:查看 manifest 解析结果和安装条件。
- Service Workers:查看注册状态、更新、停止、模拟离线。
- Cache Storage:查看缓存资源列表,删除指定缓存。
- Lighthouse 审计:生成 PWA 报告,指导 PWA 最佳实践。
- 离线模拟:在 Service Workers 面板勾选 “Offline”,然后刷新页面,验证离线可用性。
7. 更新你的 PWA
当 sw.js 文件本身发生变化时,浏览器会检测到新的 Service Worker。但默认旧版本仍在运行,直到所有使用它的页面关闭。为了更及时的更新,可在 install 事件中调用 self.skipWaiting(),并在 activate 中调用 self.clients.claim():
self.addEventListener('install', event => {
self.skipWaiting(); // 立即生效,不等待页面关闭
// 预缓存...
});
self.addEventListener('activate', event => {
event.waitUntil(
clients.claim() // 让新 SW 立即控制所有客户端
.then(() => caches.keys().then(/*清理旧缓存*/))
);
});
8. 最佳实践总结
- 合理拆分缓存版本:用不同版本号或名称管理静态资源、图片、API 缓存,便于独立更新。
- 谨慎选择缓存策略:避免将 HTML 入口文件缓存过久导致更新困难,通常对首页用 Network First 或 Stale-While-Revalidate。
- 控制缓存大小:限制 Cache Storage 的大小,定期清理旧数据。
- 确保 key 资源及时更新:利用 Service Worker 的
message事件通知用户有新内容可用。 - 渐进增强:始终保证基础功能在不支持 Service Worker 的浏览器下也能访问。
9. 完整代码获取
你可以在 GitHub 上找到本教程的示例项目(示例地址),直接克隆并替换自己的资源即可体验。
现在你已经掌握了如何将一个普通网页转变为可安装、可离线访问的 PWA。进一步的探索方向包括推送通知、后台同步、IndexedDB 离线存储等。PWA 的世界刚刚开始。