渐进式 Web 应用(PWA)入门与开发指南
标签 :PWA、渐进式Web应用、Service Worker、Web App Manifest、前端开发
摘要:本文系统介绍什么是 PWA、具备哪些能力、优缺点是什么,以及如何从零实现「可安装」的 PWA(Manifest、Service Worker、安装提示),并附带可复用的代码示例与验证方式,方便日后复习与对外分享。
📑 目录
- [一、什么是 PWA](#一、什么是 PWA)
- [1.1 定义](#1.1 定义)
- [1.2 由来与标准](#1.2 由来与标准)
- [二、PWA 的核心能力与"功能清单"](#二、PWA 的核心能力与"功能清单")
- [2.1 官方常说的几大特性](#2.1 官方常说的几大特性)
- [2.2 三件技术"基础设施"](#2.2 三件技术"基础设施")
- [三、PWA 的优缺点(便于做技术选型)](#三、PWA 的优缺点(便于做技术选型))
- [3.1 优点](#3.1 优点)
- [3.2 缺点与限制](#3.2 缺点与限制)
- 四、从零到"可安装"的代码怎么搭
- [4.1 Web App Manifest](#4.1 Web App Manifest)
- [4.2 在 HTML 中引用 Manifest](#4.2 在 HTML 中引用 Manifest)
- [4.3 Service Worker:用 Workbox 做"可安装 + 简单缓存"](#4.3 Service Worker:用 Workbox 做"可安装 + 简单缓存")
- [4.4 注册 Service Worker(浏览器端)](#4.4 注册 Service Worker(浏览器端))
- [4.5 安装提示组件(引导用户点"安装")](#4.5 安装提示组件(引导用户点"安装"))
- [五、如何验证"算不算 PWA"](#五、如何验证"算不算 PWA")
- 六、开发时要注意什么
- 七、小结
一、什么是 PWA
1.1 定义
渐进式 Web 应用(Progressive Web App,PWA) 是使用 Web 技术 (HTML、CSS、JavaScript)开发,但在体验上接近 原生应用 的 Web 应用。
核心特点:
- 渐进式:在不支持高级特性的浏览器里,仍以普通网页形式可用;在支持的浏览器里,逐步获得「可安装、离线、推送」等能力。
- Web 应用:本质仍是网页,通过 URL 访问,无需上架应用商店即可分发。
💡 简单理解:用网页做的"假 App",但用起来像真 App。
1.2 由来与标准
- 2015 年:Google 提出 PWA 概念,随后 Chrome 率先支持。
- 标准组织 :核心能力依赖 W3C 与 WHATWG 标准:Service Worker、Web App Manifest 等。
- 浏览器支持:目前主流浏览器(Chrome、Firefox、Safari、Edge)均已支持 PWA 相关特性,只是支持程度略有差异。
二、PWA 的核心能力与"功能清单"
2.1 官方常说的几大特性
| 特性 | 含义 | 依赖技术 |
|---|---|---|
| 可安装(Installable) | 可"添加到主屏幕",有独立图标和启动方式 | Web App Manifest + 安装条件 |
| 离线可用(Offline-first) | 弱网/无网时仍能打开已访问过的内容 | Service Worker + 缓存策略 |
| 可靠(Reliable) | 加载快、少白屏、少报错 | Service Worker、预缓存、错误处理 |
| 体验佳(Engaging) | 全屏/ standalone、推送通知、快捷方式 | Manifest、Push API、Shortcuts |
| 安全(Secure) | 数据与脚本在安全上下文中运行 | 必须 HTTPS |
| 可发现、可链接 | 仍是网页,可被搜索、分享链接 | 无需改现有 URL 体系 |
📝 说明:上面这些就是 PWA 的"功能清单",我们说的"做 PWA",其实就是逐步实现其中几项或全部。
2.2 三件技术"基础设施"
要做成 PWA,至少需要这三块:
-
Web App Manifest(
manifest.json)告诉浏览器:名字、图标、主题色、从哪一页启动、是否全屏等,从而支持「添加到主屏幕」和安装体验。
-
Service Worker(SW)
在页面之外的独立线程里运行,可以拦截请求、读写缓存、在离线时返回缓存内容,是实现「离线、加速、后台同步、推送」的基础。
-
HTTPS
PWA 必须在安全源(HTTPS 或 localhost)下才能注册 Service Worker,这是硬性要求。
三、PWA 的优缺点(便于做技术选型)
3.1 优点
✅ 开发与维护成本低:一套 Web 代码,多端复用,无需分别维护 iOS/Android 原生 App。
✅ 无需应用商店:直接通过链接分享、推广,更新即部署即可,没有审核与上架周期。
✅ 体积小、安装快:用户"安装"的其实是入口 + 缓存策略,不必下载完整安装包。
✅ 可渐进增强:老浏览器当普通网页用,新浏览器自动获得安装、离线等能力。
✅ 利于 SEO 与分享:本质仍是网页,链接可被搜索、收藏、转发。
✅ 易于灰度与回滚:和普通前端发布一样,可按域名/路由做灰度或快速回滚。
3.2 缺点与限制
❌ 系统级能力有限:蓝牙、NFC、部分传感器、复杂后台任务等,仍不如原生。
❌ iOS / Safari 支持不完整:例如推送、后台同步等,在 iOS 上至今支持有限或行为不一致。
❌ 依赖浏览器实现:安装入口、安装条件、外观由各浏览器决定,无法完全统一。
❌ 需自己设计缓存与更新策略:Service Worker 一旦缓存过度或更新不当,会导致"一直用旧版"或"总是重新下载"等体验问题。
❌ 调试相对复杂:SW 在独立线程、持久存活,需要熟悉 Chrome DevTools 的 Application → Service Workers 等工具。
💡 提示:了解这些,可以在技术评审时更客观地评估:什么时候上 PWA、做到什么程度、和原生 App / TWA 如何分工。
四、从零到"可安装"的代码怎么搭
下面按 Manifest → Service Worker → 安装提示 → 在 HTML 里接好 的顺序,给出一套最小可运行示例。
4.1 Web App Manifest
在站点的静态资源目录 (如 public/)下放一个 manifest.json:
json
{
"name": "My App Full Name",
"short_name": "Short Name",
"description": "App description for install prompt and search engines",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#000000",
"icons": [
{
"src": "/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
}
]
}
常见字段说明:
name/short_name:安装后图标下显示的名称,空间有限时多用short_name。start_url:从主屏点击图标时打开的地址,一般设为/或首页路径。display:standalone表示"像 App 一样"单独窗口,没有浏览器地址栏;还可选fullscreen、minimal-ui、browser。icons:至少准备 192×192 和 512×512,系统会按场景选用。scope:约定哪些路径属于"本 PWA",超出范围的会用浏览器打开。
📝 提示 :准备好多尺寸图标(如 72、96、128、144、192、384、512)后,在
icons里都列出来即可。
4.2 在 HTML 中引用 Manifest
在 index.html 的 <head> 里加上:
html
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#000000" />
<!-- iOS Apple Touch Icon -->
<link rel="apple-touch-icon" href="/icons/icon-192x192.png" />
这样浏览器才能发现"这是一个可安装的 PWA"。
4.3 Service Worker:用 Workbox 做"可安装 + 简单缓存"
实际项目里一般不会手写裸的 Service Worker,而是用 Workbox 这类库。下面是一段示例代码:
使用说明:
- 使用 InjectManifest 时,会把构建产物(如 JS/CSS)注入到
self.__WB_MANIFEST,再交给precacheAndRoute做预缓存。 - 对图片、字体等静态资源,用 Cache First 即可;对 HTML/API 可按需用 Network First 等。
javascript
// sw-source.js - Workbox will inject __WB_MANIFEST during build
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
// Precache build assets (injected at build time)
precacheAndRoute(self.__WB_MANIFEST || []);
// Cache images and fonts with Cache First strategy
registerRoute(
({ request }) =>
request.destination === 'image' || request.destination === 'font',
new CacheFirst({
cacheName: 'static-resources',
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({
maxEntries: 100,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
}),
],
})
);
要点:
- precacheAndRoute:保证核心静态资源在安装阶段就进缓存,满足「可安装」和首屏速度。
- registerRoute:为某类请求指定缓存策略,这里只是示例,实际可按项目的接口与资源类型再细分。
⚠️ 注意 :若项目里已有别的 SW 逻辑(例如代理、长连接),记得在
fetch里先处理这些路径,再把剩余请求交给 Workbox,避免被缓存或错误处理。
4.4 注册 Service Worker(浏览器端)
在单页应用的入口(如 main.tsx 或 root.tsx)里,在合适的生命周期中注册 SW:
javascript
// Register Service Worker in app entry (main.tsx or root.tsx)
if ('serviceWorker' in navigator && import.meta.env.PROD) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js', { scope: '/' })
.then((reg) => {
console.log('SW registered:', reg.scope);
// Optional: Check for updates
reg.addEventListener('updatefound', () => {
const newWorker = reg.installing;
newWorker?.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New version available, prompt user to refresh
console.log('New SW available, please refresh');
}
});
});
})
.catch((err) => console.error('SW registration failed', err));
});
}
📝 说明 :很多项目会把 SW 的构建交给 Webpack 的
workbox-webpack-plugin(或 Vite 的对应插件),/sw.js由构建输出到public或根路径,这里只要保证路径一致即可。
4.5 安装提示组件(引导用户点"安装")
浏览器在满足一定条件时会触发 beforeinstallprompt,我们可以把该事件里的"安装能力"存起来,再在合适时机调 prompt() 弹出系统安装框。
React 组件示例:
tsx
// InstallPrompt.tsx
import { useState, useEffect } from 'react';
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}
export function InstallPrompt() {
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [showPrompt, setShowPrompt] = useState(false);
useEffect(() => {
// Already opened as PWA, no need to prompt
if (window.matchMedia('(display-mode: standalone)').matches) {
return;
}
// Check if user dismissed before (don't prompt again within 7 days)
const dismissed = localStorage.getItem('pwa-install-dismissed');
if (dismissed) {
const dismissedTime = parseInt(dismissed, 10);
const daysSinceDismissed = (Date.now() - dismissedTime) / (1000 * 60 * 60 * 24);
if (daysSinceDismissed < 7) return;
}
// Listen for install prompt event
const handler = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e as BeforeInstallPromptEvent);
setShowPrompt(true);
};
window.addEventListener('beforeinstallprompt', handler);
return () => window.removeEventListener('beforeinstallprompt', handler);
}, []);
// Handle install
const handleInstall = async () => {
if (!deferredPrompt) return;
try {
await deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
console.log('User accepted install prompt');
}
setDeferredPrompt(null);
setShowPrompt(false);
} catch (error) {
console.error('Install failed:', error);
}
};
// Handle dismiss
const handleDismiss = () => {
setShowPrompt(false);
localStorage.setItem('pwa-install-dismissed', String(Date.now()));
};
if (!showPrompt) return null;
return (
<div className="pwa-install-banner">
<p>安装到主屏幕,使用更方便</p>
<button onClick={handleInstall}>安装</button>
<button onClick={handleDismiss}>暂不</button>
</div>
);
}
要点:
- 用
display-mode: standalone判断是否已经是从主屏启动的 PWA,避免已安装用户还看到安装条。 - 用 localStorage 记录"用户点过暂不"的时间,避免频繁打扰。
- 只有在 HTTPS(或 localhost)且满足浏览器安装条件时 才会触发
beforeinstallprompt,本地调试时可借助 Chrome 的"Application → Manifest"里触发的"Add to home screen"做验证。
💡 使用 :把
<InstallPrompt />放在根布局或首页,即可在合适时机引导用户安装。
五、如何验证"算不算 PWA"
5.1 Chrome DevTools
-
Application → Manifest
检查能否正确解析、是否提示可安装。
-
Application → Service Workers
查看 SW 是否已注册、处于 active,以及缓存列表。
5.2 Lighthouse
在 Application 面板勾选"PWA"后跑一次 Lighthouse,会给出可安装性、SW、Manifest 等评分和修改建议。
5.3 真机测试
在手机浏览器里访问你的 HTTPS 地址,看是否有"添加到主屏幕"或"安装应用"的入口,以及安装后图标、启动页、全屏是否符合预期。
六、开发时要注意什么
⚠️ 必须 HTTPS(或 localhost):Service Worker 只能在安全源下注册,生产环境必须是 HTTPS。
⚠️ Manifest 的 start_url 要在 scope 内:否则安装后可能打不开或行为异常。
⚠️ 图标尺寸要够 :至少 192×192 和 512×512,且格式、路径与 manifest.json 里一致。
⚠️ SW 更新策略 :SW 会长期缓存,更新后要用 skipWaiting + clients.claim() 或在客户端提示用户刷新,否则用户可能一直用旧版。
⚠️ 缓存不要过猛:API、实时数据慎用长期缓存,避免"一直看到旧数据";静态资源可大胆 Cache First。
⚠️ beforeinstallprompt 不是一定会触发:浏览器会根据「访问频次、交互、HTTPS」等条件决定是否弹出安装条,本地调试时可结合 Lighthouse / Application 面板验证。
七、小结
看到"PWA"时,你会想到:
- 是什么:用 Web 技术做的、体验接近原生且可安装的 Web 应用。
- 靠什么实现:Web App Manifest + Service Worker + HTTPS,三者缺一不可。
- 能做什么:可安装、离线/弱网可用、更快更稳、推送与快捷方式(视浏览器支持而定)。
- 优缺点:开发效率高、无需上架、易于迭代;但系统能力与部分浏览器支持不如原生,需要自己设计缓存与更新策略。
- 代码上 :先写好
manifest.json并在 HTML 里挂好;再用 Workbox(或等价方案)在 SW 里做预缓存和简单路由策略;最后在页面上接beforeinstallprompt做安装引导即可。
💡 记住:把上面几条记牢,以后无论是做技术方案还是看别人博客里的「PWA 改造」,都会更容易抓到重点。
📚 写在最后
本文内容基于实际项目中的 PWA 实践整理,示例思路与项目内的 manifest.json、sw-source.js、InstallPrompt 保持一致,可直接用于 CSDN 发布或团队内部分享。
若要做「从零到可安装」的最小清单,可按第二节的功能清单 + 第四节的代码顺序,逐项补齐 Manifest、SW 注册、安装提示即可。
相关资源:
如果觉得文章有帮助,欢迎点赞、收藏、关注! 🎉