当网络不稳定或彻底离线时,网页就白屏了——这是传统 Web 应用最大的短板。PWA(Progressive Web App,渐进式 Web 应用)用 Service Worker 与 Cache API 把核心资源缓存到本地,让站点像原生 App 一样可离线打开、可添加到主屏、可接收推送。本文从零讲清 PWA 的三大支柱、离线缓存策略与工程化落地,附可直接复用的 Service Worker 代码。
一、什么是 PWA,为什么需要它
PWA 不是某个具体框架,而是一组 Web 标准能力的统称。它让网站具备”可安装、可离线、可推送”的原生体验,同时保留 Web 免安装、即点即用的优势。对于内容站、工具站、后台系统来说,PWA 能显著提升首屏速度与留存——尤其在网络较差的移动端。
核心收益有三点:① 离线可用,断网也能浏览已访问页面;② 秒开,Service Worker 拦截请求走缓存,跳过网络往返;③ 可安装,用户把站点”添加至主屏”,入口与 App 无异。配合前端性能优化手段,PWA 是性价比极高的体验升级。
一个真实场景:地铁进隧道时网络中断,传统网页立刻白屏,用户直接流失;而 PWA 因为 App Shell 已缓存,页面照常渲染、已读文章照常可读,网络恢复后再静默同步新内容。对内容型站点(博客、文档、资讯)而言,这种”弱网不崩”的韧性,往往比首屏快几百毫秒更影响留存。
二、PWA 三大支柱:Manifest、Service Worker、HTTPS
一个合格的 PWA 必须满足三项基础:① Web App Manifest(manifest.json)——定义名称、图标、启动页;② Service Worker——后台脚本,负责缓存与请求拦截;③ HTTPS——Service Worker 仅在安全上下文注册(localhost 开发例外)。三者缺一不可,否则浏览器不会弹出”安装”提示。
三、注册 Service Worker 与生命周期
Service Worker 本质是独立于页面运行的脚本,通过三个事件管理缓存:install(首次安装,预缓存静态资源)、activate(旧缓存清理)、fetch(拦截网络请求决定走缓存还是网络)。在页面入口注册即可:
// main.js —— 在页面入口注册 Service Worker
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 failed:', err));
});
}
注册后浏览器会在后台安装 sw.js。注意作用域:注册路径决定它能拦截哪些请求,/sw.js 默认管控整站。开发中可打开 Chrome DevTools 的 Application → Service Workers 面板实时调试,配合VS Code的 Live Server 即可联调。
需要特别留意 skipWaiting 与 clients.claim 是一把双刃剑:自动激活能让新版本尽快生效,但可能打断正在进行的请求或让旧页面逻辑错乱。对强一致性的后台系统,更稳妥的做法是监听 controllerchange 事件,在用户空闲时再 location.reload(),而不是强制刷新。调试时记得勾选 DevTools 的”Update on reload”,否则每次刷新用的仍是旧缓存。
四、离线缓存策略:Cache API 实战
install 阶段预缓存 App Shell(HTML/CSS/JS/图标),fetch 阶段按策略响应请求。下面是一段生产可用的 sw.js:
// sw.js
const CACHE = 'pwa-v1';
const PRECACHE = ['/', '/index.html', '/app.css', '/app.js', '/icon-192.png'];
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open(CACHE).then((c) => c.addAll(PRECACHE)).then(() => self.skipWaiting())
);
});
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
).then(() => self.clients.claim())
);
});
// 缓存优先(适合静态资源),失败再回源
self.addEventListener('fetch', (e) => {
if (e.request.method !== 'GET') return;
e.respondWith(
caches.match(e.request).then((hit) =>
hit ||
fetch(e.request)
.then((res) => {
const copy = res.clone();
caches.open(CACHE).then((c) => c.put(e.request, copy));
return res;
})
.catch(() => caches.match('/index.html')) // 离线兜底
)
);
});
不同资源适合不同策略,切勿一刀切:
| 策略 | 适用资源 | 取舍 |
| Cache First(缓存优先) | 静态资源、图标、字体 | 最快,但更新有延迟 |
| Network First(网络优先) | API、动态数据 | 数据新,断网才走缓存 |
| Stale-While-Revalidate | 频繁访问的列表页 | 先返缓存、后台更新 |
| Network Only / Cache Only | 登录请求 / 纯离线包 | 极端场景专用 |
对 API 与动态数据,更稳妥的是”网络优先、失败回缓存”。下面把 fetch 策略按路径分流:静态资源走缓存优先,接口走网络优先,并给接口加上兜底响应,避免断网时整页报错。
// 按资源类型分流的 fetch 策略
self.addEventListener('fetch', (e) => {
const url = new URL(e.request.url);
if (e.request.method !== 'GET') return;
// 接口:网络优先,失败回缓存
if (url.pathname.startsWith('/api/')) {
e.respondWith(
fetch(e.request)
.then((res) => {
const copy = res.clone();
caches.open(CACHE).then((c) => c.put(e.request, copy));
return res;
})
.catch(() => caches.match(e.request).then((h) => h || new Response('{}', { status: 503 })))
);
return;
}
// 静态资源:缓存优先,后台更新
e.respondWith(
caches.match(e.request).then((hit) =>
hit || fetch(e.request).then((res) => {
const copy = res.clone();
caches.open(CACHE).then((c) => c.put(e.request, copy));
return res;
}).catch(() => caches.match('/index.html'))
)
);
});
五、Web App Manifest 配置
manifest.json 告诉浏览器”安装后长什么样”。图标需提供 192 与 512 两种尺寸,并声明 purpose,否则部分机型不显示安装横幅。
{
"name": "FSData 技术站",
"short_name": "FSData",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2563eb",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
]
}
在 HTML 的 <head> 中通过 <link rel="manifest" href="/manifest.json"> 引入。若用Vue 3等框架,可在 public/ 目录放置 manifest 与图标,构建后自动随站点发布。
六、更新与版本管理
Service Worker 的更新是最大的坑:旧 SW 会一直管控页面直到所有标签页关闭。推荐做法是用缓存版本号(如 pwa-v1)做”原子替换”——activate 阶段删除旧版本缓存,配合 skipWaiting + clients.claim 可在新版本发布后立即生效。注意:改了 sw.js 内容,浏览器才会重新安装,单纯更新被缓存的资源不会触发刷新。
七、调试与常见误区
上线前务必在真机与 DevTools 验证。以下是高频踩坑:
| 现象 | 根因 | 解法 |
| 不弹出”安装” | 缺 manifest 或图标尺寸不全 | 补齐 192/512 图标与 name |
| 更新不生效 | 旧 SW 未 skipWaiting | 激活阶段清理旧缓存 |
| 本地改了资源却没变 | 缓存优先策略 | 改用 Network First 或加版本号 |
| 开发时 404 | 非 HTTPS 且非 localhost | 用Docker起本地 https 或走 localhost |
| 跨域资源缓存失败 | fetch 未带 credentials | 对跨域请求单独处理 opaque 响应 |
八、与构建工具 / 框架集成
现代框架大多有成熟方案:Vue CLI / Vite 可用 vite-plugin-pwa,Next.js 用 next-pwa,都能自动生成 manifest 与 SW。若站点经Nginx 反向代理或 CDN 分发,记得对 /sw.js 关闭缓存(设 Cache-Control: no-cache),否则更新永远到不了用户。静态资源则相反,应设长缓存并带 hash 文件名,由 SW 的版本号控制失效。
以 Vite 为例,配置 vite-plugin-pwa 后几乎零代码量即可获得完整 PWA 能力,构建时自动注入 manifest、生成带预缓存清单的 Service Worker:
// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa';
export default {
plugins: [
VitePWA({
registerType: 'autoUpdate', // 新版本发布后自动激活
includeAssets: ['favicon.ico', 'icon-192.png', 'icon-512.png'],
manifest: {
name: 'FSData 技术站',
short_name: 'FSData',
theme_color: '#2563eb',
icons: [
{ src: 'icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: 'icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' }
]
},
workbox: { globPatterns: ['**/*.{js,css,html,png,svg}'] }
})
]
};
九、生产落地检查清单
| 检查项 | 是否完成 |
| 全站 HTTPS(含子资源) | □ |
| manifest.json 含 192/512 图标 | □ |
| SW 注册成功且 install/activate 无报错 | □ |
| 离线打开已访问页面正常 | □ |
| /sw.js 关闭浏览器缓存 | □ |
| Lighthouse PWA 评分 ≥ 90 | □ |
把以上清单跑通,你的站点就具备原生级体验了。PWA 改动量小、收益大,配合GitHub Actions做持续部署,每次发布新版本都会自动推送给用户。下一步可接入 Web Push 实现消息推送,让站点真正”活”起来。
验证 PWA 是否达标,最直接的方式是跑 Lighthouse:在 DevTools 的 Lighthouse 面板选”Progressive Web App”模式,得分 ≥ 90 即代表安装能力、离线能力、响应式三项基本合格。若反复卡在”无离线兜底”,通常是因为 fetch 事件没给 caches.match('/index.html') 的兜底分支;若安装横幅不出现,则回到清单图标尺寸与 HTTPS 覆盖度排查。上线后建议用真实弱网(Chrome DevTools 的 Network 面板调成 Offline)实测一次首屏与已访问页回看,确认断网不白屏再对外宣扬”已支持 PWA”。




