乐闻世界logo
搜索文章和话题

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 的开发需要使用一系列工具和框架来提高开发效率和代码质量。以下是常用的 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
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