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

面试题手册

PWA 开发中有哪些常用的工具和框架?如何使用它们?

PWA 的开发需要使用一系列工具和框架来提高开发效率和代码质量。以下是常用的 PWA 开发工具和框架:核心开发工具1. WorkboxWorkbox 是 Google 提供的 PWA 开发工具集,简化了 Service Worker 的开发。安装npm install workbox-cli --globalnpm install workbox-webpack-plugin --save-dev使用 Workbox CLI# 生成 Service Workerworkbox generateSW workbox-config.js# 预缓存文件workbox wizardWorkbox 配置// workbox-config.jsmodule.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// webpack.config.jsconst { 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 BuilderPWA Builder 是 Microsoft 提供的工具,可以将 PWA 打包为原生应用。# 安装 PWA Builder CLInpm install -g pwabuilder# 打包应用pwabuilder package3. LighthouseLighthouse 是 Google 提供的网站性能和质量审计工具。# 安装 Lighthousenpm install -g lighthouse# 运行审计lighthouse https://your-pwa.com --view使用 Lighthouse CI# 安装 Lighthouse CInpm install -g @lhci/cli# 初始化配置lhci autorun# 运行 CIlhci autorun --collect.url=https://your-pwa.com框架和库1. React PWACreate React App# 创建 PWA 项目npx create-react-app my-pwa --template cra-template-pwa使用 React PWA// src/serviceWorkerRegistration.jsexport 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 PWAVue CLI PWA 插件# 创建 PWA 项目vue create my-pwa# 选择 PWA 插件配置 vue.config.js// vue.config.jsmodule.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# 添加 PWA 支持ng add @angular/pwa配置 ngsw-config.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 DevToolsService Worker 调试// 在 DevTools Console 中// 查看所有 Service Workernavigator.serviceWorker.getRegistrations().then(registrations => { registrations.forEach(registration => { console.log('SW:', registration); });});// 取消注册 Service Workernavigator.serviceWorker.getRegistrations().then(registrations => { registrations.forEach(registration => { registration.unregister(); });});Application 面板Service Workers:查看和管理 Service WorkerCache Storage:查看和管理缓存Manifest:查看和验证 Manifest 文件Background Services:查看后台服务状态2. React DevTools# 安装 React DevToolsnpm install --save-dev react-devtools3. Vue DevTools# 安装 Vue DevToolsnpm install --save-dev @vue/devtools测试工具1. Jest// sw.test.jsdescribe('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// cypress/integration/pwa.spec.jsdescribe('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// test-pwa.jsconst 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// webpack.config.jsconst { 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// vite.config.jsimport { 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# 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 = 2002. Vercel// 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// 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// 在 Service Worker 中跟踪self.addEventListener('fetch', event => { if (navigator.sendBeacon) { navigator.sendBeacon('/analytics', JSON.stringify({ url: event.request.url, timestamp: Date.now() })); }});2. Sentry// 在 Service Worker 中捕获错误self.addEventListener('error', event => { Sentry.captureException(event.error);});self.addEventListener('unhandledrejection', event => { Sentry.captureException(event.reason);});最佳实践使用 Workbox:简化 Service Worker 开发自动化测试:使用 Jest、Cypress 等工具性能监控:使用 Lighthouse 定期审计错误追踪:使用 Sentry 等工具CI/CD 集成:自动化构建和部署代码分割:使用 Webpack、Vite 等工具缓存策略:根据资源类型选择合适的策略渐进增强:确保在不支持 PWA 的浏览器中正常工作
阅读 0·2月18日 22:02

PWA 的安全性如何保障?有哪些关键的安全措施?

PWA 的安全性非常重要,因为 PWA 可以像原生应用一样安装到设备上,并且可以访问更多设备功能。以下是 PWA 安全性的关键方面和最佳实践:1. HTTPS 的必要性为什么 PWA 必须使用 HTTPSService Worker 要求:Service Worker 只能在 HTTPS 环境下运行(localhost 除外)数据安全:保护用户数据在传输过程中的安全信任度:HTTPS 提供身份验证,防止中间人攻击浏览器要求:现代浏览器要求 PWA 必须使用 HTTPS推送通知:Web Push API 要求 HTTPS配置 HTTPS# 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 重定向到 HTTPSserver { listen 80; server_name example.com; return 301 https://$server_name$request_uri;}2. Content Security Policy (CSP)CSP 的作用CSP 可以帮助防止跨站脚本攻击(XSS)、点击劫持等安全威胁。配置 CSP<!-- 通过 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 配置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 作用域// 将 Service Worker 放在根目录,控制整个应用navigator.serviceWorker.register('/sw.js');// 或者放在特定目录,只控制该目录navigator.serviceWorker.register('/app/sw.js', { scope: '/app/'});验证 Service Worker 更新// sw.jsself.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); } }) ); }) );});防止缓存污染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. 数据安全敏感数据加密// 使用 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);}安全存储// 使用 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 密钥管理// 服务器端安全存储 VAPID 密钥const vapidKeys = { publicKey: process.env.VAPID_PUBLIC_KEY, privateKey: process.env.VAPID_PRIVATE_KEY};// 不要在前端暴露私钥// ❌ 错误const privateKey = 'my-private-key'; // 不要这样做// ✅ 正确:私钥只在服务器端使用验证推送订阅// 服务器端验证订阅信息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 配置 CORSlocation /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; }}// 前端请求配置fetch('https://api.example.com/data', { method: 'GET', credentials: 'include', // 包含 cookies headers: { 'Content-Type': 'application/json' }});7. 认证和授权使用 JWT 进行认证// 登录后获取 JWTasync 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;}// 在请求中携带 JWTasync function fetchData() { const token = await getSecureToken(); const response = await fetch('/api/data', { headers: { 'Authorization': `Bearer ${token}` } }); return response.json();}Token 刷新机制// 自动刷新过期 Tokenasync 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. 输入验证// 验证用户输入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// 使用 CSRF Tokenasync 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 更新// 检查 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 进行安全审计# 运行 Lighthouse 安全审计lighthouse https://your-pwa.com --view --only-categories=security监控安全事件// 监控可疑活动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 安全性的关键点:必须使用 HTTPS:Service Worker 和推送通知都要求 HTTPS配置 CSP:防止 XSS 和其他注入攻击限制 Service Worker 作用域:防止缓存污染安全存储敏感数据:使用 IndexedDB 和加密验证推送订阅:防止恶意推送正确配置 CORS:防止跨域攻击使用安全的认证机制:JWT + Token 刷新输入验证和输出编码:防止注入攻击定期安全审计:使用 Lighthouse 等工具监控安全事件:及时发现和响应安全威胁
阅读 0·2月18日 21:58

PWA 如何实现更新机制?如何确保用户使用最新版本?

PWA 的更新机制对于确保用户始终使用最新版本的应用非常重要。以下是 PWA 更新的完整流程和最佳实践:Service Worker 更新流程1. 更新检测浏览器会在以下情况检查 Service Worker 更新:导航到应用页面时Service Worker 事件触发时(如 push、sync 等)每 24 小时自动检查一次2. 更新生命周期// sw.jsconst 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. 更新通知用户// 在主线程中监听更新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(); });}手动触发更新// 手动检查更新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. 版本化缓存// 使用版本号管理缓存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. 智能缓存更新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)); }});预缓存更新// 在安装时预缓存关键资源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' ]); }) );});后台同步更新// 注册后台同步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. 立即更新// 强制立即更新function forceUpdate() { if ('serviceWorker' in navigator) { navigator.serviceWorker.getRegistration().then(registration => { if (registration && registration.waiting) { registration.waiting.postMessage({ action: 'skipWaiting' }); } }); }}2. 延迟更新// 在用户空闲时更新function updateWhenIdle() { if ('serviceWorker' in navigator) { navigator.serviceWorker.getRegistration().then(registration => { if (registration) { registration.update(); } }); }}// 使用 requestIdleCallbackwindow.requestIdleCallback(updateWhenIdle);3. 智能更新// 根据网络条件决定更新策略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. 版本管理// 使用语义化版本号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. 回滚机制// 保留旧版本缓存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. 更新通知// 提供详细的更新信息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. 更新日志// 记录更新事件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. 调试工具// 添加调试信息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 更新的关键点:版本管理:使用版本号管理缓存更新检测:定期检查 Service Worker 更新用户通知:及时通知用户有新版本可用平滑更新:提供良好的更新体验回滚机制:保留旧版本以便回滚智能策略:根据网络条件选择更新策略监控日志:记录更新事件便于调试测试验证:在不同条件下测试更新流程
阅读 0·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 的应用类型内容型应用新闻资讯博客和文章电商网站企业官网工具型应用计算器待办事项笔记应用在线表单轻量级社交应用简单的聊天应用社区论坛评论系统需要快速迭代的应用MVP 产品初创公司产品需要频繁更新的应用预算有限的项目小型企业应用个人项目实验性项目PWA 的优势场景需要跨平台:一套代码多平台运行需要快速上线:开发周期短需要频繁更新:更新无需审核用户流量主要来自搜索:SEO 友好需要离线功能:缓存机制支持离线安装门槛要低:无需应用商店选择原生应用的场景适合原生应用的应用类型性能要求高的应用游戏视频编辑图像处理实时通讯需要深度设备访问的应用相机应用健康监测导航应用IoT 控制复杂交互的应用社交媒体即时通讯生产力工具企业应用需要后台运行的应用音乐播放器位置追踪数据同步消息推送需要系统集成高的应用支付应用银行应用系统工具安全应用原生应用的优势场景性能是关键:需要最佳性能需要完整设备功能:访问所有设备 API复杂交互:需要流畅的原生交互后台运行:需要在后台持续运行品牌要求高:需要完全自定义的 UI用户粘性高:用户会频繁使用混合方案Progressive Enhancement(渐进增强)// 检测 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 作为原生应用的补充PWA 作为试用版:让用户先体验 PWA,再决定是否安装原生应用PWA 作为移动版:原生应用提供完整功能,PWA 提供核心功能PWA 用于营销:通过 PWA 吸引用户,引导安装原生应用使用框架开发跨平台应用React Native:使用 React 开发原生应用Flutter:使用 Dart 开发跨平台应用Ionic:使用 Web 技术开发混合应用Capacitor:将 Web 应用打包为原生应用决策框架评估维度性能要求高性能需求 → 原生应用中等性能需求 → PWA设备功能需求需要完整设备访问 → 原生应用基础功能即可 → PWA开发预算预算充足 → 原生应用预算有限 → PWA时间要求快速上线 → PWA可以等待 → 原生应用更新频率频繁更新 → PWA稳定更新 → 原生应用用户群体技术用户 → PWA普通用户 → 原生应用商业模式应用商店分发 → 原生应用网站引流 → PWA决策流程开始 ↓是否需要高性能? ├─ 是 → 原生应用 └─ 否 ↓ 是否需要完整设备功能? ├─ 是 → 原生应用 └─ 否 ↓ 预算是否充足? ├─ 是 → 考虑原生应用 └─ 否 ↓ 是否需要快速上线? ├─ 是 → PWA └─ 否 ↓ 更新是否频繁? ├─ 是 → PWA └─ 否 → 原生应用实际案例PWA 成功案例Twitter Lite减少了 75% 的数据使用加载时间减少了 30%用户参与度提高了 65%Pinterest PWA核心用户参与度提高了 60%广告收入增加了 44%用户生成广告收入增加了 18%AliExpress PWA新用户转化率提高了 104%每个会话的页面浏览量增加了 74%iOS Safari 上的转化率提高了 82%原生应用成功案例Instagram复杂的图像处理丰富的相机功能流畅的交互体验Uber实时位置追踪后台运行复杂的地图交互总结选择 PWA 如果:预算有限需要快速上线需要频繁更新目标用户主要通过搜索发现应用功能相对简单需要跨平台支持选择原生应用如果:性能是关键因素需要访问完整的设备功能需要复杂的交互体验需要在后台运行有充足的开发预算用户粘性高,会频繁使用考虑混合方案如果:想要降低风险需要逐步迭代目标用户群体多样化需要测试市场反应最终的选择应该基于项目的具体需求、目标用户、预算和时间表进行综合评估。
阅读 0·2月18日 21:55

如何优化 PWA 的性能?有哪些关键的性能优化策略?

PWA 的性能优化对于提供良好的用户体验至关重要。以下是全面的性能优化策略:1. 资源加载优化预缓存关键资源// sw.jsconst 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()) );});懒加载非关键资源// 图片懒加载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'));代码分割// 使用动态 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. 缓存策略优化智能缓存策略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; });}缓存版本管理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. 图片优化使用现代图片格式<picture> <source srcset="image.webp" type="image/webp"> <source srcset="image.jpg" type="image/jpeg"> <img src="image.jpg" alt="Description" loading="lazy"></picture>响应式图片<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">图片压缩和优化// 使用 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 优化减少包体积// 使用 Tree Shaking// 只导入需要的函数import { debounce } from 'lodash-es';// 避免导入整个库// import _ from 'lodash'; // ❌ 避免使用 Web Workers// 主线程const worker = new Worker('worker.js');worker.postMessage({ data: largeDataSet });worker.onmessage = (event) => { console.log('Processed data:', event.data);};// worker.jsself.onmessage = (event) => { const result = processData(event.data.data); self.postMessage(result);};防抖和节流// 防抖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 内联<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 压缩和优化// 使用 cssnano 压缩 CSSconst cssnano = require('cssnano');const postcss = require('postcss');postcss([cssnano]) .process(css, { from: undefined }) .then(result => { console.log(result.css); });使用 CSS 变量:root { --primary-color: #007bff; --secondary-color: #6c757d; --spacing: 1rem;}.button { background: var(--primary-color); padding: var(--spacing);}6. 网络优化使用 HTTP/2# Nginx 配置server { listen 443 ssl http2; server_name example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem;}启用压缩// Express.js 中启用压缩const compression = require('compression');const express = require('express');const app = express();app.use(compression());CDN 加速<!-- 使用 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// 页面加载时间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 Vitalsimport { 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 更新// 定期检查更新setInterval(() => { navigator.serviceWorker.getRegistration().then(registration => { if (registration) { registration.update(); } });}, 60 * 60 * 1000); // 每小时检查一次使用 Cache Storage API// 检查缓存大小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`);}最佳实践总结预缓存关键资源:确保首次加载快速使用合适的缓存策略:根据资源类型选择策略优化图片:使用现代格式和响应式图片代码分割:减少初始加载时间懒加载:延迟加载非关键资源压缩资源:减小文件体积使用 CDN:加速资源加载监控性能:持续跟踪和优化性能指标定期更新缓存:确保内容新鲜度测试不同网络条件:确保在各种网络下都有良好体验
阅读 0·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)用于验证推送服务器的身份。# 使用 web-push 生成密钥npm install -g web-pushweb-push generate-vapid-keys生成结果:Public Key: <your-public-key>Private Key: <your-private-key>步骤 2:请求推送订阅权限// 在主线程中请求订阅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 字符串转换为 Uint8Arrayfunction 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 中处理推送事件// sw.jsself.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:处理通知点击事件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 库: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. 静默推送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. 定时推送// 使用 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. 富媒体通知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) );});推送通知的最佳实践请求权限的时机:在用户有明确需求时请求,而不是页面加载时通知内容:提供有价值的信息,避免垃圾通知频率控制:不要过于频繁发送通知个性化:根据用户偏好定制通知内容可操作性:提供有用的操作按钮错误处理:妥善处理订阅失效的情况测试:在不同设备和浏览器上测试通知效果浏览器兼容性Chrome、Edge、Firefox:完全支持Safari:部分支持(iOS 16.4+)需要用户授权调试推送通知使用 Chrome DevTools:打开 Application 面板查看 Service Workers 标签点击 "Push" 按钮模拟推送查看通知显示效果
阅读 0·2月18日 21:53

如何实现 PWA 的离线功能?需要哪些关键步骤?

PWA 的离线功能是其核心特性之一,主要通过 Service Worker 和缓存机制实现。以下是实现离线功能的完整方案:离线功能的核心组件1. Service WorkerService Worker 是离线功能的基础,它能够拦截网络请求并从缓存中返回资源。2. Cache API用于存储和管理缓存资源。实现离线功能的步骤步骤 1:注册 Service Worker// 在主线程中注册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:预缓存关键资源// sw.jsconst 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:实现缓存策略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:创建离线页面<!-- 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:监听网络状态// 在主线程中监听网络状态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(后台同步)// 注册同步事件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 存储离线数据// 打开 IndexedDBconst 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);}离线功能的最佳实践预缓存关键资源:确保核心功能离线可用提供友好的离线页面:告知用户当前状态并提供解决方案监听网络状态:及时响应网络变化实现数据同步:离线时存储数据,在线时同步设置合理的缓存策略:平衡性能和新鲜度测试离线场景:使用 Chrome DevTools 的 Offline 模式测试提供网络状态指示器:让用户了解当前网络状态测试离线功能使用 Chrome DevTools 测试:打开 DevTools(F12)切换到 Network 标签勾选 "Offline" 模式刷新页面,测试离线功能取消勾选 "Offline",测试恢复功能
阅读 0·2月18日 21:53

PWA 有哪些常见的缓存策略?它们分别适用于什么场景?

PWA 的缓存策略决定了如何处理网络请求和缓存资源,不同的策略适用于不同的场景。常见的缓存策略1. Cache First(缓存优先)适用场景:静态资源(CSS、JS、图片、字体等)实现方式:self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(response => { return response || fetch(event.request); }) );});优点:响应速度快减少网络流量离线可用缺点:可能返回过期内容需要手动更新缓存2. Network First(网络优先)适用场景:动态内容、API 请求实现方式: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(缓存同时更新)适用场景:需要快速响应但也要保持内容新鲜度的资源实现方式: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(仅网络)适用场景:实时数据、支付等敏感操作实现方式:self.addEventListener('fetch', event => { if (event.request.url.includes('/api/realtime')) { event.respondWith(fetch(event.request)); }});优点:确保数据最新适合实时性要求高的场景缺点:无法离线使用依赖网络稳定性5. Cache Only(仅缓存)适用场景:离线页面、预加载的资源实现方式:self.addEventListener('fetch', event => { if (event.request.url.includes('/offline')) { event.respondWith(caches.match(event.request)); }});优点:完全离线可用响应速度最快缺点:需要预先缓存内容可能过期6. Cache First with Network Fallback(缓存优先,网络降级)适用场景:需要快速响应,但在缓存失败时尝试网络实现方式: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 | 预缓存,完全离线 |缓存管理缓存版本控制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); } }) ); }) );});缓存清理策略定期清理过期缓存按需清理特定资源限制缓存大小最佳实践混合使用策略:根据不同资源类型使用不同策略设置缓存过期:为缓存设置合理的过期时间监控缓存大小:避免占用过多存储空间提供离线页面:在网络不可用时提供友好的提示测试缓存行为:在不同网络条件下测试缓存策略更新机制:实现自动更新机制确保内容新鲜度
阅读 0·2月18日 21:52

Service Worker 是什么?它的生命周期和主要功能有哪些?

Service Worker 是 PWA 的核心技术之一,它是一个运行在浏览器后台的独立线程,主要功能包括:Service Worker 的生命周期注册(Registration)if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') .then(registration => console.log('SW registered')) .catch(error => console.log('SW registration failed'));}安装(Installing)触发 install 事件预缓存静态资源调用 self.skipWaiting() 可以跳过等待阶段激活(Activating)触发 activate 事件清理旧缓存调用 self.clients.claim() 立即控制所有页面已激活(Activated)开始拦截网络请求处理 fetch 事件Service Worker 的核心功能1. 网络请求拦截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 的限制HTTPS 要求:必须在 HTTPS 环境下运行(localhost 除外)作用域限制:只能控制注册路径及其子路径存储限制:浏览器对缓存大小有限制生命周期管理:需要手动更新和清理调试困难:运行在独立线程,调试相对复杂最佳实践版本控制:为缓存添加版本号,便于更新缓存清理:在 activate 事件中清理旧缓存错误处理:妥善处理网络请求失败性能优化:合理设置缓存过期时间渐进增强:在不支持 Service Worker 的浏览器中优雅降级
阅读 0·2月18日 21:52