你有没有想过,为什么有些网站用起来就像 App 一样?点开很快,还能离线访问,甚至还能发通知?没错,这就是传说中的 PWA------渐进式 Web 应用。它是一种让网站"进化"的技术,不需要你下载 App 却能享受到原生应用的体验。很多大厂,比如 Twitter、Instagram、甚至阿里巴巴,都用 PWA 提升了移动端用户体验和留存率。其实实现 PWA 并不复杂,只需要搞懂几个核心概念:manifest.json
、Service Worker
和缓存策略。只要你熟悉基本的前端技术,基本几步就能给自己的网站"变身"。
PWA 简介
渐进式 Web 应用(Progressive Web App,PWA)是一种结合 Web 和原生应用优势的技术,旨在提供快速、可靠、可交互的用户体验。PWA 的核心特性包括:
- 渐进增强:在支持 PWA 的浏览器中提供增强功能,老旧浏览器仍可正常访问。
- 离线支持:通过 Service Worker 缓存资源,实现离线或弱网环境下的可用性。
- 可安装:通过 Web App Manifest 提供原生应用的安装体验,支持桌面和移动设备。
- 推送通知:使用 Web Push API 实现消息推送,提升用户 engagement。
- 响应式设计:适配多种设备和屏幕尺寸。
- 安全性:强制使用 HTTPS 确保数据安全。
PWA 的技术栈:
- Service Worker:浏览器后台线程,管理缓存和网络请求。
- Web App Manifest:JSON 文件,定义应用元数据(如图标、名称)。
- Web Push API:与推送服务交互,发送通知。
- HTTPS:确保安全通信。
前端价值:
- 提升用户体验(快速加载、离线访问)。
- 降低开发成本(跨平台,无需原生开发)。
- 提高留存率(推送通知、可安装性)。
本教程基于 2025 年 5 月 26 日的浏览器标准(Chrome 125、Firefox 126、Safari 18 等),涵盖 PWA 的完整实现流程。
PWA 基础
环境准备
技术要求
- 浏览器:支持 Service Worker 和 Web App Manifest(主流浏览器均支持)。
- Node.js:用于开发和构建(推荐 18.x 或 20.x)。
- HTTPS :本地开发可使用
localhost
或自签名证书,生产环境需有效证书。 - 工具 :
- Workbox:Google 提供的 PWA 工具库,简化 Service Worker 配置。
- Lighthouse:Chrome DevTools 的 PWA 审计工具。
安装工具
-
Node.js:
bashnvm install 18 nvm use 18
-
Workbox CLI:
bashnpm install -g workbox-cli
-
HTTPS 开发环境 (使用
mkcert
生成本地证书):bashbrew install mkcert mkcert localhost
项目初始化
创建一个简单的 Web 项目:
bash
mkdir pwa-demo
cd pwa-demo
npm init -y
npm install workbox-cli express
项目结构:
java
pwa-demo/
├── public/
│ ├── index.html
│ ├── styles.css
│ ├── app.js
│ ├── manifest.json
│ └── sw.js
├── server.js
└── package.json
第一个 PWA
1. 创建 HTML 文件
public/index.html
:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My PWA</title>
<link rel="stylesheet" href="/styles.css">
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#007bff">
</head>
<body>
<h1>Welcome to My PWA</h1>
<p>This is a Progressive Web App!</p>
<script src="/app.js"></script>
</body>
</html>
分析:
viewport
确保响应式。manifest.json
链接 Web App Manifest。theme-color
设置浏览器 UI 颜色。
2. 创建 Web App Manifest
public/manifest.json
:
json
{
"name": "My PWA",
"short_name": "PWA",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#007bff",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
分析:
name
和short_name
定义应用名称。start_url
指定启动页面。display: standalone
提供原生应用体验。icons
提供不同尺寸的图标。
生成图标 :
使用工具(如 favicon.io
)生成 192x192 和 512x512 的 PNG 图标,放置在 public/
。
3. 注册 Service Worker
public/sw.js
:
javascript
self.addEventListener('install', event => {
event.waitUntil(
caches.open('v1').then(cache => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/app.js',
'/manifest.json',
'/icon-192.png',
'/icon-512.png'
]);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request).then(fetchResponse => {
caches.open('v1').then(cache => {
cache.put(event.request, fetchResponse.clone());
});
return fetchResponse;
});
})
);
});
public/app.js
:
javascript
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('Service Worker registered', reg))
.catch(err => console.error('Service Worker registration failed', err));
}
分析:
install
事件缓存核心资源。fetch
事件实现"缓存优先,网络更新"策略。app.js
注册 Service Worker。
4. 配置服务器
server.js
:
javascript
const express = require('express');
const path = require('path');
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
app.listen(3000, () => console.log('Server running on http://localhost:3000'));
启动:
bash
node server.js
5. 测试 PWA
- 访问
http://localhost:3000
。 - 使用 Chrome DevTools 的 Lighthouse 审计:
- 打开 DevTools(F12)。
- 切换到 "Lighthouse" 面板。
- 勾选 "Progressive Web App",生成报告。
- 结果 :
- 确认离线支持:断开网络,刷新页面。
- 确认可安装性:浏览器显示"安装"提示。
逐步分析:
- HTML 提供基本结构,Manifest 定义应用元数据。
- Service Worker 缓存资源,支持离线访问。
- Express 服务器托管静态文件。
- Lighthouse 验证 PWA 合规性。
面试题 1:PWA 核心特性
问题:PWA 的核心特性是什么?如何实现离线支持?
答案:
- 特性:离线支持、可安装、推送通知、响应式、安全。
- 离线支持 :
-
使用 Service Worker 缓存资源:
javascriptself.addEventListener('install', event => { event.waitUntil(caches.open('v1').then(cache => cache.addAll(['/', '/index.html']))); });
-
拦截
fetch
请求,返回缓存或网络响应。
-
PWA 核心功能实现
1. 高级 Service Worker 配置(使用 Workbox)
Workbox 简化 Service Worker 的开发,提供预缓存、运行时缓存等功能。
安装 Workbox
bash
npm install workbox-build
生成 Service Worker
workbox-config.js
:
javascript
module.exports = {
globDirectory: 'public/',
globPatterns: ['**/*.{html,css,js,png,json}'],
swDest: 'public/sw.js',
clientsClaim: true,
skipWaiting: true
};
生成命令:
bash
npx workbox generateSW workbox-config.js
生成的 sw.js
(简化版):
javascript
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-sw.js');
workbox.precaching.precacheAndRoute([
{ url: '/', revision: '1' },
{ url: '/index.html', revision: '1' },
// 其他资源
]);
workbox.routing.registerRoute(
({ request }) => request.destination === 'image',
new workbox.strategies.CacheFirst({
cacheName: 'images',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 30 * 24 * 60 * 60 // 30 天
})
]
})
);
分析:
precacheAndRoute
预缓存指定资源。CacheFirst
策略优先使用缓存,适合静态资源。ExpirationPlugin
限制缓存大小和有效期。
动态缓存
修改 sw.js
:
javascript
workbox.routing.registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new workbox.strategies.NetworkFirst({
cacheName: 'api',
plugins: [
new workbox.cacheableResponse.CacheableResponsePlugin({
statuses: [200]
})
]
})
);
分析:
NetworkFirst
优先尝试网络,失败则用缓存。- 适合动态 API 数据。
面试题 2:Workbox 缓存策略
问题:Workbox 的常见缓存策略有哪些?适合的场景?
答案:
- CacheFirst:缓存优先,适合静态资源(如图片、CSS)。
- NetworkFirst:网络优先,适合动态数据(如 API)。
- StaleWhileRevalidate:返回缓存,同时更新,适合频繁更新的资源。
- NetworkOnly/CacheOnly:仅网络或仅缓存,适合特殊场景。
2. 推送通知
推送通知通过 Web Push API 实现,需要后端支持。
配置 VAPID 密钥
安装 web-push
:
bash
npm install web-push
生成密钥:
javascript
const
高级 Service Worker 功能
后台同步(Background Sync)
后台同步允许在网络恢复时执行任务,如发送离线保存的数据。
实现后台同步
public/app.js
(更新):
javascript
async function saveDataOffline(data) {
if (!navigator.onLine) {
// 保存到 IndexedDB
const db = await openDB('pwa-store', 1, {
upgrade(db) {
db.createObjectStore('sync-data', { autoIncrement: true });
}
});
await db.put('sync-data', data);
// 注册同步任务
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('sync-data');
} else {
await fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
}
// IndexedDB 封装
function openDB(name, version, upgrade) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, version);
request.onupgradeneeded = event => upgrade(event.target.result);
request.onsuccess = event => resolve(event.target.result);
request.onerror = () => reject(request.error);
});
}
public/sw.js
(更新):
javascript
self.addEventListener('sync', event => {
if (event.tag === 'sync-data') {
event.waitUntil(syncData());
}
});
async function syncData() {
const db = await openDB('pwa-store', 1);
const tx = db.transaction('sync-data', 'readonly');
const store = tx.objectStore('sync-data');
const data = await store.getAll();
for (const item of data) {
await fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item)
});
await store.delete(item.id);
}
}
后端 (server.js
更新):
javascript
app.post('/api/save', (req, res) => {
// 保存数据到数据库
console.log('Saved:', req.body);
res.status(200).send();
});
逐步分析:
- 离线时,数据存储到 IndexedDB,注册
sync
任务。 - Service Worker 监听
sync
事件,网络恢复时发送数据。 - 后端接收数据,客户端删除已同步的记录。
前端应用:
- 离线表单提交(如评论、订单)。
- 消息队列同步(如聊天应用)。
面试题 4:后台同步的优势
问题:后台同步与定时轮询相比有何优势?
答案:
- 优势 :
- 仅在网络恢复时触发,节省资源。
- 由浏览器管理,自动优化时机。
- 支持离线场景,无需用户在线。
- 定时轮询:持续请求,消耗流量和电池,离线无效。
周期性同步(Periodic Sync)
周期性同步适合定期更新内容,如新闻 feed。
实现周期性同步
public/app.js
(更新):
javascript
async function registerPeriodicSync() {
const reg = await navigator.serviceWorker.ready;
if ('periodicSync' in reg) {
const status = await navigator.permissions.query({ name: 'periodic-background-sync' });
if (status.state === 'granted') {
await reg.periodicSync.register('update-content', {
minInterval: 24 * 60 * 60 * 1000 // 每天
});
}
}
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(reg => {
registerPeriodicSync();
});
}
public/sw.js
(更新):
javascript
self.addEventListener('periodicsync', event => {
if (event.tag === 'update-content') {
event.waitUntil(updateContent());
}
});
async function updateContent() {
const response = await fetch('/api/content');
const data = await response.json();
await caches.open('content').then(cache => cache.put('/content', new Response(JSON.stringify(data))));
}
后端:
javascript
app.get('/api/content', (req, res) => {
res.json({ articles: ['News 1', 'News 2'] });
});
逐步分析:
- 检查
periodicSync
支持和权限。 - 注册每天同步任务。
- Service Worker 定期获取内容,缓存到
content
存储。
注意:
- 周期性同步需用户授予权限,浏览器限制执行频率。
- 目前仅 Chrome 支持,需降级方案(如定时
fetch
)。
面试题 5:周期性同步限制
问题:周期性同步的限制是什么?如何降级?
答案:
-
限制:
- 仅部分浏览器支持(Chrome)。
- 需用户权限。
- 最小间隔由浏览器决定(通常 24 小时)。
-
降级:
javascriptif (!('periodicSync' in reg)) { setInterval(async () => { const response = await fetch('/api/content'); const data = await response.json(); // 更新 UI }, 24 * 60 * 60 * 1000); }
推送通知高级场景
富文本通知与动作按钮
增强通知的用户交互性。
实现富文本通知
public/sw.js
(更新):
javascript
self.addEventListener('push', event => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icon-192.png',
badge: '/badge.png',
image: '/preview.jpg',
actions: [
{ action: 'view', title: 'View Details' },
{ action: 'dismiss', title: 'Dismiss' }
],
data: { url: data.url }
})
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
if (event.action === 'view') {
clients.openWindow(event.notification.data.url);
}
});
后端推送 (server.js
更新):
javascript
app.post('/api/sendNotification', (req, res) => {
const payload = JSON.stringify({
title: 'New Article',
body: 'Check out our latest post!',
url: 'https://example.com/article',
});
Promise.all(subscriptions.map(sub => webPush.sendNotification(sub, payload)))
.then(() => res.status(200).send())
.catch(err => res.status(500).send(err));
});
逐步分析:
- 通知包含图片(
image
)、徽章(badge
)和动作按钮。 notificationclick
事件处理用户交互。- 动作
view
打开指定 URL。
前端应用:
- 电商促销通知。
- 新闻推送。
数据驱动通知
根据用户数据动态生成通知。
示例
前端订阅 (app.js
更新):
javascript
async function subscribeUser(reg) {
const publicKey = await fetch('/api/vapidPublicKey').then(res => res.text());
const subscription = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey)
});
await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
subscription,
userId: 'user123' // 假设用户 ID
})
});
}
后端 (server.js
更新):
javascript
const subscriptions = new Map();
app.post('/api/subscribe', (req, res) => {
subscriptions.set(req.body.userId, req.body.subscription);
res.status(201).send();
});
app.post('/api/sendUserNotification', (req, res) => {
const { userId, message } = req.body;
const subscription = subscriptions.get(userId);
if (subscription) {
const payload = JSON.stringify({
title: 'Personalized Message',
body: message,
url: '/dashboard'
});
webPush.sendNotification(subscription, payload)
.then(() => res.status(200).send())
.catch(err => res.status(500).send(err));
} else {
res.status(404).send('User not found');
}
});
测试:
bash
curl -X POST http://localhost:3000/api/sendUserNotification \
-H "Content-Type: application/json" \
-d '{"userId": "user123", "message": "Your order has shipped!"}'
分析:
- 按用户 ID 存储订阅信息。
- 后端发送个性化通知。
PWA 安全性
HTTPS 配置
PWA 强制使用 HTTPS,生产环境需有效证书。
使用 Let's Encrypt
安装 Certbot:
bash
sudo apt install certbot python3-certbot-nginx
获取证书:
bash
sudo certbot --nginx -d example.com
Nginx 配置:
nginx
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
root /var/www/pwa/public;
location / {
try_files $uri $uri/ /index.html;
}
}
分析:
- Let's Encrypt 提供免费证书,自动续期。
- Nginx 重定向 HTTP 到 HTTPS。
权限管理
限制 Service Worker 和通知权限。
检查权限
app.js
:
javascript
async function checkPermissions() {
const swPermission = await navigator.permissions.query({ name: 'service-worker' });
const pushPermission = await navigator.permissions.query({ name: 'push', userVisibleOnly: true });
console.log('Service Worker:', swPermission.state);
console.log('Push:', pushPermission.state);
}
分析:
- 动态检查权限状态,提示用户授权。
数据加密
加密本地存储的数据。
使用 Web Crypto API
app.js
:
javascript
async function encryptData(data) {
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(JSON.stringify(data));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
encoded
);
return { encrypted: new Uint8Array(encrypted), iv, key };
}
分析:
- AES-GCM 加密确保数据安全。
- 适合离线存储敏感信息。
复杂案例:离线文档编辑器
实现
index.html
:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Offline Editor</title>
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#007bff">
</head>
<body>
<textarea id="editor"></textarea>
<button id="save">Save</button>
<script src="/app.js"></script>
</body>
</html>
app.js
:
javascript
import { openDB } from 'idb';
const dbPromise = openDB('editor-store', 1, {
upgrade(db) {
db.createObjectStore('documents', { keyPath: 'id', autoIncrement: true });
}
});
document.getElementById('save').addEventListener('click', async () => {
const content = document.getElementById('editor').value;
const db = await dbPromise;
await db.put('documents', { content, timestamp: Date.now() });
if (!navigator.onLine) {
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('sync-docs');
} else {
await syncDocs();
}
});
async function syncDocs() {
const db = await dbPromise;
const docs = await db.getAll('documents');
for (const doc of docs) {
await fetch('/api/saveDoc', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(doc)
});
await db.delete('documents', doc.id);
}
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(reg => {
reg.sync.register('sync-docs').catch(() => {});
});
}
sw.js
:
javascript
self.addEventListener('install', event => {
event.waitUntil(
caches.open('v1').then(cache => cache.addAll([
'/',
'/index.html',
'/manifest.json'
]))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => response || fetch(event.request))
);
});
self.addEventListener('sync', event => {
if (event.tag === 'sync-docs') {
event.waitUntil(syncDocs());
}
});
async function syncDocs() {
// 与 app.js 的 syncDocs 逻辑相同
}
后端 (server.js
更新):
javascript
app.post('/api/saveDoc', (req, res) => {
// 保存到数据库
console.log('Document saved:', req.body);
res.status(200).send();
});
分析:
- IndexedDB 存储离线文档。
- 后台同步上传文档。
- Service Worker 确保离线访问。
与 WebAssembly 整合
结合 WebAssembly 提升 PWA 性能。
示例:Markdown 渲染
Rust 代码 (src/lib.rs
):
rust
use wasm_bindgen::prelude::*;
use pulldown_cmark::{html, Parser};
#[wasm_bindgen]
pub fn render_markdown(md: &str) -> String {
let parser = Parser::new(md);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
html_output
}
编译:
bash
wasm-pack build --target web
前端 (app.js
更新):
javascript
import init, { render_markdown } from './pkg/markdown.js';
async function initEditor() {
await init();
const editor = document.getElementById('editor');
const preview = document.createElement('div');
document.body.appendChild(preview);
editor.addEventListener('input', () => {
preview.innerHTML = render_markdown(editor.value);
});
}
initEditor();
分析:
- WASM 加速 Markdown 解析。
- PWA 提供离线支持。
企业级实践(进阶)
微前端与 PWA
Qiankun 配置:
javascript
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'pwaEditor',
entry: '//localhost:3001',
container: '#editorContainer',
activeRule: '/editor'
}
]);
start();
Service Worker(子应用):
javascript
self.addEventListener('install', event => {
event.waitUntil(
caches.open('editor-v1').then(cache => cache.addAll([
'/editor',
'/editor/index.html'
]))
);
});
分析:
- 微前端分离 PWA 模块。
- 各模块独立缓存。
Serverless 部署
使用 AWS Amplify:
bash
amplify init
amplify add hosting
amplify publish
分析:
- Serverless 简化 PWA 部署。
- 自动配置 HTTPS 和 CDN。
Kubernetes 高级部署
自动扩缩容:
bash
kubectl create -f - <<EOF
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: pwa-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: pwa-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80
EOF
分析:
- 根据 CPU 使用率动态调整副本。
- 确保高流量下的稳定性。