Web 应用一直有个尴尬的定位:不用安装、跨平台、随时更新,但和原生 App 相比,它断网就白屏、没有桌面图标、打开依赖网络。PWA 正是为了抹平这道差距——让网页拥有接近原生 App 的可靠性、速度和安装体验。
一、为什么需要 PWA
传统 Web 应用的几个短板:
- 离线不可用:网络一断,页面直接打不开;弱网下白屏等待。
- 没有安装入口:用户每次都要打开浏览器、输网址,无法像 App 一样「放在桌面」。
- 启动依赖网络:每次都重新请求 HTML、JS、CSS,首屏慢。
- 缺少沉浸感:带着浏览器地址栏,不像原生应用。
- 无法主动触达:不能像 App 一样推送通知(虽然现在有 Web Push)。
而原生 App 虽然体验好,却有开发成本高、要上架审核、跨平台重复开发、更新需用户下载等问题。
PWA 的思路是:用 Web 技术,拿到接近原生的体验——不装 App,也能离线可用、一键添加到桌面、秒开。
二、PWA 是什么
PWA 全称 Progressive Web App(渐进式 Web 应用)。它不是一个具体框架或库,而是一组 Web 能力与最佳实践的组合,核心由三块构成:
- Service Worker:运行在后台的独立脚本,能拦截网络请求、管理缓存,是「离线可用」的基础。
- Web App Manifest:一份描述应用元信息的 JSON,决定它能否被「安装」到桌面、图标长什么样。
- HTTPS:上述能力的前提,必须在安全上下文(HTTPS 或 localhost)下才能工作。
围绕这三块,PWA 通常具备以下特性:
| 特性 | 依赖 | 效果 |
|---|---|---|
| 离线可用 | Service Worker + Cache | 断网也能打开 |
| 秒开、弱网可用 | 缓存策略 | 静态资源本地读取 |
| 可安装 | Web App Manifest | 添加到主屏、独立窗口 |
| 沉浸式体验 | display: standalone | 隐藏浏览器地址栏 |
| 消息推送 | Push API + Notification | 主动触达用户 |
| 后台同步 | Background Sync | 网络恢复后补发请求 |
「渐进式(Progressive)」的含义是:能力可以逐步增强。老浏览器不支持的,退化成普通网页;支持的,则获得离线、安装等体验,而不是全有或全无。
三、核心之一:Service Worker
1. 它是什么
Service Worker 是浏览器在页面之外单独开的一个后台线程,生命周期不依附于某个页面。它能:
- 拦截页面发出的所有网络请求(
fetch事件)。 - 读写 Cache Storage,自己决定「用缓存还是走网络」。
- 在页面关闭后依然存在,从而实现离线、后台同步等能力。
它的生命周期分三步:
install → 预缓存资源,安装完成后进入 waiting
activate → 清理旧缓存,正式接管
fetch → 拦截请求,按策略返回响应
默认情况下,新的 Service Worker 会先进入 waiting,等所有页面关闭后才 activate,以避免版本错乱。用 self.skipWaiting() 可以让它立即接管。
2. 一个最小的 Service Worker
// sw.js
const CACHE = 'app-v1';
const ASSETS = ['/', '/index.html', '/js/main.js', '/css/main.css'];
// 预缓存
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE).then((cache) => cache.addAll(ASSETS))
);
self.skipWaiting();
});
// 清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
)
);
self.clients.claim();
});
// 拦截请求:缓存优先
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => cached || fetch(event.request))
);
});
页面中注册:
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
}
3. 常用缓存策略
不同资源适合不同策略:
| 策略 | 行为 | 适用资源 |
|---|---|---|
| Cache First | 优先读缓存,没有才请求网络 | 带 hash 的静态资源、图片 |
| Network First | 优先请求网络,失败回退缓存 | 接口数据、频繁更新的内容 |
| Stale-While-Revalidate | 先返回缓存,同时后台更新缓存 | 更新不频繁但要求快的资源 |
| Cache Only | 只读缓存 | 预缓存的固定资源 |
| Network Only | 只走网络 | 实时性要求极高的接口 |
四、核心之二:Web App Manifest
Manifest 是一个 JSON 文件,在 HTML 里通过 <link> 引入:
<link rel="manifest" href="/manifest.webmanifest" />
{
"name": "我的应用",
"short_name": "应用",
"description": "一个 PWA 示例",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#1f6feb",
"orientation": "portrait",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icons/icon-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
几个关键字段:
display: standalone:让应用以独立窗口打开,隐藏地址栏,接近原生体验。icons:至少提供 192×192 和 512×512;maskable图标适配不同系统的裁切形状。start_url/scope:定义从哪启动、管理哪些路径范围。theme_color/background_color:影响状态栏和启动画面的颜色。
满足「有 manifest + 有 Service Worker + HTTPS」后,浏览器才会显示「安装到桌面」的入口(各浏览器判断标准略有差异)。
五、在 webpack 中落地
手写 Service Worker 要自己处理缓存清单、版本更新,容易出错。工程上更推荐用 Workbox,它是 Google 出品的 PWA 工具库,并提供了 webpack 插件。
1. 安装
npm install workbox-webpack-plugin --save-dev
2. 两种使用方式
GenerateSW:不写 SW,插件根据配置自动生成(适合大多数场景)。
const { GenerateSW } = require('workbox-webpack-plugin');
module.exports = {
plugins: [
new GenerateSW({
clientsClaim: true, // 新 SW 立即接管
skipWaiting: true, // 跳过 waiting
cleanupOutdatedCaches: true,
runtimeCaching: [
{
// 接口:网络优先
urlPattern: /\/api\//,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
networkTimeoutSeconds: 5,
expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 },
},
},
{
// 图片:缓存优先
urlPattern: /\.(?:png|jpg|jpeg|svg|webp)$/,
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 30 },
},
},
],
}),
],
};
它会自动把 webpack 产出的资源加入预缓存清单,并生成带 runtime 缓存策略的 service-worker.js。
InjectManifest:自己写 SW(需要更复杂的逻辑时),插件只负责把预缓存清单注入进去。
const { InjectManifest } = require('workbox-webpack-plugin');
module.exports = {
plugins: [
new InjectManifest({
swSrc: './src/sw.js', // 自己写的 SW
swDest: 'service-worker.js',
}),
],
};
src/sw.js 里用 precacheAndRoute(self.__WB_MANIFEST) 接入注入的清单。
3. 注册 Service Worker
// 开发时判断,避免在本地调试缓存干扰
if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js');
});
}
4. 生成 manifest
可以用 webpack-pwa-manifest 自动生成图标和 manifest:
npm install webpack-pwa-manifest --save-dev
const WebpackPwaManifest = require('webpack-pwa-manifest');
module.exports = {
plugins: [
new WebpackPwaManifest({
name: '我的应用',
short_name: '应用',
start_url: '/',
display: 'standalone',
theme_color: '#1f6feb',
background_color: '#ffffff',
icons: [
{ src: 'src/assets/logo.png', sizes: [192, 512], destination: 'icons' },
],
}),
],
};
也可以把写好的 manifest.webmanifest 和图标放进 public,用 copy-webpack-plugin 原样拷贝,再用 HtmlWebpackPlugin 注入 <link rel="manifest">。
六、注意事项
- 必须 HTTPS(
localhost例外)。Service Worker 在 http 下不会注册,这是硬性安全要求。 - 调试要在 Application 面板。Chrome DevTools 的 Application → Service Workers 可以更新、注销、模拟离线;Cache Storage 可以查看缓存内容。
- 缓存要能更新。用了
[contenthash]的资源更新后文件名会变,预缓存清单随之更新;但要处理好旧缓存清理(cleanupOutdatedCaches)和 SW 更新时机,否则用户可能一直看到旧版本。 - 别缓存不该缓存的东西。把登录接口、实时数据缓存住会导致用户看到过期内容;接口通常用 Network First。
- 开发环境容易踩缓存坑。本地调试时缓存会造成「改了没生效」的错觉,建议仅在生产环境注册 SW,或使用 DevTools 的 Bypass for network。
- 兼容性:现代浏览器普遍支持 Service Worker(Safari 自 11.1 起支持),但仍需做好「不支持时退化为普通网页」的处理。
七、完整 webpack 配置示例
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { GenerateSW } = require('workbox-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
publicPath: '/',
clean: true,
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
new CopyWebpackPlugin({
patterns: [
{ from: 'public/manifest.webmanifest', to: 'manifest.webmanifest' },
{ from: 'public/icons', to: 'icons' },
],
}),
new GenerateSW({
clientsClaim: true,
skipWaiting: true,
cleanupOutdatedCaches: true,
runtimeCaching: [
{
urlPattern: /\/api\//,
handler: 'NetworkFirst',
options: { cacheName: 'api-cache', networkTimeoutSeconds: 5 },
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|webp)$/,
handler: 'CacheFirst',
options: { cacheName: 'image-cache' },
},
],
}),
],
};
小结
| 问题 | 答案 |
|---|---|
| PWA 是什么 | 一组让 Web 拥有接近原生体验的技术组合 |
| 核心组成 | Service Worker + Web App Manifest + HTTPS |
| 解决什么 | 离线可用、秒开、可安装、沉浸式、推送 |
| 关键概念 | Service Worker 生命周期、缓存策略、manifest 字段 |
| 工程化方案 | Workbox(workbox-webpack-plugin) |
| 首要前提 | HTTPS |
一句话:Service Worker 让它「可靠、快」,Manifest 让它「能被安装」,HTTPS 让这一切成立。 三者齐备,网页才真正进阶成一个可离线、可安装、可推送的 PWA。