PWA
PWA (Progressive Web App)是一种渐进式Web应用程序,它是基于Web技术构建的应用程序,可以像本地应用程序一样运行在各种设备和平台上,包括桌面、移动设备和平板电脑等。
PWA具有许多本地应用程序的功能,例如离线访问、推送通知、主屏幕快捷方式等,同时还具有网页应用程序的优势,例如可访问性、跨平台性等。PWA可以通过Web浏览器安装到设备中,无需通过应用商店进行下载和安装。
PWA使用一组现代Web技术,例如Service Workers、Web App Manifests、HTTPS等,以提供一个类似于本地应用程序的体验。使用PWA,用户可以在设备上安装应用程序,无需访问应用商店,而且在网络不可用时,应用程序仍然可以正常工作。
PWA是一种新兴的Web技术,它正在成为Web应用程序的未来,许多公司和组织正在将其应用于其Web应用程序,以提供更好的用户体验和更高的用户参与度。

查看更多相关内容
PWA 开发中有哪些常用的工具和框架?如何使用它们?PWA 的开发需要使用一系列工具和框架来提高开发效率和代码质量。以下是常用的 PWA 开发工具和框架:
## 核心开发工具
### 1. Workbox
Workbox 是 Google 提供的 PWA 开发工具集,简化了 Service Worker 的开发。
#### 安装
```bash
npm install workbox-cli --global
npm install workbox-webpack-plugin --save-dev
```
#### 使用 Workbox CLI
```bash
# 生成 Service Worker
workbox generateSW workbox-config.js
# 预缓存文件
workbox wizard
```
#### Workbox 配置
```javascript
// workbox-config.js
module.exports = {
globDirectory: 'dist/',
globPatterns: [
'**/*.{html,js,css,png,jpg,jpeg,svg,woff,woff2}'
],
swDest: 'dist/sw.js',
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 30 * 24 * 60 * 60 // 30 days
}
}
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
expiration: {
maxEntries: 60,
maxAgeSeconds: 60 * 24 * 60 * 60 // 60 days
}
}
}
]
};
```
#### 在 Webpack 中使用 Workbox
```javascript
// webpack.config.js
const { GenerateSW } = require('workbox-webpack-plugin');
module.exports = {
plugins: [
new GenerateSW({
clientsClaim: true,
skipWaiting: true,
runtimeCaching: [
{
urlPattern: /\.(?:png|jpg|jpeg|svg)$/,
handler: 'CacheFirst',
options: {
cacheName: 'images',
expiration: {
maxEntries: 60
}
}
}
]
})
]
};
```
### 2. PWA Builder
PWA Builder 是 Microsoft 提供的工具,可以将 PWA 打包为原生应用。
```bash
# 安装 PWA Builder CLI
npm install -g pwabuilder
# 打包应用
pwabuilder package
```
### 3. Lighthouse
Lighthouse 是 Google 提供的网站性能和质量审计工具。
```bash
# 安装 Lighthouse
npm install -g lighthouse
# 运行审计
lighthouse https://your-pwa.com --view
```
#### 使用 Lighthouse CI
```bash
# 安装 Lighthouse CI
npm install -g @lhci/cli
# 初始化配置
lhci autorun
# 运行 CI
lhci autorun --collect.url=https://your-pwa.com
```
## 框架和库
### 1. React PWA
#### Create React App
```bash
# 创建 PWA 项目
npx create-react-app my-pwa --template cra-template-pwa
```
#### 使用 React PWA
```javascript
// src/serviceWorkerRegistration.js
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) return;
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
registerValidSW(swUrl, config);
});
}
}
```
### 2. Vue PWA
#### Vue CLI PWA 插件
```bash
# 创建 PWA 项目
vue create my-pwa
# 选择 PWA 插件
```
#### 配置 vue.config.js
```javascript
// vue.config.js
module.exports = {
pwa: {
name: 'My PWA',
themeColor: '#4DBA87',
msTileColor: '#000000',
appleMobileWebAppCapable: 'yes',
appleMobileWebAppStatusBarStyle: 'black',
workboxPluginMode: 'GenerateSW',
workboxOptions: {
runtimeCaching: [
{
urlPattern: /\.(?:png|jpg|jpeg|svg)$/,
handler: 'CacheFirst',
options: {
cacheName: 'images',
expiration: {
maxEntries: 60
}
}
}
]
}
}
};
```
### 3. Angular PWA
```bash
# 添加 PWA 支持
ng add @angular/pwa
```
#### 配置 ngsw-config.json
```json
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/manifest.webmanifest",
"/*.css",
"/*.js"
]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
]
}
}
],
"dataGroups": [
{
"name": "api-freshness",
"urls": [
"/api/**"
],
"cacheConfig": {
"maxSize": 100,
"maxAge": "3d",
"timeout": "10s",
"strategy": "freshness"
}
}
]
}
```
## 开发工具
### 1. Chrome DevTools
#### Service Worker 调试
```javascript
// 在 DevTools Console 中
// 查看所有 Service Worker
navigator.serviceWorker.getRegistrations().then(registrations => {
registrations.forEach(registration => {
console.log('SW:', registration);
});
});
// 取消注册 Service Worker
navigator.serviceWorker.getRegistrations().then(registrations => {
registrations.forEach(registration => {
registration.unregister();
});
});
```
#### Application 面板
- **Service Workers**:查看和管理 Service Worker
- **Cache Storage**:查看和管理缓存
- **Manifest**:查看和验证 Manifest 文件
- **Background Services**:查看后台服务状态
### 2. React DevTools
```bash
# 安装 React DevTools
npm install --save-dev react-devtools
```
### 3. Vue DevTools
```bash
# 安装 Vue DevTools
npm install --save-dev @vue/devtools
```
## 测试工具
### 1. Jest
```javascript
// sw.test.js
describe('Service Worker', () => {
beforeEach(() => {
return navigator.serviceWorker.register('/sw.js');
});
test('should cache static assets', async () => {
const cache = await caches.open('static-cache');
const response = await cache.match('/styles/main.css');
expect(response).toBeDefined();
});
});
```
### 2. Cypress
```javascript
// cypress/integration/pwa.spec.js
describe('PWA', () => {
it('should work offline', () => {
cy.visit('/');
// 模拟离线
cy.window().then((win) => {
win.navigator.serviceWorker.controller.postMessage({ type: 'OFFLINE' });
});
// 验证离线功能
cy.contains('Offline').should('be.visible');
});
});
```
### 3. Puppeteer
```javascript
// test-pwa.js
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// 检查 Service Worker
await page.goto('https://your-pwa.com');
const sw = await page.evaluate(() => {
return navigator.serviceWorker.getRegistration();
});
console.log('Service Worker:', sw);
await browser.close();
})();
```
## 构建工具
### 1. Webpack
```javascript
// webpack.config.js
const { InjectManifest } = require('workbox-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const WebpackPwaManifest = require('webpack-pwa-manifest');
module.exports = {
plugins: [
new CopyWebpackPlugin({
patterns: [
{ from: 'public/manifest.json', to: 'manifest.json' }
]
}),
new WebpackPwaManifest({
name: 'My PWA',
short_name: 'MyPWA',
description: 'My Progressive Web App',
background_color: '#ffffff',
theme_color: '#4DBA87',
icons: [
{
src: 'src/assets/icon.png',
sizes: [96, 128, 192, 256, 384, 512],
destination: 'icons'
}
]
}),
new InjectManifest({
swSrc: './src/sw.js',
swDest: 'sw.js'
})
]
};
```
### 2. Vite
```javascript
// vite.config.js
import { VitePWA } from 'vite-plugin-pwa';
export default {
plugins: [
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'apple-touch-icon.png'],
manifest: {
name: 'My PWA',
short_name: 'MyPWA',
description: 'My Progressive Web App',
theme_color: '#ffffff',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png'
}
]
},
workbox: {
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 30 * 24 * 60 * 60
}
}
}
]
}
})
]
};
```
## 部署工具
### 1. Netlify
```toml
# netlify.toml
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
Content-Security-Policy = "default-src 'self'"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
```
### 2. Vercel
```json
// vercel.json
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "X-XSS-Protection",
"value": "1; mode=block"
}
]
}
],
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}
```
### 3. Firebase Hosting
```json
// firebase.json
{
"hosting": {
"public": "dist",
"headers": [
{
"source": "**/*.@(eot|otf|ttf|ttc|woff|font.css)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000"
}
]
}
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}
```
## 监控和分析
### 1. Google Analytics
```javascript
// 在 Service Worker 中跟踪
self.addEventListener('fetch', event => {
if (navigator.sendBeacon) {
navigator.sendBeacon('/analytics', JSON.stringify({
url: event.request.url,
timestamp: Date.now()
}));
}
});
```
### 2. Sentry
```javascript
// 在 Service Worker 中捕获错误
self.addEventListener('error', event => {
Sentry.captureException(event.error);
});
self.addEventListener('unhandledrejection', event => {
Sentry.captureException(event.reason);
});
```
## 最佳实践
1. **使用 Workbox**:简化 Service Worker 开发
2. **自动化测试**:使用 Jest、Cypress 等工具
3. **性能监控**:使用 Lighthouse 定期审计
4. **错误追踪**:使用 Sentry 等工具
5. **CI/CD 集成**:自动化构建和部署
6. **代码分割**:使用 Webpack、Vite 等工具
7. **缓存策略**:根据资源类型选择合适的策略
8. **渐进增强**:确保在不支持 PWA 的浏览器中正常工作
服务端 · 2月18日 22:02
PWA 的安全性如何保障?有哪些关键的安全措施?PWA 的安全性非常重要,因为 PWA 可以像原生应用一样安装到设备上,并且可以访问更多设备功能。以下是 PWA 安全性的关键方面和最佳实践:
## 1. HTTPS 的必要性
### 为什么 PWA 必须使用 HTTPS
1. **Service Worker 要求**:Service Worker 只能在 HTTPS 环境下运行(localhost 除外)
2. **数据安全**:保护用户数据在传输过程中的安全
3. **信任度**:HTTPS 提供身份验证,防止中间人攻击
4. **浏览器要求**:现代浏览器要求 PWA 必须使用 HTTPS
5. **推送通知**:Web Push API 要求 HTTPS
### 配置 HTTPS
```nginx
# Nginx 配置示例
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# SSL 配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
# HTTP 重定向到 HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
```
## 2. Content Security Policy (CSP)
### CSP 的作用
CSP 可以帮助防止跨站脚本攻击(XSS)、点击劫持等安全威胁。
### 配置 CSP
```html
<!-- 通过 HTTP 头配置 -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'unsafe-inline' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
font-src 'self' https://fonts.gstatic.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';">
<!-- 通过服务器配置 -->
```
```nginx
# Nginx 配置
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; font-src 'self' https://fonts.gstatic.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';";
```
### CSP 指令说明
- `default-src`:默认策略
- `script-src`:脚本来源
- `style-src`:样式来源
- `img-src`:图片来源
- `connect-src`:网络请求来源
- `font-src`:字体来源
- `object-src`:插件来源
- `frame-ancestors`:允许嵌入的父页面
- `form-action`:表单提交目标
## 3. Service Worker 安全
### 限制 Service Worker 作用域
```javascript
// 将 Service Worker 放在根目录,控制整个应用
navigator.serviceWorker.register('/sw.js');
// 或者放在特定目录,只控制该目录
navigator.serviceWorker.register('/app/sw.js', {
scope: '/app/'
});
```
### 验证 Service Worker 更新
```javascript
// sw.js
self.addEventListener('install', event => {
event.waitUntil(
caches.open('my-cache').then(cache => {
return cache.addAll([
'/',
'/styles/main.css',
'/scripts/app.js'
]);
})
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
// 只删除属于当前应用的缓存
if (cacheName.startsWith('my-app-')) {
return caches.delete(cacheName);
}
})
);
})
);
});
```
### 防止缓存污染
```javascript
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 只缓存同源资源
if (url.origin !== self.location.origin) {
event.respondWith(fetch(event.request));
return;
}
// 处理同源请求
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
```
## 4. 数据安全
### 敏感数据加密
```javascript
// 使用 Web Crypto API 加密数据
async function encryptData(data, key) {
const encoder = new TextEncoder();
const encodedData = encoder.encode(data);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedData = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
encodedData
);
return {
iv: Array.from(iv),
data: Array.from(new Uint8Array(encryptedData))
};
}
async function decryptData(encryptedData, key) {
const iv = new Uint8Array(encryptedData.iv);
const data = new Uint8Array(encryptedData.data);
const decryptedData = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
data
);
const decoder = new TextDecoder();
return decoder.decode(decryptedData);
}
```
### 安全存储
```javascript
// 使用 IndexedDB 存储敏感数据
const dbPromise = idb.open('secure-db', 1, upgradeDB => {
upgradeDB.createObjectStore('secure-data', { keyPath: 'id' });
});
async function storeSecureData(id, data) {
const db = await dbPromise;
await db.put('secure-data', {
id: id,
data: data,
timestamp: Date.now()
});
}
// 避免在 localStorage 中存储敏感信息
// ❌ 不安全
localStorage.setItem('token', 'sensitive-token');
// ✅ 使用 IndexedDB 或加密后存储
```
## 5. 推送通知安全
### VAPID 密钥管理
```javascript
// 服务器端安全存储 VAPID 密钥
const vapidKeys = {
publicKey: process.env.VAPID_PUBLIC_KEY,
privateKey: process.env.VAPID_PRIVATE_KEY
};
// 不要在前端暴露私钥
// ❌ 错误
const privateKey = 'my-private-key'; // 不要这样做
// ✅ 正确:私钥只在服务器端使用
```
### 验证推送订阅
```javascript
// 服务器端验证订阅信息
async function validateSubscription(subscription) {
// 检查必需字段
if (!subscription.endpoint || !subscription.keys) {
throw new Error('Invalid subscription');
}
// 检查密钥格式
if (!subscription.keys.p256dh || !subscription.keys.auth) {
throw new Error('Invalid keys');
}
// 验证 endpoint 格式
try {
new URL(subscription.endpoint);
} catch (error) {
throw new Error('Invalid endpoint');
}
return true;
}
```
## 6. 跨域安全
### CORS 配置
```nginx
# Nginx 配置 CORS
location /api/ {
add_header 'Access-Control-Allow-Origin' 'https://your-pwa.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';
if ($request_method = 'OPTIONS') {
return 204;
}
}
```
```javascript
// 前端请求配置
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include', // 包含 cookies
headers: {
'Content-Type': 'application/json'
}
});
```
## 7. 认证和授权
### 使用 JWT 进行认证
```javascript
// 登录后获取 JWT
async function login(username, password) {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
// 安全存储 JWT
if (data.token) {
await storeSecureToken(data.token);
}
return data;
}
// 在请求中携带 JWT
async function fetchData() {
const token = await getSecureToken();
const response = await fetch('/api/data', {
headers: {
'Authorization': `Bearer ${token}`
}
});
return response.json();
}
```
### Token 刷新机制
```javascript
// 自动刷新过期 Token
async function fetchWithAuth(url, options = {}) {
let token = await getSecureToken();
// 检查 Token 是否即将过期
if (isTokenExpiringSoon(token)) {
token = await refreshToken();
}
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
});
// 如果 Token 无效,尝试刷新
if (response.status === 401) {
token = await refreshToken();
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
});
}
return response;
}
```
## 8. 安全最佳实践
### 1. 输入验证
```javascript
// 验证用户输入
function validateInput(input) {
// 防止 XSS
const sanitized = input.replace(/[<>]/g, '');
// 验证格式
if (!/^[a-zA-Z0-9]+$/.test(sanitized)) {
throw new Error('Invalid input');
}
return sanitized;
}
```
### 2. 防止 CSRF
```javascript
// 使用 CSRF Token
async function submitForm(data) {
const csrfToken = await getCsrfToken();
const response = await fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
});
return response.json();
}
```
### 3. 安全的 Service Worker 更新
```javascript
// 检查 Service Worker 更新
navigator.serviceWorker.addEventListener('controllerchange', () => {
// 通知用户应用已更新
showUpdateNotification();
});
// 提示用户刷新页面
function showUpdateNotification() {
const notification = document.createElement('div');
notification.textContent = '应用有新版本可用,点击刷新';
notification.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: #007bff;
color: white;
padding: 15px 20px;
border-radius: 4px;
cursor: pointer;
z-index: 9999;
`;
notification.addEventListener('click', () => {
window.location.reload();
});
document.body.appendChild(notification);
}
```
## 9. 安全审计和监控
### 使用 Lighthouse 进行安全审计
```bash
# 运行 Lighthouse 安全审计
lighthouse https://your-pwa.com --view --only-categories=security
```
### 监控安全事件
```javascript
// 监控可疑活动
function logSecurityEvent(event) {
fetch('/api/security-log', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
event: event.type,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href
})
});
}
// 监控 Service Worker 错误
navigator.serviceWorker.addEventListener('error', (event) => {
logSecurityEvent({ type: 'service-worker-error', error: event.error });
});
```
## 总结
PWA 安全性的关键点:
1. **必须使用 HTTPS**:Service Worker 和推送通知都要求 HTTPS
2. **配置 CSP**:防止 XSS 和其他注入攻击
3. **限制 Service Worker 作用域**:防止缓存污染
4. **安全存储敏感数据**:使用 IndexedDB 和加密
5. **验证推送订阅**:防止恶意推送
6. **正确配置 CORS**:防止跨域攻击
7. **使用安全的认证机制**:JWT + Token 刷新
8. **输入验证和输出编码**:防止注入攻击
9. **定期安全审计**:使用 Lighthouse 等工具
10. **监控安全事件**:及时发现和响应安全威胁
服务端 · 2月18日 21:58
PWA 如何实现更新机制?如何确保用户使用最新版本?PWA 的更新机制对于确保用户始终使用最新版本的应用非常重要。以下是 PWA 更新的完整流程和最佳实践:
## Service Worker 更新流程
### 1. 更新检测
浏览器会在以下情况检查 Service Worker 更新:
- 导航到应用页面时
- Service Worker 事件触发时(如 push、sync 等)
- 每 24 小时自动检查一次
### 2. 更新生命周期
```javascript
// sw.js
const CACHE_VERSION = 'v2';
const CACHE_NAME = `my-pwa-${CACHE_VERSION}`;
// 安装事件
self.addEventListener('install', event => {
console.log('Installing new Service Worker:', CACHE_VERSION);
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
return cache.addAll([
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png'
]);
})
.then(() => {
// 跳过等待,立即激活
return self.skipWaiting();
})
);
});
// 激活事件
self.addEventListener('activate', event => {
console.log('Activating new Service Worker:', CACHE_VERSION);
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
// 删除旧版本的缓存
if (cacheName.startsWith('my-pwa-') && cacheName !== CACHE_NAME) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
}).then(() => {
// 立即控制所有客户端
return self.clients.claim();
})
);
});
```
### 3. 更新通知用户
```javascript
// 在主线程中监听更新
let newWorker;
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
// 检查是否有新的 Service Worker
registration.addEventListener('updatefound', () => {
newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// 有新的 Service Worker 可用
showUpdateNotification();
}
});
});
});
}
// 显示更新通知
function showUpdateNotification() {
const notification = document.createElement('div');
notification.innerHTML = `
<div class="update-notification">
<span>应用有新版本可用</span>
<button id="update-btn">立即更新</button>
<button id="dismiss-btn">稍后</button>
</div>
`;
notification.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: #007bff;
color: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 9999;
font-family: Arial, sans-serif;
`;
document.body.appendChild(notification);
// 立即更新按钮
document.getElementById('update-btn').addEventListener('click', () => {
newWorker.postMessage({ action: 'skipWaiting' });
window.location.reload();
});
// 稍后按钮
document.getElementById('dismiss-btn').addEventListener('click', () => {
notification.remove();
});
}
```
## 手动触发更新
```javascript
// 手动检查更新
async function checkForUpdates() {
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
await registration.update();
console.log('Checked for updates');
}
}
}
// 定期检查更新(每小时)
setInterval(checkForUpdates, 60 * 60 * 1000);
// 在页面获得焦点时检查更新
window.addEventListener('focus', checkForUpdates);
```
## 缓存更新策略
### 1. 版本化缓存
```javascript
// 使用版本号管理缓存
const CACHE_VERSIONS = {
static: 'v1',
dynamic: 'v1',
images: 'v1'
};
const CACHE_NAMES = {
static: `static-${CACHE_VERSIONS.static}`,
dynamic: `dynamic-${CACHE_VERSIONS.dynamic}`,
images: `images-${CACHE_VERSIONS.images}`
};
// 更新特定类型的缓存
function updateCacheType(type) {
CACHE_VERSIONS[type] = 'v' + (parseInt(CACHE_VERSIONS[type].slice(1)) + 1);
CACHE_NAMES[type] = `${type}-${CACHE_VERSIONS[type]}`;
}
```
### 2. 智能缓存更新
```javascript
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 对于 HTML 文档,总是从网络获取最新版本
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.then(response => {
const responseClone = response.clone();
caches.open(CACHE_NAMES.dynamic).then(cache => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => caches.match(event.request))
);
}
// 对于静态资源,使用缓存优先
else if (url.pathname.match(/\.(css|js|png|jpg|jpeg|gif|svg|woff|woff2)$/)) {
event.respondWith(cacheFirst(event.request));
}
// 对于 API 请求,使用网络优先
else if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request));
}
});
```
## 预缓存更新
```javascript
// 在安装时预缓存关键资源
self.addEventListener('install', event => {
const CRITICAL_ASSETS = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/offline.html'
];
event.waitUntil(
caches.open(CACHE_NAMES.static)
.then(cache => {
return cache.addAll(CRITICAL_ASSETS);
})
);
});
// 在激活时更新预缓存
self.addEventListener('activate', event => {
event.waitUntil(
caches.open(CACHE_NAMES.static)
.then(cache => {
return cache.addAll([
'/styles/main.css',
'/scripts/app.js'
]);
})
);
});
```
## 后台同步更新
```javascript
// 注册后台同步
self.addEventListener('sync', event => {
if (event.tag === 'sync-updates') {
event.waitUntil(syncUpdates());
}
});
async function syncUpdates() {
try {
// 获取最新的资源列表
const response = await fetch('/api/updates');
const updates = await response.json();
// 更新缓存
const cache = await caches.open(CACHE_NAMES.dynamic);
for (const update of updates) {
await cache.add(update.url);
}
console.log('Background sync completed');
} catch (error) {
console.error('Background sync failed:', error);
}
}
// 在主线程中请求后台同步
async function requestBackgroundSync() {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-updates');
}
```
## 更新策略选择
### 1. 立即更新
```javascript
// 强制立即更新
function forceUpdate() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistration().then(registration => {
if (registration && registration.waiting) {
registration.waiting.postMessage({ action: 'skipWaiting' });
}
});
}
}
```
### 2. 延迟更新
```javascript
// 在用户空闲时更新
function updateWhenIdle() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistration().then(registration => {
if (registration) {
registration.update();
}
});
}
}
// 使用 requestIdleCallback
window.requestIdleCallback(updateWhenIdle);
```
### 3. 智能更新
```javascript
// 根据网络条件决定更新策略
function smartUpdate() {
if ('connection' in navigator) {
const connection = navigator.connection;
// 在 Wi-Fi 或快速网络时更新
if (connection.effectiveType === '4g' || connection.type === 'wifi') {
checkForUpdates();
}
// 在慢速网络时延迟更新
else {
setTimeout(checkForUpdates, 60000); // 1分钟后更新
}
}
}
```
## 更新最佳实践
### 1. 版本管理
```javascript
// 使用语义化版本号
const VERSION = {
major: 1,
minor: 2,
patch: 3
};
const CACHE_VERSION = `v${VERSION.major}.${VERSION.minor}.${VERSION.patch}`;
// 更新版本号
function incrementVersion(type) {
if (type === 'major') {
VERSION.major++;
VERSION.minor = 0;
VERSION.patch = 0;
} else if (type === 'minor') {
VERSION.minor++;
VERSION.patch = 0;
} else {
VERSION.patch++;
}
}
```
### 2. 回滚机制
```javascript
// 保留旧版本缓存
const MAX_CACHE_VERSIONS = 3;
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
// 获取所有版本号
const versions = cacheNames
.filter(name => name.startsWith('my-pwa-'))
.map(name => name.replace('my-pwa-', ''))
.sort()
.reverse();
// 删除旧版本,保留最近的几个版本
const versionsToDelete = versions.slice(MAX_CACHE_VERSIONS);
return Promise.all(
versionsToDelete.map(version => {
return caches.delete(`my-pwa-${version}`);
})
);
})
);
});
```
### 3. 更新通知
```javascript
// 提供详细的更新信息
function showDetailedUpdateNotification(updateInfo) {
const notification = document.createElement('div');
notification.innerHTML = `
<div class="update-notification">
<h3>新版本可用</h3>
<p>版本: ${updateInfo.version}</p>
<p>更新内容:</p>
<ul>
${updateInfo.changes.map(change => `<li>${change}</li>`).join('')}
</ul>
<button id="update-btn">立即更新</button>
<button id="dismiss-btn">稍后</button>
</div>
`;
document.body.appendChild(notification);
document.getElementById('update-btn').addEventListener('click', () => {
forceUpdate();
window.location.reload();
});
document.getElementById('dismiss-btn').addEventListener('click', () => {
notification.remove();
});
}
```
## 监控和调试
### 1. 更新日志
```javascript
// 记录更新事件
function logUpdateEvent(event) {
const logData = {
timestamp: Date.now(),
event: event.type,
version: CACHE_VERSION,
userAgent: navigator.userAgent
};
// 发送到服务器
fetch('/api/update-log', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(logData)
});
}
// 监听 Service Worker 事件
self.addEventListener('install', logUpdateEvent);
self.addEventListener('activate', logUpdateEvent);
```
### 2. 调试工具
```javascript
// 添加调试信息
if (location.hostname === 'localhost') {
self.addEventListener('install', event => {
console.log('[SW] Installing:', CACHE_VERSION);
});
self.addEventListener('activate', event => {
console.log('[SW] Activating:', CACHE_VERSION);
});
self.addEventListener('fetch', event => {
console.log('[SW] Fetch:', event.request.url);
});
}
```
## 总结
PWA 更新的关键点:
1. **版本管理**:使用版本号管理缓存
2. **更新检测**:定期检查 Service Worker 更新
3. **用户通知**:及时通知用户有新版本可用
4. **平滑更新**:提供良好的更新体验
5. **回滚机制**:保留旧版本以便回滚
6. **智能策略**:根据网络条件选择更新策略
7. **监控日志**:记录更新事件便于调试
8. **测试验证**:在不同条件下测试更新流程
服务端 · 2月18日 21:57
PWA 与原生应用相比有哪些优势和劣势?如何选择?PWA 与原生应用各有优劣,选择哪种技术栈取决于项目需求、目标用户和开发资源。以下是详细的对比分析:
## PWA 与原生应用的核心差异
### 1. 开发成本和维护
**PWA**
- 开发成本低:一套代码可以在多个平台运行
- 维护简单:更新只需部署到服务器,用户无需手动更新
- 开发周期短:使用 Web 技术栈,开发速度快
- 人才储备丰富:Web 开发人员更容易找到
**原生应用**
- 开发成本高:需要为 iOS 和 Android 分别开发
- 维护复杂:需要维护多套代码,更新需要应用商店审核
- 开发周期长:需要学习平台特定的开发语言和框架
- 人才要求高:需要专业的移动端开发人员
### 2. 用户体验
**PWA**
- 启动速度快:通过缓存实现快速加载
- 离线可用:可以离线访问缓存的内容
- 跨平台一致:在不同设备上提供一致的体验
- 安装便捷:无需应用商店,直接通过浏览器安装
- 占用空间小:通常比原生应用小很多
**原生应用**
- 性能更优:可以充分利用设备硬件性能
- 功能更全:可以访问更多设备功能和 API
- 交互更流畅:原生 UI 组件提供更好的交互体验
- 后台运行:可以在后台持续运行
- 推送通知:支持更丰富的推送通知功能
### 3. 功能访问
**PWA**
- 受限的设备访问:只能访问有限的设备功能
- 依赖浏览器:功能受限于浏览器支持
- 缓存限制:缓存大小受浏览器限制
- 推送通知:支持但功能相对有限
**原生应用**
- 完整的设备访问:可以访问摄像头、麦克风、GPS 等
- 系统级集成:可以与系统深度集成
- 无缓存限制:可以存储大量数据
- 丰富的推送:支持丰富的推送通知和后台任务
### 4. 分发和安装
**PWA**
- 无需审核:不需要应用商店审核
- 即时更新:更新后用户立即获得新版本
- 易于分享:可以通过 URL 直接分享
- 搜索引擎优化:可以被搜索引擎索引
- 安装门槛低:用户无需下载大文件
**原生应用**
- 需要审核:需要通过应用商店审核流程
- 更新延迟:更新需要用户手动下载安装
- 分发受限:主要通过应用商店分发
- 可发现性低:需要用户主动搜索
- 安装门槛高:需要下载较大的安装包
### 5. 性能表现
**PWA**
- 加载速度:首次加载较慢,后续加载快
- 运行性能:中等,受限于浏览器性能
- 内存占用:相对较低
- 电池消耗:相对较高(浏览器开销)
**原生应用**
- 加载速度:快,直接运行
- 运行性能:高,充分利用硬件
- 内存占用:可能较高
- 电池消耗:相对较低(优化更好)
## 选择 PWA 的场景
### 适合 PWA 的应用类型
1. **内容型应用**
- 新闻资讯
- 博客和文章
- 电商网站
- 企业官网
2. **工具型应用**
- 计算器
- 待办事项
- 笔记应用
- 在线表单
3. **轻量级社交应用**
- 简单的聊天应用
- 社区论坛
- 评论系统
4. **需要快速迭代的应用**
- MVP 产品
- 初创公司产品
- 需要频繁更新的应用
5. **预算有限的项目**
- 小型企业应用
- 个人项目
- 实验性项目
### PWA 的优势场景
- **需要跨平台**:一套代码多平台运行
- **需要快速上线**:开发周期短
- **需要频繁更新**:更新无需审核
- **用户流量主要来自搜索**:SEO 友好
- **需要离线功能**:缓存机制支持离线
- **安装门槛要低**:无需应用商店
## 选择原生应用的场景
### 适合原生应用的应用类型
1. **性能要求高的应用**
- 游戏
- 视频编辑
- 图像处理
- 实时通讯
2. **需要深度设备访问的应用**
- 相机应用
- 健康监测
- 导航应用
- IoT 控制
3. **复杂交互的应用**
- 社交媒体
- 即时通讯
- 生产力工具
- 企业应用
4. **需要后台运行的应用**
- 音乐播放器
- 位置追踪
- 数据同步
- 消息推送
5. **需要系统集成高的应用**
- 支付应用
- 银行应用
- 系统工具
- 安全应用
### 原生应用的优势场景
- **性能是关键**:需要最佳性能
- **需要完整设备功能**:访问所有设备 API
- **复杂交互**:需要流畅的原生交互
- **后台运行**:需要在后台持续运行
- **品牌要求高**:需要完全自定义的 UI
- **用户粘性高**:用户会频繁使用
## 混合方案
### Progressive Enhancement(渐进增强)
```javascript
// 检测 PWA 支持
if ('serviceWorker' in navigator && 'PushManager' in window) {
// 支持 PWA,启用 PWA 功能
registerServiceWorker();
enablePushNotifications();
} else {
// 不支持 PWA,使用传统 Web 功能
console.log('PWA not supported, using traditional web features');
}
```
### 使用 PWA 作为原生应用的补充
1. **PWA 作为试用版**:让用户先体验 PWA,再决定是否安装原生应用
2. **PWA 作为移动版**:原生应用提供完整功能,PWA 提供核心功能
3. **PWA 用于营销**:通过 PWA 吸引用户,引导安装原生应用
### 使用框架开发跨平台应用
- **React Native**:使用 React 开发原生应用
- **Flutter**:使用 Dart 开发跨平台应用
- **Ionic**:使用 Web 技术开发混合应用
- **Capacitor**:将 Web 应用打包为原生应用
## 决策框架
### 评估维度
1. **性能要求**
- 高性能需求 → 原生应用
- 中等性能需求 → PWA
2. **设备功能需求**
- 需要完整设备访问 → 原生应用
- 基础功能即可 → PWA
3. **开发预算**
- 预算充足 → 原生应用
- 预算有限 → PWA
4. **时间要求**
- 快速上线 → PWA
- 可以等待 → 原生应用
5. **更新频率**
- 频繁更新 → PWA
- 稳定更新 → 原生应用
6. **用户群体**
- 技术用户 → PWA
- 普通用户 → 原生应用
7. **商业模式**
- 应用商店分发 → 原生应用
- 网站引流 → PWA
### 决策流程
```
开始
↓
是否需要高性能?
├─ 是 → 原生应用
└─ 否 ↓
是否需要完整设备功能?
├─ 是 → 原生应用
└─ 否 ↓
预算是否充足?
├─ 是 → 考虑原生应用
└─ 否 ↓
是否需要快速上线?
├─ 是 → PWA
└─ 否 ↓
更新是否频繁?
├─ 是 → PWA
└─ 否 → 原生应用
```
## 实际案例
### PWA 成功案例
1. **Twitter Lite**
- 减少了 75% 的数据使用
- 加载时间减少了 30%
- 用户参与度提高了 65%
2. **Pinterest PWA**
- 核心用户参与度提高了 60%
- 广告收入增加了 44%
- 用户生成广告收入增加了 18%
3. **AliExpress PWA**
- 新用户转化率提高了 104%
- 每个会话的页面浏览量增加了 74%
- iOS Safari 上的转化率提高了 82%
### 原生应用成功案例
1. **Instagram**
- 复杂的图像处理
- 丰富的相机功能
- 流畅的交互体验
2. **Uber**
- 实时位置追踪
- 后台运行
- 复杂的地图交互
## 总结
### 选择 PWA 如果:
- 预算有限
- 需要快速上线
- 需要频繁更新
- 目标用户主要通过搜索发现
- 应用功能相对简单
- 需要跨平台支持
### 选择原生应用如果:
- 性能是关键因素
- 需要访问完整的设备功能
- 需要复杂的交互体验
- 需要在后台运行
- 有充足的开发预算
- 用户粘性高,会频繁使用
### 考虑混合方案如果:
- 想要降低风险
- 需要逐步迭代
- 目标用户群体多样化
- 需要测试市场反应
最终的选择应该基于项目的具体需求、目标用户、预算和时间表进行综合评估。
服务端 · 2月18日 21:55
如何优化 PWA 的性能?有哪些关键的性能优化策略?PWA 的性能优化对于提供良好的用户体验至关重要。以下是全面的性能优化策略:
## 1. 资源加载优化
### 预缓存关键资源
```javascript
// sw.js
const CACHE_NAME = 'my-pwa-v1';
const CRITICAL_ASSETS = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(CRITICAL_ASSETS))
.then(() => self.skipWaiting())
);
});
```
### 懒加载非关键资源
```javascript
// 图片懒加载
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
// 组件懒加载
const LazyComponent = React.lazy(() => import('./LazyComponent'));
```
### 代码分割
```javascript
// 使用动态 import 进行代码分割
async function loadFeature() {
const module = await import('./feature.js');
module.init();
}
// React 中的代码分割
const Home = React.lazy(() => import('./Home'));
const About = React.lazy(() => import('./About'));
```
## 2. 缓存策略优化
### 智能缓存策略
```javascript
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 静态资源:缓存优先
if (url.pathname.match(/\.(css|js|png|jpg|jpeg|gif|svg|woff|woff2)$/)) {
event.respondWith(cacheFirst(event.request));
}
// API 请求:网络优先
else if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request));
}
// HTML 文档:网络优先,失败时返回缓存
else if (event.request.mode === 'navigate') {
event.respondWith(networkFirstWithFallback(event.request));
}
// 其他:缓存同时更新
else {
event.respondWith(staleWhileRevalidate(event.request));
}
});
function cacheFirst(request) {
return caches.match(request).then(response => {
return response || fetch(request).then(networkResponse => {
const responseClone = networkResponse.clone();
caches.open('static-cache').then(cache => {
cache.put(request, responseClone);
});
return networkResponse;
});
});
}
function networkFirst(request) {
return fetch(request).then(networkResponse => {
const responseClone = networkResponse.clone();
caches.open('dynamic-cache').then(cache => {
cache.put(request, responseClone);
});
return networkResponse;
}).catch(() => caches.match(request));
}
function staleWhileRevalidate(request) {
return caches.match(request).then(cachedResponse => {
const fetchPromise = fetch(request).then(networkResponse => {
caches.open('dynamic-cache').then(cache => {
cache.put(request, networkResponse.clone());
});
return networkResponse;
});
return cachedResponse || fetchPromise;
});
}
```
### 缓存版本管理
```javascript
const CACHE_VERSION = 'v2';
const CACHE_NAME = `my-pwa-${CACHE_VERSION}`;
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME && cacheName.startsWith('my-pwa-')) {
return caches.delete(cacheName);
}
})
);
}).then(() => self.clients.claim())
);
});
```
## 3. 图片优化
### 使用现代图片格式
```html
<picture>
<source srcset="image.webp" type="image/webp">
<source srcset="image.jpg" type="image/jpeg">
<img src="image.jpg" alt="Description" loading="lazy">
</picture>
```
### 响应式图片
```html
<img
src="image-small.jpg"
srcset="image-small.jpg 480w,
image-medium.jpg 768w,
image-large.jpg 1024w"
sizes="(max-width: 480px) 480px,
(max-width: 768px) 768px,
1024px"
alt="Description"
loading="lazy"
>
```
### 图片压缩和优化
```javascript
// 使用 sharp 库压缩图片
const sharp = require('sharp');
async function optimizeImage(inputPath, outputPath) {
await sharp(inputPath)
.resize(800, 600, { fit: 'inside' })
.webp({ quality: 80 })
.toFile(outputPath);
}
```
## 4. JavaScript 优化
### 减少包体积
```javascript
// 使用 Tree Shaking
// 只导入需要的函数
import { debounce } from 'lodash-es';
// 避免导入整个库
// import _ from 'lodash'; // ❌ 避免
```
### 使用 Web Workers
```javascript
// 主线程
const worker = new Worker('worker.js');
worker.postMessage({ data: largeDataSet });
worker.onmessage = (event) => {
console.log('Processed data:', event.data);
};
// worker.js
self.onmessage = (event) => {
const result = processData(event.data.data);
self.postMessage(result);
};
```
### 防抖和节流
```javascript
// 防抖
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// 节流
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 使用示例
window.addEventListener('resize', debounce(handleResize, 300));
window.addEventListener('scroll', throttle(handleScroll, 100));
```
## 5. CSS 优化
### 关键 CSS 内联
```html
<style>
/* 关键 CSS */
body { margin: 0; font-family: Arial; }
.header { background: #333; color: white; }
</style>
<link rel="preload" href="styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles/main.css"></noscript>
```
### CSS 压缩和优化
```javascript
// 使用 cssnano 压缩 CSS
const cssnano = require('cssnano');
const postcss = require('postcss');
postcss([cssnano])
.process(css, { from: undefined })
.then(result => {
console.log(result.css);
});
```
### 使用 CSS 变量
```css
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--spacing: 1rem;
}
.button {
background: var(--primary-color);
padding: var(--spacing);
}
```
## 6. 网络优化
### 使用 HTTP/2
```nginx
# Nginx 配置
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
}
```
### 启用压缩
```javascript
// Express.js 中启用压缩
const compression = require('compression');
const express = require('express');
const app = express();
app.use(compression());
```
### CDN 加速
```html
<!-- 使用 CDN 加载资源 -->
<link rel="stylesheet" href="https://cdn.example.com/styles/main.css">
<script src="https://cdn.example.com/scripts/app.js"></script>
```
## 7. 性能监控
### 使用 Performance API
```javascript
// 页面加载时间
window.addEventListener('load', () => {
const perfData = performance.getEntriesByType('navigation')[0];
console.log('Page load time:', perfData.loadEventEnd - perfData.fetchStart);
console.log('DOM ready time:', perfData.domContentLoadedEventEnd - perfData.fetchStart);
});
// 资源加载时间
const resources = performance.getEntriesByType('resource');
resources.forEach(resource => {
console.log(`${resource.name}: ${resource.duration}ms`);
});
```
### 使用 Web Vitals
```javascript
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
getCLS(console.log);
getFID(console.log);
getFCP(console.log);
getLCP(console.log);
getTTFB(console.log);
```
## 8. Service Worker 优化
### 优化 Service Worker 更新
```javascript
// 定期检查更新
setInterval(() => {
navigator.serviceWorker.getRegistration().then(registration => {
if (registration) {
registration.update();
}
});
}, 60 * 60 * 1000); // 每小时检查一次
```
### 使用 Cache Storage API
```javascript
// 检查缓存大小
async function getCacheSize() {
const cache = await caches.open('my-cache');
const keys = await cache.keys();
let totalSize = 0;
for (const request of keys) {
const response = await cache.match(request);
const blob = await response.blob();
totalSize += blob.size;
}
console.log(`Cache size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
}
```
## 最佳实践总结
1. **预缓存关键资源**:确保首次加载快速
2. **使用合适的缓存策略**:根据资源类型选择策略
3. **优化图片**:使用现代格式和响应式图片
4. **代码分割**:减少初始加载时间
5. **懒加载**:延迟加载非关键资源
6. **压缩资源**:减小文件体积
7. **使用 CDN**:加速资源加载
8. **监控性能**:持续跟踪和优化性能指标
9. **定期更新缓存**:确保内容新鲜度
10. **测试不同网络条件**:确保在各种网络下都有良好体验
服务端 · 2月18日 21:53
PWA 如何实现推送通知功能?需要哪些步骤和组件?PWA 的推送通知功能使用 Web Push API 和 Service Worker 实现,能够在用户未打开应用时发送通知。以下是完整的实现方案:
## 推送通知的核心组件
### 1. Push API
用于接收服务器推送的消息
### 2. Notification API
用于显示通知
### 3. Service Worker
在后台处理推送事件
## 实现推送通知的步骤
### 步骤 1:生成 VAPID 密钥
VAPID(Voluntary Application Server Identification)用于验证推送服务器的身份。
```bash
# 使用 web-push 生成密钥
npm install -g web-push
web-push generate-vapid-keys
```
生成结果:
```
Public Key: <your-public-key>
Private Key: <your-private-key>
```
### 步骤 2:请求推送订阅权限
```javascript
// 在主线程中请求订阅
async function subscribeUser() {
// 检查浏览器支持
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
console.log('Push messaging is not supported');
return;
}
try {
// 获取 Service Worker 注册
const registration = await navigator.serviceWorker.ready;
// 请求订阅
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array('<your-public-key>')
});
console.log('User is subscribed:', subscription);
// 将订阅信息发送到服务器
await saveSubscriptionToServer(subscription);
} catch (error) {
console.log('Failed to subscribe the user:', error);
}
}
// 将 Base64 字符串转换为 Uint8Array
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
```
### 步骤 3:在 Service Worker 中处理推送事件
```javascript
// sw.js
self.addEventListener('push', event => {
console.log('Push event received:', event);
let data = {
title: '新消息',
body: '您有一条新消息',
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png'
};
// 解析推送数据
if (event.data) {
try {
data = { ...data, ...event.data.json() };
} catch (error) {
data.body = event.data.text();
}
}
// 显示通知
const options = {
body: data.body,
icon: data.icon,
badge: data.badge,
vibrate: [200, 100, 200],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'explore',
title: '查看详情',
icon: '/icons/explore.png'
},
{
action: 'close',
title: '关闭',
icon: '/icons/close.png'
}
]
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
```
### 步骤 4:处理通知点击事件
```javascript
self.addEventListener('notificationclick', event => {
console.log('Notification click received:', event);
event.notification.close();
// 处理不同的操作
if (event.action === 'explore') {
// 打开特定页面
event.waitUntil(
clients.openWindow('/details')
);
} else if (event.action === 'close') {
// 关闭通知,不做其他操作
return;
} else {
// 默认操作:打开应用首页
event.waitUntil(
clients.matchAll({ type: 'window' }).then(clientList => {
// 如果已有打开的窗口,聚焦到它
for (const client of clientList) {
if (client.url === '/' && 'focus' in client) {
return client.focus();
}
}
// 否则打开新窗口
if (clients.openWindow) {
return clients.openWindow('/');
}
})
);
}
});
```
### 步骤 5:服务器端发送推送消息
使用 Node.js 和 web-push 库:
```javascript
const webpush = require('web-push');
// 设置 VAPID 密钥
const vapidKeys = {
publicKey: '<your-public-key>',
privateKey: '<your-private-key>'
};
webpush.setVapidDetails(
'mailto:your-email@example.com',
vapidKeys.publicKey,
vapidKeys.privateKey
);
// 发送推送消息
async function sendPushNotification(subscription, data) {
try {
await webpush.sendNotification(subscription, JSON.stringify(data));
console.log('Push notification sent successfully');
} catch (error) {
console.error('Error sending push notification:', error);
// 如果订阅无效,从数据库中删除
if (error.statusCode === 410) {
await removeSubscriptionFromDatabase(subscription);
}
}
}
// 示例:向所有订阅者发送通知
async function sendToAllSubscribers(message) {
const subscriptions = await getAllSubscriptionsFromDatabase();
const promises = subscriptions.map(subscription => {
return sendPushNotification(subscription, {
title: '新通知',
body: message,
icon: '/icons/icon-192x192.png'
});
});
await Promise.allSettled(promises);
}
```
## 推送通知的高级功能
### 1. 静默推送
```javascript
self.addEventListener('push', event => {
if (!event.data) return;
const data = event.data.json();
// 如果是静默推送,不显示通知
if (data.silent) {
event.waitUntil(
// 执行后台任务,如同步数据
syncData()
);
return;
}
// 否则显示通知
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon
})
);
});
```
### 2. 定时推送
```javascript
// 使用 setTimeout 实现延迟推送
self.addEventListener('push', event => {
const data = event.data.json();
if (data.delay) {
setTimeout(() => {
self.registration.showNotification(data.title, {
body: data.body
});
}, data.delay);
} else {
self.registration.showNotification(data.title, {
body: data.body
});
}
});
```
### 3. 富媒体通知
```javascript
self.addEventListener('push', event => {
const data = event.data.json();
const options = {
body: data.body,
icon: data.icon,
image: data.image, // 大图片
badge: data.badge, // 小图标
vibrate: [200, 100, 200],
sound: '/sounds/notification.mp3',
tag: 'unique-tag', // 用于替换相同标签的通知
renotify: true, // 重复通知时提醒用户
requireInteraction: true, // 需要用户交互才能关闭
actions: [
{
action: 'reply',
title: '回复',
icon: '/icons/reply.png',
type: 'text',
placeholder: '输入回复内容'
}
],
data: {
// 自定义数据
url: data.url
}
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
```
## 推送通知的最佳实践
1. **请求权限的时机**:在用户有明确需求时请求,而不是页面加载时
2. **通知内容**:提供有价值的信息,避免垃圾通知
3. **频率控制**:不要过于频繁发送通知
4. **个性化**:根据用户偏好定制通知内容
5. **可操作性**:提供有用的操作按钮
6. **错误处理**:妥善处理订阅失效的情况
7. **测试**:在不同设备和浏览器上测试通知效果
## 浏览器兼容性
- Chrome、Edge、Firefox:完全支持
- Safari:部分支持(iOS 16.4+)
- 需要用户授权
## 调试推送通知
使用 Chrome DevTools:
1. 打开 Application 面板
2. 查看 Service Workers 标签
3. 点击 "Push" 按钮模拟推送
4. 查看通知显示效果
服务端 · 2月18日 21:53
如何实现 PWA 的离线功能?需要哪些关键步骤?PWA 的离线功能是其核心特性之一,主要通过 Service Worker 和缓存机制实现。以下是实现离线功能的完整方案:
## 离线功能的核心组件
### 1. Service Worker
Service Worker 是离线功能的基础,它能够拦截网络请求并从缓存中返回资源。
### 2. Cache API
用于存储和管理缓存资源。
## 实现离线功能的步骤
### 步骤 1:注册 Service Worker
```javascript
// 在主线程中注册
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered:', registration);
})
.catch(error => {
console.log('SW registration failed:', error);
});
});
}
```
### 步骤 2:预缓存关键资源
```javascript
// sw.js
const CACHE_NAME = 'my-pwa-v1';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png',
'/offline.html'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
return cache.addAll(ASSETS_TO_CACHE);
})
.then(() => {
return self.skipWaiting();
})
);
});
```
### 步骤 3:实现缓存策略
```javascript
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 缓存命中,直接返回
if (response) {
return response;
}
// 缓存未命中,请求网络
return fetch(event.request)
.then(response => {
// 检查响应是否有效
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 克隆响应并缓存
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(() => {
// 网络请求失败,返回离线页面
if (event.request.mode === 'navigate') {
return caches.match('/offline.html');
}
});
})
);
});
```
### 步骤 4:创建离线页面
```html
<!-- offline.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>离线</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
font-family: Arial, sans-serif;
background: #f5f5f5;
}
.offline-container {
text-align: center;
padding: 40px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.offline-icon {
font-size: 64px;
margin-bottom: 20px;
}
.offline-title {
font-size: 24px;
margin-bottom: 10px;
color: #333;
}
.offline-message {
color: #666;
margin-bottom: 20px;
}
.retry-button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="offline-container">
<div class="offline-icon">📡</div>
<h1 class="offline-title">您当前处于离线状态</h1>
<p class="offline-message">请检查您的网络连接后重试</p>
<button class="retry-button" onclick="window.location.reload()">重新加载</button>
</div>
<script>
// 监听网络状态变化
window.addEventListener('online', () => {
window.location.reload();
});
</script>
</body>
</html>
```
### 步骤 5:监听网络状态
```javascript
// 在主线程中监听网络状态
window.addEventListener('online', () => {
console.log('网络已连接');
// 可以在这里执行一些操作,比如同步数据
});
window.addEventListener('offline', () => {
console.log('网络已断开');
// 显示离线提示
showOfflineNotification();
});
function showOfflineNotification() {
const notification = document.createElement('div');
notification.textContent = '您当前处于离线状态';
notification.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: #ff9800;
color: white;
padding: 10px 20px;
border-radius: 4px;
z-index: 9999;
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
```
## 高级离线功能
### 1. Background Sync(后台同步)
```javascript
// 注册同步事件
self.addEventListener('sync', event => {
if (event.tag === 'sync-data') {
event.waitUntil(syncData());
}
});
async function syncData() {
// 获取离线时存储的数据
const offlineData = await getOfflineData();
// 同步到服务器
for (const data of offlineData) {
try {
await fetch('/api/sync', {
method: 'POST',
body: JSON.stringify(data)
});
// 同步成功,删除本地数据
await removeOfflineData(data.id);
} catch (error) {
console.error('同步失败:', error);
}
}
}
// 在主线程中请求同步
navigator.serviceWorker.ready.then(registration => {
registration.sync.register('sync-data');
});
```
### 2. IndexedDB 存储离线数据
```javascript
// 打开 IndexedDB
const dbPromise = idb.open('my-pwa-db', 1, upgradeDB => {
upgradeDB.createObjectStore('offline-data', { keyPath: 'id' });
});
// 保存离线数据
async function saveOfflineData(data) {
const db = await dbPromise;
await db.add('offline-data', data);
}
// 获取离线数据
async function getOfflineData() {
const db = await dbPromise;
return await db.getAll('offline-data');
}
// 删除离线数据
async function removeOfflineData(id) {
const db = await dbPromise;
await db.delete('offline-data', id);
}
```
## 离线功能的最佳实践
1. **预缓存关键资源**:确保核心功能离线可用
2. **提供友好的离线页面**:告知用户当前状态并提供解决方案
3. **监听网络状态**:及时响应网络变化
4. **实现数据同步**:离线时存储数据,在线时同步
5. **设置合理的缓存策略**:平衡性能和新鲜度
6. **测试离线场景**:使用 Chrome DevTools 的 Offline 模式测试
7. **提供网络状态指示器**:让用户了解当前网络状态
## 测试离线功能
使用 Chrome DevTools 测试:
1. 打开 DevTools(F12)
2. 切换到 Network 标签
3. 勾选 "Offline" 模式
4. 刷新页面,测试离线功能
5. 取消勾选 "Offline",测试恢复功能
服务端 · 2月18日 21:53
PWA 有哪些常见的缓存策略?它们分别适用于什么场景?PWA 的缓存策略决定了如何处理网络请求和缓存资源,不同的策略适用于不同的场景。
## 常见的缓存策略
### 1. Cache First(缓存优先)
**适用场景**:静态资源(CSS、JS、图片、字体等)
**实现方式**:
```javascript
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
return response || fetch(event.request);
})
);
});
```
**优点**:
- 响应速度快
- 减少网络流量
- 离线可用
**缺点**:
- 可能返回过期内容
- 需要手动更新缓存
### 2. Network First(网络优先)
**适用场景**:动态内容、API 请求
**实现方式**:
```javascript
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request)
.then(response => {
// 缓存响应
const responseClone = response.clone();
caches.open('dynamic-cache').then(cache => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => {
return caches.match(event.request);
})
);
});
```
**优点**:
- 总是返回最新内容
- 网络失败时有降级方案
**缺点**:
- 首次加载较慢
- 网络不稳定时体验差
### 3. Stale While Revalidate(缓存同时更新)
**适用场景**:需要快速响应但也要保持内容新鲜度的资源
**实现方式**:
```javascript
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
const fetchPromise = fetch(event.request).then(networkResponse => {
caches.open('dynamic-cache').then(cache => {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
});
return cachedResponse || fetchPromise;
})
);
});
```
**优点**:
- 快速响应(立即返回缓存)
- 后台更新缓存
- 平衡速度和新鲜度
**缺点**:
- 实现相对复杂
- 可能短暂显示过期内容
### 4. Network Only(仅网络)
**适用场景**:实时数据、支付等敏感操作
**实现方式**:
```javascript
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/realtime')) {
event.respondWith(fetch(event.request));
}
});
```
**优点**:
- 确保数据最新
- 适合实时性要求高的场景
**缺点**:
- 无法离线使用
- 依赖网络稳定性
### 5. Cache Only(仅缓存)
**适用场景**:离线页面、预加载的资源
**实现方式**:
```javascript
self.addEventListener('fetch', event => {
if (event.request.url.includes('/offline')) {
event.respondWith(caches.match(event.request));
}
});
```
**优点**:
- 完全离线可用
- 响应速度最快
**缺点**:
- 需要预先缓存
- 内容可能过期
### 6. Cache First with Network Fallback(缓存优先,网络降级)
**适用场景**:需要快速响应,但在缓存失败时尝试网络
**实现方式**:
```javascript
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
return response;
}
return fetch(event.request).catch(() => {
return caches.match('/offline.html');
});
})
);
});
```
## 缓存策略的选择
根据资源类型选择合适的策略:
| 资源类型 | 推荐策略 | 原因 |
|---------|---------|------|
| 静态资源(CSS、JS、图片) | Cache First | 不常变化,优先速度 |
| API 请求 | Network First | 需要最新数据 |
| HTML 文档 | Network First with Cache Fallback | 需要最新,但可降级 |
| 字体文件 | Cache First | 不常变化,离线可用 |
| 实时数据 | Network Only | 必须最新 |
| 离线页面 | Cache Only | 预缓存,完全离线 |
## 缓存管理
### 缓存版本控制
```javascript
const CACHE_VERSION = 'v1';
const CACHE_NAME = `my-app-${CACHE_VERSION}`;
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll([
'/',
'/styles/main.css',
'/scripts/main.js'
]);
})
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
```
### 缓存清理策略
- 定期清理过期缓存
- 按需清理特定资源
- 限制缓存大小
## 最佳实践
1. **混合使用策略**:根据不同资源类型使用不同策略
2. **设置缓存过期**:为缓存设置合理的过期时间
3. **监控缓存大小**:避免占用过多存储空间
4. **提供离线页面**:在网络不可用时提供友好的提示
5. **测试缓存行为**:在不同网络条件下测试缓存策略
6. **更新机制**:实现自动更新机制确保内容新鲜度
服务端 · 2月18日 21:52
什么是 PWA(Progressive Web App)?它有哪些核心特征和优势?PWA(Progressive Web App)是一种渐进式 Web 应用程序,它结合了 Web 和原生应用的优点。PWA 的核心特征包括:
1. **渐进式增强**:PWA 能够在所有现代浏览器中运行,并在支持的浏览器中提供更好的体验
2. **响应式设计**:适应各种设备和屏幕尺寸
3. **离线可用**:通过 Service Worker 实现离线访问
4. **类原生应用体验**:可以安装到主屏幕,提供全屏体验
5. **可发现性**:可以通过搜索引擎找到
6. **可链接性**:可以通过 URL 分享
7. **安全性**:必须通过 HTTPS 提供
8. **推送通知**:支持 Web Push API
9. **定期更新**:通过 Service Worker 自动更新
PWA 的关键技术包括:
- **Service Worker**:用于缓存资源和实现离线功能
- **Web App Manifest**:定义应用的元数据和安装信息
- **Push API**:实现推送通知
- **Background Sync**:在后台同步数据
PWA 相比传统 Web 应用的优势:
- 更快的加载速度(通过缓存)
- 更好的用户体验(离线可用、推送通知)
- 更高的用户参与度(可安装到主屏幕)
- 更低的开发成本(一套代码多平台运行)
PWA 相比原生应用的优势:
- 无需应用商店审核
- 更容易更新
- 跨平台兼容
- 更小的安装包
- 更好的可发现性
服务端 · 2月18日 21:52
Service Worker 是什么?它的生命周期和主要功能有哪些?Service Worker 是 PWA 的核心技术之一,它是一个运行在浏览器后台的独立线程,主要功能包括:
## Service Worker 的生命周期
1. **注册(Registration)**
```javascript
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => console.log('SW registered'))
.catch(error => console.log('SW registration failed'));
}
```
2. **安装(Installing)**
- 触发 `install` 事件
- 预缓存静态资源
- 调用 `self.skipWaiting()` 可以跳过等待阶段
3. **激活(Activating)**
- 触发 `activate` 事件
- 清理旧缓存
- 调用 `self.clients.claim()` 立即控制所有页面
4. **已激活(Activated)**
- 开始拦截网络请求
- 处理 `fetch` 事件
## Service Worker 的核心功能
### 1. 网络请求拦截
```javascript
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
```
### 2. 资源缓存策略
**Cache First(缓存优先)**
- 优先从缓存读取
- 缓存不存在时再请求网络
- 适用于静态资源
**Network First(网络优先)**
- 优先请求网络
- 网络失败时使用缓存
- 适用于动态内容
**Stale While Revalidate(缓存同时更新)**
- 立即返回缓存
- 同时请求网络更新缓存
- 平衡速度和新鲜度
**Network Only(仅网络)**
- 不使用缓存
- 适用于实时数据
**Cache Only(仅缓存)**
- 仅从缓存读取
- 适用于离线场景
### 3. 离线支持
- 缓存关键资源
- 提供离线页面
- 离线时返回缓存内容
### 4. 推送通知
- 接收服务器推送
- 显示通知
- 处理通知点击
## Service Worker 的限制
1. **HTTPS 要求**:必须在 HTTPS 环境下运行(localhost 除外)
2. **作用域限制**:只能控制注册路径及其子路径
3. **存储限制**:浏览器对缓存大小有限制
4. **生命周期管理**:需要手动更新和清理
5. **调试困难**:运行在独立线程,调试相对复杂
## 最佳实践
1. **版本控制**:为缓存添加版本号,便于更新
2. **缓存清理**:在 `activate` 事件中清理旧缓存
3. **错误处理**:妥善处理网络请求失败
4. **性能优化**:合理设置缓存过期时间
5. **渐进增强**:在不支持 Service Worker 的浏览器中优雅降级
服务端 · 2月18日 21:52