H5 PWA 渐进式 Web 应用
H5 PWA 渐进式 Web 应用
摘要:PWA 让网页具备可安装、离线可用、推送通知等原生能力。本文从 manifest、Service Worker、缓存策略、推送、安装到调试,系统讲解如何把普通网站升级为 PWA。
一、PWA 是什么
PWA(Progressive Web App)是一组技术组合,让 Web 应用在体验上接近原生:
- 可安装到主屏,启动无浏览器外壳。
- 离线可用,弱网下仍能访问。
- 支持推送通知与后台同步。
- 跨平台,一套代码运行所有设备。
核心三件套:Web App Manifest、Service Worker、HTTPS(本地开发 localhost 例外)。
二、Web App Manifest
在 HTML 中引用:
<link rel="manifest" href="/manifest.json">
manifest.json:
{
"name": "京行 · 北京旅游",
"short_name": "京行",
"description": "北京旅游推荐指南",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#1A1A1A",
"theme_color": "#D4A84B",
"lang": "zh-CN",
"icons": [
{ "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
],
"shortcuts": [
{ "name": "必游景点", "url": "/#destinations", "icons": [{ "src": "/icons/spot.png", "sizes": "96x96" }] }
]
}
关键字段:
display:standalone隐藏浏览器 UI;fullscreen全屏;minimal-ui保留最少控件。theme_color:影响状态栏与窗口边框颜色。purpose: maskable:让图标可被系统裁剪到安全区域,避免被切圆角时缺角。shortcuts:长按图标快捷菜单。
三、Service Worker 基础
SW 是运行在主线程之外的脚本,可拦截网络请求、管理缓存、接收推送。
注册:
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("/sw.js").then(reg => {
console.log("注册成功", reg.scope);
});
});
}
生命周期:install → installed → activating → activated → redundant。
四、缓存策略
预缓存
const CACHE = "v1";
const ASSETS = ["/", "/index.html", "/styles.css", "/script.js"];
self.addEventListener("install", e => {
e.waitUntil(caches.open(CACHE).then(c => c.addAll(ASSETS)));
});
self.addEventListener("activate", e => {
e.waitUntil(
caches.keys().then(keys => Promise.all(
keys.filter(k => k !== CACHE).map(k => caches.delete(k))
))
);
});
install 时预缓存核心资源,activate 时清理旧缓存。
拦截请求
self.addEventListener("fetch", e => {
e.respondWith(
caches.match(e.request).then(cached => {
return cached || fetch(e.request).then(resp => {
const copy = resp.clone();
caches.open(CACHE).then(c => c.put(e.request, copy));
return resp;
});
})
);
});
这是"缓存优先,回退网络"策略。不同资源适合不同策略:
- HTML:网络优先(保证最新),失败回退缓存。
- CSS/JS/图片:缓存优先(带 hash 时长缓存)。
- API:stale-while-revalidate(先返回缓存,同时更新)。
Stale While Revalidate
self.addEventListener("fetch", e => {
e.respondWith(
caches.open(CACHE).then(async c => {
const cached = await c.match(e.request);
const fetchPromise = fetch(e.request).then(resp => {
c.put(e.request, resp.clone());
return resp;
});
return cached || fetchPromise;
})
);
});
兼顾速度与新鲜度,适合 API 数据。
五、用 Workbox 简化
手写 SW 容易出错,Google 的 Workbox 库封装了常用策略:
import { registerRoute } from "workbox-routing";
import { CacheFirst, StaleWhileRevalidate, NetworkFirst } from "workbox-strategies";
import { precacheAndRoute } from "workbox-precaching";
precacheAndRoute(self.__WB_MANIFEST);
registerRoute(
({ request }) => request.destination === "image",
new CacheFirst({ cacheName: "images" })
);
registerRoute(
({ url }) => url.pathname.startsWith("/api/"),
new StaleWhileRevalidate({ cacheName: "api" })
);
registerRoute(
({ request }) => request.mode === "navigate",
new NetworkFirst({ cacheName: "pages" })
);
配合构建插件自动生成预缓存清单,省去手写资源列表。
六、离线兜底
const OFFLINE_URL = "/offline.html";
self.addEventListener("fetch", e => {
if (e.request.mode === "navigate") {
e.respondWith(
fetch(e.request).catch(() => caches.match(OFFLINE_URL))
);
}
});
离线时返回一个友好的兜底页,而非浏览器默认错误。
七、推送通知
订阅
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC)
});
await fetch("/api/subscribe", { method: "POST", body: JSON.stringify(sub) });
接收
self.addEventListener("push", e => {
const data = e.data.json();
self.registration.showNotification(data.title, {
body: data.body,
icon: "/icons/192.png",
badge: "/icons/badge.png",
data: { url: data.url }
});
});
self.addEventListener("notificationclick", e => {
e.notification.close();
e.waitUntil(clients.openWindow(e.notification.data.url));
});
服务端用 Web Push 协议 + VAPID 密钥推送,浏览器收到后唤醒 SW 触发通知。
八、后台同步
// 注册同步
const reg = await navigator.serviceWorker.ready;
reg.sync.register("send-outbox");
// SW 中处理
self.addEventListener("sync", e => {
if (e.tag === "send-outbox") {
e.waitUntil(sendOutbox());
}
});
用户离线时把请求存到 IndexedDB,恢复网络后 SW 自动重试,无需用户打开页面。
九、安装提示
监听 beforeinstallprompt 自定义安装按钮:
let deferredPrompt;
window.addEventListener("beforeinstallprompt", e => {
e.preventDefault();
deferredPrompt = e;
showInstallButton();
});
installButton.addEventListener("click", async () => {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === "accepted") {
trackInstall();
}
deferredPrompt = null;
});
iOS Safari 不支持该事件,需用引导提示让用户手动"添加到主屏"。
十、检测与调试
- Chrome DevTools → Application → Manifest 查看清单与安装。
- Application → Service Workers 查看注册状态、更新、绕过网络。
- Lighthouse PWA 评分给出完整检查项。
- 用
workbox-window监听 SW 更新提示用户刷新。
十一、更新策略
SW 默认在下次启动时激活旧版本仍服务本次会话。要即时更新:
self.addEventListener("install", e => self.skipWaiting());
self.addEventListener("activate", e => e.waitUntil(clients.claim()));
配合 navigator.serviceWorker.addEventListener("controllerchange", () => location.reload()) 提示用户刷新。生产中更稳妥的是显示"有新版本"提示,让用户主动刷新,避免中途刷新打断操作。
十二、总结
PWA 用 Web 技术提供接近原生的体验,关键在 Manifest + Service Worker + 缓存策略三件套。建议:用 Workbox 管缓存、按资源类型选策略、提供离线兜底页、用真实用户数据验证效果。PWA 不是要替代原生,而是让 Web 在合适场景下足够好用——内容型应用、工具型应用尤其适合。
- 点赞
- 收藏
- 关注作者
评论(0)