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

面试题手册

iframe 有哪些替代方案?在不同场景下应该如何选择合适的嵌入方式?

iframe 替代方案概述虽然 iframe 是嵌入外部内容的常用方法,但在某些场景下,使用替代方案可能更合适。选择合适的嵌入方式需要考虑性能、安全性、SEO 和可维护性等因素。iframe 的主要替代方案1. AJAX 动态加载内容使用 JavaScript 动态加载和插入内容。// 使用 fetch API 加载内容fetch('https://api.example.com/content') .then(response => response.text()) .then(html => { document.getElementById('content-container').innerHTML = html; }) .catch(error => { console.error('加载内容失败:', error); document.getElementById('content-container').innerHTML = '<p>加载失败,请稍后重试。</p>'; });// 使用 XMLHttpRequest(传统方式)const xhr = new XMLHttpRequest();xhr.open('GET', 'https://api.example.com/content', true);xhr.onload = function() { if (xhr.status === 200) { document.getElementById('content-container').innerHTML = xhr.responseText; }};xhr.send();优点:更好的 SEO:内容直接嵌入主页面更好的性能:减少额外的文档加载更好的控制:可以完全控制内容样式和行为更好的可访问性:屏幕阅读器更容易访问缺点:需要服务器支持 CORS跨域加载可能受到限制需要更多的 JavaScript 代码2. Server-Side Includes (SSI)在服务器端直接包含其他文件的内容。<!-- Apache SSI --><!--#include virtual="/includes/header.html" --><!--#include file="footer.html" --><!-- Nginx SSI --><!--# include virtual="/includes/header.html" -->优点:简单易用无需 JavaScript对 SEO 友好服务器端处理,性能好缺点:需要服务器配置支持只能包含同源内容不适合动态内容3. 组件化开发(React、Vue 等)使用现代前端框架的组件系统。// React 组件function ProductCard({ product }) { return ( <div className="product-card"> <img src={product.image} alt={product.name} /> <h3>{product.name}</h3> <p>{product.description}</p> <button onClick={() => addToCart(product.id)}> 添加到购物车 </button> </div> );}// 使用组件function ProductList() { const [products, setProducts] = useState([]); useEffect(() => { fetch('https://api.example.com/products') .then(response => response.json()) .then(data => setProducts(data)); }, []); return ( <div className="product-list"> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </div> );}优点:组件复用性强状态管理方便生态系统完善开发效率高缺点:学习曲线较陡构建配置复杂初始加载时间较长4. Web Components使用浏览器原生的组件化技术。// 定义自定义元素class ProductCard extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); } connectedCallback() { const product = JSON.parse(this.getAttribute('product')); this.render(product); } render(product) { this.shadowRoot.innerHTML = ` <style> .product-card { border: 1px solid #ddd; padding: 16px; border-radius: 8px; } .product-card img { max-width: 100%; } </style> <div class="product-card"> <img src="${product.image}" alt="${product.name}"> <h3>${product.name}</h3> <p>${product.description}</p> <button>添加到购物车</button> </div> `; }}customElements.define('product-card', ProductCard);// 使用自定义元素<product-card product='{"id":1,"name":"产品1","description":"描述","image":"image.jpg"}'></product-card>优点:浏览器原生支持跨框架兼容样式隔离可复用性强缺点:浏览器兼容性要求较高开发复杂度较高生态系统不如框架完善5. Object 和 Embed 标签使用 HTML5 的 object 和 embed 标签嵌入内容。<!-- 使用 object 标签 --><object data="https://example.com/content.pdf" type="application/pdf" width="100%" height="500"> <p>您的浏览器不支持 PDF,请 <a href="https://example.com/content.pdf">下载</a> 查看。</p></object><!-- 使用 embed 标签 --><embed src="https://example.com/content.pdf" type="application/pdf" width="100%" height="500"><!-- 嵌入 Flash(已过时) --><object data="content.swf" type="application/x-shockwave-flash"> <param name="movie" value="content.swf"></object>优点:适合嵌入特定类型的内容(PDF、Flash 等)提供更好的 fallback 机制浏览器支持良好缺点:主要用于特定类型的内容不适合嵌入完整的 HTML 页面Flash 已被废弃6. Shadow DOM使用 Shadow DOM 实现样式隔离。// 创建 Shadow DOMconst host = document.createElement('div');const shadow = host.attachShadow({ mode: 'open' });// 添加内容shadow.innerHTML = ` <style> p { color: red; font-size: 18px; } </style> <p>这是 Shadow DOM 中的内容</p>`;// 添加到页面document.body.appendChild(host);优点:样式隔离封装性好避免样式冲突缺点:浏览器兼容性要求较高开发复杂度较高不适合跨域内容7. Portals API使用 Portals API 将内容渲染到页面外的元素中。// 创建 Portalimport { createPortal } from 'react-dom';function Modal({ children, onClose }) { return createPortal( <div className="modal-overlay" onClick={onClose}> <div className="modal-content" onClick={e => e.stopPropagation()}> {children} <button onClick={onClose}>关闭</button> </div> </div>, document.body );}// 使用 Portalfunction App() { const [showModal, setShowModal] = useState(false); return ( <div> <button onClick={() => setShowModal(true)}>打开模态框</button> {showModal && ( <Modal onClose={() => setShowModal(false)}> <h2>模态框内容</h2> <p>这是通过 Portal 渲染的内容</p> </Modal> )} </div> );}优点:可以渲染到 DOM 树的任何位置避免样式冲突适合模态框、下拉菜单等场景缺点:需要框架支持浏览器兼容性要求较高不适合跨域内容选择替代方案的考虑因素1. 性能考虑// 性能对比// iframe: 额外的文档加载、独立的 JS 执行环境// AJAX: 单个文档、共享 JS 环境// SSI: 服务器端处理、无额外请求// 组件化: 构建时优化、运行时高效2. SEO 考虑<!-- SEO 友好的方案 --><!-- 直接嵌入内容 --><div id="content"> <!-- 内容直接嵌入,搜索引擎可以索引 --></div><!-- 不利于 SEO 的方案 --><iframe src="https://example.com/content"></iframe>3. 安全性考虑// 安全性对比// iframe: 需要使用 sandbox、CSP 等安全措施// AJAX: 需要验证 CORS、CSRF Token// SSI: 服务器端处理,相对安全// 组件化: 需要防范 XSS、CSRF 等攻击4. 可维护性考虑// 可维护性对比// iframe: 独立维护,但难以控制样式// AJAX: 集中管理,但需要处理跨域// SSI: 简单直接,但功能有限// 组件化: 结构清晰,但学习成本高替代方案使用场景1. 适合使用 iframe 的场景<!-- 嵌入第三方视频 --><iframe src="https://www.youtube.com/embed/VIDEO_ID" allowfullscreen></iframe><!-- 嵌入地图 --><iframe src="https://www.google.com/maps/embed?pb=..."></iframe><!-- 嵌入社交媒体内容 --><iframe src="https://www.facebook.com/plugins/post.php?href=..."></iframe>2. 适合使用 AJAX 的场景// 加载产品列表fetch('https://api.example.com/products') .then(response => response.json()) .then(products => { renderProducts(products); });// 加载用户信息fetch('https://api.example.com/user/profile') .then(response => response.json()) .then(user => { updateUserProfile(user); });3. 适合使用组件化的场景// 复杂的 UI 组件function Dashboard() { return ( <div> <Header /> <Sidebar /> <MainContent /> <Footer /> </div> );}// 可复用的业务组件function ProductCard({ product }) { return ( <div className="product-card"> <ProductImage product={product} /> <ProductInfo product={product} /> <AddToCartButton productId={product.id} /> </div> );}总结选择 iframe 替代方案的关键要点:性能优先: AJAX 和组件化通常比 iframe 性能更好SEO 友好: 直接嵌入内容比 iframe 更有利于 SEO安全考虑: 根据内容来源选择合适的方案可维护性: 选择团队熟悉且易于维护的方案场景匹配: 根据具体使用场景选择最合适的方案浏览器兼容: 考虑目标用户的浏览器环境开发效率: 平衡开发效率和长期维护成本
阅读 0·3月7日 12:06

Service Worker 与 Web Worker 有什么区别?

Service Worker vs Web Worker 对比Service Worker 和 Web Worker 都是运行在浏览器后台的 JavaScript 线程,但它们的设计目标和应用场景完全不同。核心区别对比表| 特性 | Service Worker | Web Worker ||------|----------------|------------|| 主要用途 | 网络代理、离线缓存、推送通知 | 执行复杂计算、避免阻塞主线程 || 生命周期 | 独立于页面,可长期运行 | 与页面绑定,页面关闭即终止 || DOM 访问 | ❌ 无法访问 | ❌ 无法访问 || 网络拦截 | ✅ 可以拦截所有网络请求 | ❌ 无法拦截 || 事件驱动 | ✅ 基于事件(fetch, push, sync) | ✅ 基于消息传递 || 安装方式 | 需要注册,有独立生命周期 | 直接实例化 Worker 对象 || 持久化 | ✅ 浏览器可自动重启 | ❌ 页面关闭即销毁 || 通信方式 | postMessage + clients API | postMessage |Service Worker 特点1. 网络代理能力// Service Worker 可以拦截所有网络请求self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) );});2. 独立于页面的生命周期安装后持续运行,即使所有页面关闭浏览器可自动重启处理事件适合后台任务(推送通知、后台同步)3. 渐进式 Web 应用核心// 推送通知self.addEventListener('push', event => { event.waitUntil( self.registration.showNotification('新消息', { body: event.data.text() }) );});// 后台同步self.addEventListener('sync', event => { if (event.tag === 'sync-data') { event.waitUntil(syncData()); }});Web Worker 特点1. 计算密集型任务// main.jsconst worker = new Worker('worker.js');worker.postMessage({ numbers: [1, 2, 3, 4, 5] });worker.onmessage = event => { console.log('Result:', event.data);};// worker.jsself.onmessage = event => { const { numbers } = event.data; const result = numbers.reduce((a, b) => a + b, 0); self.postMessage(result);};2. 页面级生命周期创建它的页面关闭时自动终止适合处理一次性计算任务无法执行后台持续任务3. 多种类型// 专用 Worker(Dedicated Worker)const worker = new Worker('worker.js');// 共享 Worker(Shared Worker)- 多页面共享const sharedWorker = new SharedWorker('shared-worker.js');// Service Worker(特殊类型)navigator.serviceWorker.register('sw.js');使用场景对比Service Worker 适用场景离线应用:缓存资源,提供离线访问网络优化:智能缓存策略,减少网络请求推送通知:接收服务器推送的消息后台同步:网络恢复后自动同步数据PWA 功能:添加到主屏幕、应用壳架构Web Worker 适用场景大数据处理:图片处理、视频编解码复杂计算:数学运算、数据分析实时数据处理:WebSocket 数据解析文件处理:大文件读取、压缩避免 UI 阻塞:耗时操作不卡顿界面代码示例对比Service Worker 示例// 注册navigator.serviceWorker.register('/sw.js');// sw.js - 拦截请求self.addEventListener('fetch', event => { event.respondWith(caches.match(event.request));});// 与主线程通信(通过 clients)self.clients.matchAll().then(clients => { clients.forEach(client => client.postMessage('update'));});Web Worker 示例// 创建 Workerconst worker = new Worker('/worker.js');// 发送消息worker.postMessage({ action: 'calculate', data: [1, 2, 3] });// 接收消息worker.onmessage = event => { console.log('Result:', event.data);};// worker.jsself.onmessage = event => { const result = performCalculation(event.data); self.postMessage(result);};总结Service Worker:网络代理专家,负责离线体验、后台任务Web Worker:计算性能专家,负责耗时运算、避免阻塞两者关系:互补而非替代,可在 PWA 中同时使用
阅读 0·3月7日 12:06

Service Worker 的安全注意事项有哪些?

Service Worker 安全注意事项详解Service Worker 作为浏览器后台运行的代理服务器,具有强大的能力,同时也带来了一些安全风险。了解这些安全问题对于开发安全的 Web 应用至关重要。1. HTTPS 要求为什么必须使用 HTTPS// Service Worker 只能在 HTTPS 环境下注册// 例外:localhost 允许 HTTPif ('serviceWorker' in navigator) { // 检查是否是安全上下文 if (window.isSecureContext) { navigator.serviceWorker.register('/sw.js'); } else { console.error('Service Worker 需要 HTTPS 环境'); }}安全风险:HTTP 环境下 Service Worker 可能被中间人攻击篡改攻击者可注入恶意 Service Worker 拦截所有网络请求用户的敏感数据可能被窃取检测安全上下文// 检查当前环境是否安全function checkSecureContext() { if (!window.isSecureContext) { console.warn('当前不是安全上下文,Service Worker 功能受限'); return false; } return true;}// 或者检查协议function isSecureProtocol() { return location.protocol === 'https:' || location.hostname === 'localhost';}2. 作用域限制作用域安全// Service Worker 只能控制其作用域内的页面// 注册时指定作用域navigator.serviceWorker.register('/sw.js', { scope: '/app/' // 只能控制 /app/ 下的页面});// 尝试访问作用域外的资源会失败// /app/page.html ✅ 可以控制// /other/page.html ❌ 无法控制路径遍历防护// ❌ 危险:不验证路径可能导致安全问题self.addEventListener('fetch', event => { const url = new URL(event.request.url); // 恶意请求可能包含 ../ 等路径遍历字符 caches.match(url.pathname); // 危险!});// ✅ 安全:验证和清理路径self.addEventListener('fetch', event => { const url = new URL(event.request.url); const pathname = url.pathname; // 验证路径不包含遍历字符 if (pathname.includes('..') || pathname.includes('//')) { event.respondWith(new Response('Invalid path', { status: 400 })); return; } // 只允许访问白名单路径 const allowedPaths = ['/api/', '/assets/', '/static/']; const isAllowed = allowedPaths.some(path => pathname.startsWith(path)); if (!isAllowed) { event.respondWith(new Response('Forbidden', { status: 403 })); return; } event.respondWith(caches.match(event.request));});3. 内容安全策略(CSP)CSP 对 Service Worker 的影响// 设置 CSP 防止 XSS 攻击// 在 HTTP 响应头中设置:// Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'// Service Worker 脚本本身需要符合 CSP// 内联脚本可能被阻止安全的 Service Worker 脚本加载// ✅ 推荐:加载外部脚本navigator.serviceWorker.register('/sw.js');// ❌ 避免:使用内联 Service Workerconst swCode = ` self.addEventListener('fetch', ...);`;const blob = new Blob([swCode], { type: 'application/javascript' });const url = URL.createObjectURL(blob);navigator.serviceWorker.register(url); // 可能违反 CSP4. 缓存安全敏感数据缓存// ❌ 危险:缓存敏感信息self.addEventListener('fetch', event => { if (event.request.url.includes('/api/user/profile')) { event.respondWith( fetch(event.request).then(response => { // 缓存包含个人信息的响应 caches.open('api-cache').then(cache => { cache.put(event.request, response.clone()); // 危险! }); return response; }) ); }});// ✅ 安全:不缓存敏感数据const SENSITIVE_PATHS = [ '/api/auth/', '/api/user/profile', '/api/payment/'];self.addEventListener('fetch', event => { const url = new URL(event.request.url); // 检查是否是敏感路径 const isSensitive = SENSITIVE_PATHS.some(path => url.pathname.includes(path) ); if (isSensitive) { // 敏感请求直接走网络,不缓存 event.respondWith(fetch(event.request)); return; } // 非敏感请求可以使用缓存 event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) );});缓存清理安全// ✅ 安全的缓存清理self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { // 只删除当前 Service Worker 创建的缓存 // 避免删除其他应用的缓存 if (cacheName.startsWith('my-app-')) { return caches.delete(cacheName); } }) ); }) );});5. 跨站脚本攻击(XSS)防护防止恶意脚本注入// ❌ 危险:直接使用用户输入self.addEventListener('message', event => { const userInput = event.data.message; // 直接执行用户输入可能导致 XSS eval(userInput); // 极度危险!});// ✅ 安全:验证和清理输入self.addEventListener('message', event => { const data = event.data; // 验证消息来源 if (event.origin !== 'https://trusted-domain.com') { console.error('Untrusted origin:', event.origin); return; } // 验证数据类型 if (typeof data.action !== 'string') { return; } // 使用白名单验证操作 const allowedActions = ['skipWaiting', 'claimClients']; if (!allowedActions.includes(data.action)) { console.error('Invalid action:', data.action); return; } // 安全执行 switch (data.action) { case 'skipWaiting': self.skipWaiting(); break; case 'claimClients': self.clients.claim(); break; }});6. 中间人攻击防护证书固定(Certificate Pinning)// 虽然 Service Worker 无法直接实现证书固定// 但可以通过检查响应头来增强安全性self.addEventListener('fetch', event => { event.respondWith( fetch(event.request).then(response => { // 检查安全响应头 const headers = response.headers; // 检查 HSTS 头 if (!headers.get('Strict-Transport-Security')) { console.warn('Missing HSTS header'); } // 检查 CSP 头 if (!headers.get('Content-Security-Policy')) { console.warn('Missing CSP header'); } return response; }) );});7. 权限控制最小权限原则// ✅ 只请求必要的权限// 推送通知权限async function requestPushPermission() { const permission = await Notification.requestPermission(); return permission === 'granted';}// 后台同步权限async function requestSyncPermission() { const registration = await navigator.serviceWorker.ready; if ('sync' in registration) { // 检查权限状态 const status = await navigator.permissions.query({ name: 'periodic-background-sync' }); if (status.state === 'granted') { return true; } } return false;}8. 更新安全安全的更新机制// ✅ 验证 Service Worker 更新navigator.serviceWorker.register('/sw.js').then(registration => { registration.addEventListener('updatefound', () => { const newWorker = registration.installing; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed') { // 验证新版本的完整性 // 可以通过计算哈希值来验证 verifyServiceWorkerIntegrity(newWorker).then(isValid => { if (isValid) { // 提示用户更新 showUpdateNotification(newWorker); } else { console.error('Service Worker 完整性验证失败'); } }); } }); });});// 验证 Service Worker 完整性(示例)async function verifyServiceWorkerIntegrity(worker) { try { const response = await fetch('/sw.js'); const scriptContent = await response.text(); // 这里可以添加哈希验证逻辑 // 比如与服务器返回的哈希值对比 return true; } catch (error) { console.error('验证失败:', error); return false; }}9. 数据泄露防护防止缓存泄露// ✅ 安全的缓存策略const PUBLIC_RESOURCES = [ '/', '/index.html', '/styles.css', '/app.js', '/images/'];const PRIVATE_RESOURCES = [ '/api/user/', '/api/orders/', '/dashboard/'];self.addEventListener('fetch', event => { const url = new URL(event.request.url); // 检查是否是公共资源 const isPublic = PUBLIC_RESOURCES.some(path => url.pathname.startsWith(path) ); // 检查是否是私有资源 const isPrivate = PRIVATE_RESOURCES.some(path => url.pathname.startsWith(path) ); if (isPrivate) { // 私有资源:网络优先,不缓存 event.respondWith( fetch(event.request).catch(() => { return new Response('Network error', { status: 503 }); }) ); } else if (isPublic) { // 公共资源:可以使用缓存 event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) ); } else { // 未知资源:默认网络请求 event.respondWith(fetch(event.request)); }});10. 安全最佳实践清单开发阶段[ ] 确保所有环境使用 HTTPS(除 localhost)[ ] 合理设置 Service Worker 作用域[ ] 不缓存敏感数据(用户信息、支付数据等)[ ] 验证所有用户输入[ ] 实施 CSP 策略[ ] 定期更新 Service Worker生产环境[ ] 监控 Service Worker 异常行为[ ] 实施 Subresource Integrity (SRI)[ ] 配置 HSTS 头[ ] 定期审计缓存内容[ ] 实施访问控制[ ] 记录安全日志安全测试建议// 安全测试清单const securityTests = { // 1. 检查 HTTPS checkHTTPS: () => location.protocol === 'https:', // 2. 检查作用域 checkScope: async () => { const registration = await navigator.serviceWorker.ready; console.log('Service Worker scope:', registration.scope); return registration.scope; }, // 3. 检查缓存内容 checkCacheContents: async () => { const cacheNames = await caches.keys(); for (const name of cacheNames) { const cache = await caches.open(name); const requests = await cache.keys(); console.log(`Cache "${name}" contains ${requests.length} items`); } }, // 4. 检查权限 checkPermissions: async () => { const permissions = await navigator.permissions.query({ name: 'notifications' }); console.log('Notification permission:', permissions.state); }};总结Service Worker 安全要点:必须使用 HTTPS:防止中间人攻击合理设置作用域:限制控制能力不缓存敏感数据:防止数据泄露验证用户输入:防止 XSS 攻击实施 CSP:增强内容安全定期更新:修复安全漏洞最小权限:只请求必要权限
阅读 0·3月7日 12:06

如何在 Service Worker 中实现推送通知功能?

Service Worker 推送通知实现详解推送通知是 Service Worker 的重要功能之一,允许服务器向用户发送消息,即使用户没有打开网站。推送通知架构┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ 服务器 │────▶│ 推送服务 │────▶│ 浏览器 ││ (Web App) │ │(FCM/APNs等) │ │(Service Worker)└─────────────┘ └─────────────┘ └─────────────┘实现步骤1. 请求通知权限// main.jsasync function requestNotificationPermission() { const permission = await Notification.requestPermission(); if (permission === 'granted') { console.log('通知权限已授予'); await subscribeUserToPush(); } else if (permission === 'denied') { console.log('通知权限被拒绝'); } else { console.log('通知权限待处理'); }}// 检查当前权限状态console.log('当前权限:', Notification.permission);// 'default' | 'granted' | 'denied'2. 订阅推送服务// main.jsasync function subscribeUserToPush() { const registration = await navigator.serviceWorker.ready; // 获取或创建订阅 let subscription = await registration.pushManager.getSubscription(); if (!subscription) { // 从服务器获取 VAPID 公钥 const vapidPublicKey = await fetch('/api/vapid-public-key').then(r => r.text()); // 转换为 Uint8Array const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey); // 创建订阅 subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, // 必须设置为 true applicationServerKey: convertedVapidKey }); console.log('推送订阅成功:', subscription); } // 将订阅信息发送到服务器 await fetch('/api/save-subscription', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(subscription) }); return subscription;}// VAPID 公钥转换函数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 接收推送// sw.js// 监听推送事件self.addEventListener('push', event => { console.log('收到推送消息:', event); let data = {}; if (event.data) { data = event.data.json(); } const options = { body: data.body || '您有一条新消息', icon: '/icons/icon-192x192.png', badge: '/icons/badge-72x72.png', image: data.image || '/images/notification-banner.jpg', tag: data.tag || 'default', requireInteraction: false, // 是否保持通知直到用户交互 actions: [ { action: 'open', title: '打开', icon: '/icons/open.png' }, { action: 'dismiss', title: '忽略', icon: '/icons/close.png' } ], data: { url: data.url || '/', timestamp: Date.now() } }; event.waitUntil( self.registration.showNotification(data.title || '新通知', options) );});4. 处理通知点击// sw.js// 监听通知点击事件self.addEventListener('notificationclick', event => { console.log('通知被点击:', event); event.notification.close(); const { action, notification } = event; const data = notification.data || {}; if (action === 'dismiss') { // 用户点击忽略按钮 return; } // 默认行为或点击打开按钮 event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }) .then(clientList => { const urlToOpen = data.url || '/'; // 检查是否已有窗口打开 for (const client of clientList) { if (client.url === urlToOpen && 'focus' in client) { return client.focus(); } } // 没有则打开新窗口 if (clients.openWindow) { return clients.openWindow(urlToOpen); } }) );});// 监听通知关闭事件self.addEventListener('notificationclose', event => { console.log('通知被关闭:', event); // 可以在这里记录统计数据});服务器端发送推送Node.js 示例(使用 web-push 库)// server.jsconst 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);// 存储订阅信息(生产环境应使用数据库)let subscriptions = [];// 保存订阅app.post('/api/save-subscription', (req, res) => { const subscription = req.body; subscriptions.push(subscription); res.json({ success: true });});// 发送推送app.post('/api/send-push', async (req, res) => { const { title, body, url } = req.body; const payload = JSON.stringify({ title, body, url, icon: '/icons/icon-192x192.png' }); const results = await Promise.allSettled( subscriptions.map(sub => webpush.sendNotification(sub, payload)) ); // 清理无效订阅 results.forEach((result, index) => { if (result.status === 'rejected' && result.reason.statusCode === 410) { subscriptions.splice(index, 1); } }); res.json({ success: true, sent: results.length });});高级功能1. 定时推送(定期后台同步)// 主线程请求定期同步navigator.serviceWorker.ready.then(registration => { registration.periodicSync.register('daily-news', { minInterval: 24 * 60 * 60 * 1000 // 24小时 });});// sw.js 监听self.addEventListener('periodicsync', event => { if (event.tag === 'daily-news') { event.waitUntil(showDailyNewsNotification()); }});2. 富媒体通知const options = { body: '点击查看详情', icon: '/icon.png', image: '/banner.jpg', // 大图 badge: '/badge.png', // 小图标 vibrate: [200, 100, 200], // 振动模式 sound: '/notification.mp3', // 声音(部分浏览器支持) dir: 'ltr', // 文字方向 lang: 'zh-CN', timestamp: Date.now(), requireInteraction: true, actions: [ { action: 'reply', title: '回复', icon: '/reply.png' }, { action: 'archive', title: '归档', icon: '/archive.png' } ]};3. 取消订阅// main.jsasync function unsubscribeFromPush() { const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.getSubscription(); if (subscription) { await subscription.unsubscribe(); // 通知服务器删除订阅 await fetch('/api/remove-subscription', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ endpoint: subscription.endpoint }) }); console.log('已取消推送订阅'); }}浏览器兼容性| 功能 | Chrome | Firefox | Safari | Edge ||------|--------|---------|--------|------|| Push API | ✅ | ✅ | ✅ (16.4+) | ✅ || Notification | ✅ | ✅ | ✅ | ✅ || Actions | ✅ | ✅ | ❌ | ✅ || Badge | ✅ | ❌ | ❌ | ✅ |最佳实践尊重用户:不要频繁发送通知,提供取消订阅选项及时更新:定期清理无效的订阅个性化内容:根据用户偏好发送相关通知优雅降级:不支持推送的浏览器提供替代方案HTTPS 必需:推送功能必须在 HTTPS 环境下运行
阅读 0·3月7日 12:06

Service Worker 的更新机制是怎样的?

Service Worker 更新机制详解Service Worker 的更新机制是其重要特性之一,理解它对于维护应用的稳定性和用户体验至关重要。更新触发条件当浏览器检测到 Service Worker 文件发生变化时(字节差异),会触发更新流程。// 浏览器会自动检查更新// 以下情况会触发检查:// 1. 用户访问页面时// 2. 调用 registration.update()// 3. 页面刷新(在特定条件下)更新生命周期1. 检测更新// 手动触发更新检查navigator.serviceWorker.register('/sw.js').then(registration => { // 检查更新 registration.update(); // 监听更新发现 registration.addEventListener('updatefound', () => { const newWorker = registration.installing; console.log('发现新版本 Service Worker'); });});2. 新版本安装// sw.jsconst CACHE_NAME = 'app-v2'; // 更新缓存版本self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME).then(cache => { return cache.addAll([ '/', '/index.html', '/app.js', '/styles.css' ]); }) ); // 立即激活,跳过等待阶段 self.skipWaiting();});3. 等待阶段(Waiting)默认情况下,新版本的 Service Worker 会进入等待状态,直到旧版本控制的所有页面关闭。// 主线程监听状态变化navigator.serviceWorker.register('/sw.js').then(registration => { registration.addEventListener('updatefound', () => { const newWorker = registration.installing; newWorker.addEventListener('statechange', () => { switch (newWorker.state) { case 'installed': if (navigator.serviceWorker.controller) { // 有新版本等待激活 console.log('新版本已安装,等待激活'); showUpdateNotification(newWorker); } break; case 'activated': console.log('新版本已激活'); break; } }); });});4. 激活阶段// sw.jsself.addEventListener('activate', event => { event.waitUntil( // 清理旧缓存 caches.keys().then(cacheNames => { return Promise.all( cacheNames .filter(name => name !== CACHE_NAME) .map(name => { console.log('删除旧缓存:', name); return caches.delete(name); }) ); }) ); // 立即接管所有页面 self.clients.claim();});用户提示更新方案一:立即更新(推荐用于开发环境)// 主线程let newWorker = null;navigator.serviceWorker.register('/sw.js').then(registration => { registration.addEventListener('updatefound', () => { newWorker = registration.installing; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { // 显示更新提示 showUpdateBar(); } }); });});// 用户点击更新按钮function applyUpdate() { if (newWorker) { newWorker.postMessage({ action: 'skipWaiting' }); }}// sw.js 中监听self.addEventListener('message', event => { if (event.data.action === 'skipWaiting') { self.skipWaiting(); }});方案二:优雅更新(推荐用于生产环境)// 监听 controller 变化navigator.serviceWorker.addEventListener('controllerchange', () => { // Service Worker 已切换,建议用户刷新页面 window.location.reload();});方案三:定期自动更新// 每 60 分钟检查一次更新setInterval(() => { navigator.serviceWorker.ready.then(registration => { registration.update(); });}, 60 * 60 * 1000);更新通知 UI 示例function showUpdateBar() { const updateBar = document.createElement('div'); updateBar.className = 'update-bar'; updateBar.innerHTML = ` <span>发现新版本,是否更新?</span> <button id="update-btn">立即更新</button> <button id="dismiss-btn">稍后</button> `; document.body.appendChild(updateBar); document.getElementById('update-btn').addEventListener('click', () => { applyUpdate(); updateBar.remove(); }); document.getElementById('dismiss-btn').addEventListener('click', () => { updateBar.remove(); });}最佳实践1. 版本控制// 使用版本号管理缓存const CACHE_VERSION = 'v2.1.0';const CACHE_NAME = `app-cache-${CACHE_VERSION}`;2. 增量更新// 只更新变化的资源const urlsToCache = [ '/', '/index.html?v=2', // 添加版本号 '/app.js?v=2.1', '/styles.css?v=2.1'];3. 更新策略选择| 策略 | 适用场景 | 用户体验 ||------|----------|----------|| 立即更新 | 开发环境、紧急修复 | 强制刷新 || 优雅更新 | 生产环境 | 用户可控 || 静默更新 | 非关键更新 | 无感知 |4. 避免更新陷阱// ❌ 错误:缓存 Service Worker 本身// Service Worker 文件不应该被缓存// ✅ 正确:确保 Service Worker 文件不被缓存// 在服务器配置中设置:// Cache-Control: no-cache, no-store, must-revalidate调试技巧// 查看当前 Service Worker 状态navigator.serviceWorker.ready.then(registration => { console.log('Active:', registration.active); console.log('Installing:', registration.installing); console.log('Waiting:', registration.waiting);});// 强制更新navigator.serviceWorker.getRegistration().then(reg => { reg.update();});// 注销 Service Workernavigator.serviceWorker.getRegistration().then(reg => { reg.unregister();});总结自动检测:浏览器自动检测 Service Worker 文件变化安装等待:新版本安装后默认进入等待状态激活接管:旧页面关闭后新版本激活缓存清理:activate 阶段清理旧版本缓存用户提示:提供友好的更新提示机制
阅读 0·3月7日 12:06

Service Worker 的生命周期包含哪些阶段?

Service Worker 生命周期详解Service Worker 的生命周期是理解其工作原理的核心,包含以下关键阶段:1. 注册阶段(Registration)// 在主线程中注册if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('SW registered:', registration); }) .catch(error => { console.log('SW registration failed:', error); });}2. 安装阶段(Installation)浏览器下载 Service Worker 脚本触发 install 事件适合进行静态资源的预缓存self.addEventListener('install', event => { event.waitUntil( caches.open('v1').then(cache => { return cache.addAll([ '/', '/styles.css', '/app.js' ]); }) ); // 立即激活新的 Service Worker self.skipWaiting();});3. 等待阶段(Waiting)新的 Service Worker 安装完成后进入等待状态等待旧的 Service Worker 控制的所有页面关闭可以通过 skipWaiting() 跳过等待4. 激活阶段(Activation)旧的 Service Worker 被替换后触发 activate 事件适合清理旧缓存此时 Service Worker 开始控制页面self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames .filter(name => name !== 'v1') .map(name => caches.delete(name)) ); }) ); // 立即接管页面 self.clients.claim();});5. 运行阶段(Running/Idle)监听 fetch、push、sync 等事件处理网络请求和后台任务浏览器可能随时终止 Service Worker 以节省资源6. 终止阶段(Termination)浏览器自动回收资源下次事件触发时重新启动保持状态不丢失(但全局变量会重置)状态转换图注册 → 下载 → 安装中 → 安装完成 → 等待中 → 激活中 → 激活完成 → 空闲/运行 ↓ 被新 SW 替换最佳实践版本管理:使用版本号管理缓存(如 'v1', 'v2')优雅升级:通过页面刷新触发新 Service Worker 激活缓存清理:在 activate 阶段清理过期缓存错误处理:每个阶段都要添加适当的错误处理
阅读 0·3月7日 12:05

Service Worker 如何实现离线访问功能?

Service Worker 离线访问实现详解离线访问是 Service Worker 最核心的功能之一,让 Web 应用在无网络环境下仍能正常工作。核心原理Service Worker 作为网络代理,拦截所有 HTTP 请求,根据缓存策略决定从缓存返回还是请求网络。实现步骤1. 注册 Service Worker// main.jsif ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('SW registered:', registration.scope); }) .catch(error => { console.log('SW registration failed:', error); }); });}2. 预缓存核心资源// sw.jsconst CACHE_NAME = 'offline-cache-v1';const urlsToCache = [ '/', '/index.html', '/styles.css', '/app.js', '/icons/icon-192x192.png', '/offline.html' // 离线 fallback 页面];self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('Cache opened'); return cache.addAll(urlsToCache); }) .catch(err => console.error('Cache failed:', err)) ); self.skipWaiting();});3. 拦截请求并返回缓存self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(response => { // 缓存命中,直接返回 if (response) { return response; } // 缓存未命中,请求网络 return fetch(event.request) .then(networkResponse => { // 动态缓存新资源 if (!networkResponse || networkResponse.status !== 200) { return networkResponse; } const responseToCache = networkResponse.clone(); caches.open(CACHE_NAME).then(cache => { cache.put(event.request, responseToCache); }); return networkResponse; }) .catch(() => { // 网络失败,返回离线页面 if (event.request.mode === 'navigate') { return caches.match('/offline.html'); } }); }) );});4. 清理旧缓存self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames .filter(name => name !== CACHE_NAME) .map(name => caches.delete(name)) ); }) ); self.clients.claim();});离线页面设计<!-- offline.html --><!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8"> <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; } .offline-icon { font-size: 64px; margin-bottom: 20px; } h1 { color: #333; } p { color: #666; } 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>您当前处于离线状态</h1> <p>请检查网络连接后重试</p> <button onclick="location.reload()">重新加载</button> </div></body></html>高级离线策略1. 网络优先 + 缓存回退self.addEventListener('fetch', event => { event.respondWith( fetch(event.request) .then(response => { // 更新缓存 const clone = response.clone(); caches.open(CACHE_NAME).then(cache => { cache.put(event.request, clone); }); return response; }) .catch(() => caches.match(event.request)) );});2. 缓存优先 + 后台更新self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request).then(response => { const fetchPromise = fetch(event.request).then(networkResponse => { caches.open(CACHE_NAME).then(cache => { cache.put(event.request, networkResponse.clone()); }); return networkResponse; }); return response || fetchPromise; }) );});检测网络状态// 主线程中检测window.addEventListener('online', () => { console.log('网络已连接');});window.addEventListener('offline', () => { console.log('网络已断开');});// 检查当前状态if (navigator.onLine) { console.log('在线');} else { console.log('离线');}最佳实践核心资源优先:确保 HTML、CSS、JS 等核心资源被缓存优雅降级:网络失败时提供友好的离线页面缓存更新:定期更新缓存,避免用户长期看到旧版本缓存清理:及时清理过期缓存,避免存储溢出测试验证:使用 Chrome DevTools 的 Network 面板模拟离线环境
阅读 0·3月7日 12:05

Service Worker 调试有哪些常用方法和工具?

Service Worker 调试方法与工具详解Service Worker 运行在浏览器后台,调试相对复杂。掌握正确的调试方法和工具对于开发至关重要。Chrome DevTools 调试1. Application 面板Chrome DevTools 的 Application 面板是调试 Service Worker 的主要工具。DevTools → Application → Service Workers主要功能:查看已注册的 Service Worker查看当前状态(activated, installing, waiting 等)手动触发更新(Update)注销 Service Worker(Unregister)模拟离线环境(Offline checkbox)绕过网络(Bypass for network)2. 网络面板调试// 查看 Service Worker 拦截的请求// DevTools → Network → 查看 Size 列// (from ServiceWorker) 表示从缓存返回// (from disk cache) 表示从磁盘缓存返回// (from memory cache) 表示从内存缓存返回3. Console 面板// Service Worker 中的 console.log 会显示在 DevTools Console// 注意:需要勾选 "Preserve log" 以保留刷新前的日志// 查看当前 Service Worker 状态navigator.serviceWorker.ready.then(registration => { console.log('Service Worker 状态:', registration);});常用调试技巧1. 强制更新 Service Worker// 方法1:手动更新navigator.serviceWorker.getRegistration().then(reg => { reg.update();});// 方法2:硬刷新(Ctrl+Shift+R 或 Cmd+Shift+R)// 会绕过 Service Worker,安装新版本// 方法3:DevTools Application 面板点击 Update2. 查看缓存内容// 查看所有缓存async function inspectCaches() { const cacheNames = await caches.keys(); console.log('缓存列表:', cacheNames); for (const name of cacheNames) { const cache = await caches.open(name); const requests = await cache.keys(); console.log(`缓存 ${name} 内容:`, requests.map(r => r.url)); }}inspectCaches();3. 清除缓存// 清除所有缓存async function clearAllCaches() { const cacheNames = await caches.keys(); await Promise.all(cacheNames.map(name => caches.delete(name))); console.log('所有缓存已清除');}// 清除特定缓存async function clearCache(cacheName) { await caches.delete(cacheName); console.log(`缓存 ${cacheName} 已清除`);}4. 模拟离线环境// 方法1:DevTools Network 面板// 选择 "Offline" 或设置自定义网络条件// 方法2:代码中检测window.addEventListener('offline', () => { console.log('进入离线模式');});// 方法3:手动触发离线状态// navigator.connection 可以查看网络状态console.log('网络状态:', navigator.connection);5. 调试 Fetch 事件// sw.js 中添加详细日志self.addEventListener('fetch', event => { console.log('Fetch 请求:', { url: event.request.url, method: event.request.method, mode: event.request.mode, destination: event.request.destination }); event.respondWith( caches.match(event.request).then(response => { if (response) { console.log('缓存命中:', event.request.url); return response; } console.log('缓存未命中,请求网络:', event.request.url); return fetch(event.request); }) );});高级调试技术1. 使用 Chrome 的 Service Worker 内部页面chrome://serviceworker-internals/可以查看所有 Service Worker 的详细信息,包括:注册信息控制台日志网络请求存储使用情况2. 使用 Workbox 调试如果使用 Workbox,可以启用详细日志:// 启用 Workbox 调试workbox.setConfig({ debug: true});// 查看 Workbox 日志workbox.core.setLogLevel(workbox.core.LOG_LEVELS.debug);3. 断点调试// 在 Service Worker 代码中设置 debuggerself.addEventListener('fetch', event => { debugger; // DevTools 会在这里暂停 event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) );});4. 使用 console.table 查看缓存async function logCacheContents() { const cache = await caches.open('my-cache'); const requests = await cache.keys(); const tableData = await Promise.all( requests.map(async request => { const response = await cache.match(request); return { URL: request.url, Status: response.status, Type: response.headers.get('content-type'), Size: response.headers.get('content-length') }; }) ); console.table(tableData);}常见问题排查1. Service Worker 未注册// 检查浏览器支持if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') .then(reg => console.log('注册成功:', reg)) .catch(err => console.error('注册失败:', err));} else { console.error('浏览器不支持 Service Worker');}2. HTTPS 问题// 检查是否 HTTPS(localhost 除外)if (location.protocol !== 'https:' && location.hostname !== 'localhost') { console.error('Service Worker 需要 HTTPS 环境');}3. 缓存未更新// 确保每次更新 Service Worker 时更改缓存名称const CACHE_NAME = 'my-app-v2'; // 更新版本号self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames .filter(name => name !== CACHE_NAME) .map(name => caches.delete(name)) ); }) );});4. 跨域资源缓存问题// CORS 资源需要特殊处理self.addEventListener('fetch', event => { if (event.request.mode === 'cors') { // 跨域请求使用 no-cors 模式缓存 event.respondWith( fetch(event.request, { mode: 'no-cors' }) .then(response => { return caches.open('cors-cache').then(cache => { cache.put(event.request, response.clone()); return response; }); }) ); }});调试清单开发阶段检查项[ ] Service Worker 成功注册[ ] Install 事件正常触发[ ] 静态资源正确缓存[ ] Fetch 事件正确拦截[ ] 缓存策略按预期工作[ ] Activate 事件清理旧缓存[ ] 离线功能正常生产环境检查项[ ] Service Worker 文件不被缓存[ ] 缓存版本控制正确[ ] 更新机制正常工作[ ] 错误处理完善[ ] 降级方案有效推荐工具| 工具 | 用途 ||------|------|| Chrome DevTools | 主要调试工具 || Workbox | Service Worker 库,带调试功能 || Lighthouse | PWA 审核和性能分析 || PWA Builder | 验证 PWA 配置 || Web Server for Chrome | 本地 HTTPS 测试 |调试最佳实践使用 Chrome DevTools:充分利用 Application 面板添加详细日志:开发和生产环境使用不同日志级别版本控制:每次更新更改缓存名称测试离线:定期测试离线功能监控错误:使用错误追踪服务监控 Service Worker 错误
阅读 0·3月7日 12:05

Service Worker 如何实现跨域资源的缓存?

Service Worker 跨域资源缓存详解跨域资源缓存是 Service Worker 中的常见问题,由于浏览器的同源策略(Same-Origin Policy),跨域请求的缓存需要特殊处理。跨域请求的基本概念什么是跨域请求// 同源请求(Same-Origin)// 当前页面: https://example.com/page.htmlfetch('/api/data'); // ✅ 同源fetch('https://example.com/api'); // ✅ 同源// 跨域请求(Cross-Origin)fetch('https://api.other.com/data'); // ❌ 跨域(不同域名)fetch('https://cdn.example.com/file'); // ❌ 跨域(不同子域)CORS 和 Opaque Response// CORS 请求 - 服务器允许跨域fetch('https://api.example.com/data', { mode: 'cors' // 默认模式});// 响应包含完整的 headers 和 body// No-CORS 请求 - 服务器不允许跨域fetch('https://cdn.example.com/image.jpg', { mode: 'no-cors'});// 响应是 "opaque",无法读取内容,但可以缓存跨域资源缓存方案方案1:使用 CORS 模式(推荐)// sw.js// 服务器需要设置 CORS 头:Access-Control-Allow-Origin: *self.addEventListener('fetch', event => { const url = new URL(event.request.url); // 处理跨域 API 请求 if (url.hostname === 'api.example.com') { event.respondWith( caches.match(event.request).then(response => { if (response) { return response; } // 使用 cors 模式请求 return fetch(event.request, { mode: 'cors' }) .then(networkResponse => { // 检查响应是否成功 if (!networkResponse || networkResponse.status !== 200) { return networkResponse; } // 缓存响应 const responseToCache = networkResponse.clone(); caches.open('api-cache').then(cache => { cache.put(event.request, responseToCache); }); return networkResponse; }) .catch(error => { console.error('跨域请求失败:', error); // 返回离线降级内容 return new Response(JSON.stringify({ error: 'Network error', offline: true }), { headers: { 'Content-Type': 'application/json' } }); }); }) ); }});方案2:使用 No-CORS 模式缓存 Opaque 响应// sw.js// 适用于 CDN 资源、图片等不需要读取内容的资源self.addEventListener('fetch', event => { const url = new URL(event.request.url); // 处理 CDN 图片资源 if (url.hostname === 'cdn.example.com') { event.respondWith( caches.match(event.request).then(response => { if (response) { return response; } // 使用 no-cors 模式 return fetch(event.request, { mode: 'no-cors' }) .then(networkResponse => { // Opaque 响应可以缓存,但无法读取状态和内容 const responseToCache = networkResponse.clone(); caches.open('cdn-cache').then(cache => { cache.put(event.request, responseToCache); }); return networkResponse; }); }) ); }});Opaque 响应的特点:状态码始终为 0无法读取响应头无法读取响应体占用缓存配额(约是实际大小的 7 倍)方案3:使用 CORS 代理// 如果第三方 API 不支持 CORS,可以搭建代理服务器// sw.jsself.addEventListener('fetch', event => { const url = new URL(event.request.url); // 代理跨域请求 if (url.pathname.startsWith('/proxy/')) { const targetUrl = url.pathname.replace('/proxy/', ''); event.respondWith( caches.match(event.request).then(response => { if (response) { return response; } // 通过同域代理转发请求 return fetch(`/api/proxy?url=${encodeURIComponent(targetUrl)}`) .then(networkResponse => { const responseToCache = networkResponse.clone(); caches.open('proxy-cache').then(cache => { cache.put(event.request, responseToCache); }); return networkResponse; }); }) ); }});方案4:预缓存跨域资源// sw.jsconst CROSS_ORIGIN_CACHE = 'cross-origin-v1';// 需要预缓存的跨域资源const crossOriginResources = [ 'https://fonts.googleapis.com/css?family=Roboto', 'https://fonts.gstatic.com/s/roboto/v20/font.woff2', 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js'];self.addEventListener('install', event => { event.waitUntil( caches.open(CROSS_ORIGIN_CACHE).then(cache => { // 使用 addAll 预缓存(自动处理 CORS) return cache.addAll(crossOriginResources); }) );});self.addEventListener('fetch', event => { const url = new URL(event.request.url); // 匹配跨域资源 if (crossOriginResources.some(resource => event.request.url.includes(resource))) { event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) ); }});实际应用示例示例1:缓存 Google Fonts// sw.jsconst FONT_CACHE = 'fonts-v1';self.addEventListener('fetch', event => { const url = new URL(event.request.url); // Google Fonts CSS(CORS 支持) if (url.hostname === 'fonts.googleapis.com') { event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request).then(fetchResponse => { return caches.open(FONT_CACHE).then(cache => { cache.put(event.request, fetchResponse.clone()); return fetchResponse; }); }); }) ); } // Google Fonts 字体文件(Opaque 响应) if (url.hostname === 'fonts.gstatic.com') { event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request, { mode: 'no-cors' }) .then(fetchResponse => { return caches.open(FONT_CACHE).then(cache => { cache.put(event.request, fetchResponse.clone()); return fetchResponse; }); }); }) ); }});示例2:缓存 CDN 资源// sw.jsconst CDN_CACHE = 'cdn-v1';const CDN_HOSTS = [ 'cdn.jsdelivr.net', 'unpkg.com', 'cdnjs.cloudflare.com'];self.addEventListener('fetch', event => { const url = new URL(event.request.url); // 检查是否是 CDN 资源 if (CDN_HOSTS.includes(url.hostname)) { event.respondWith( caches.match(event.request).then(response => { if (response) { return response; } // CDN 资源使用 no-cors 模式 return fetch(event.request, { mode: 'no-cors' }) .then(fetchResponse => { // 只缓存成功的响应 if (fetchResponse.type === 'opaque' || fetchResponse.ok) { const responseToCache = fetchResponse.clone(); caches.open(CDN_CACHE).then(cache => { cache.put(event.request, responseToCache); }); } return fetchResponse; }) .catch(() => { // 离线时返回降级 return new Response('CDN resource unavailable offline'); }); }) ); }});示例3:处理跨域图片// sw.jsconst IMAGE_CACHE = 'images-v1';self.addEventListener('fetch', event => { const request = event.request; // 处理图片请求 if (request.destination === 'image') { event.respondWith( caches.match(request).then(response => { if (response) { return response; } // 判断是否是跨域请求 const url = new URL(request.url); const isCrossOrigin = url.origin !== location.origin; const fetchOptions = isCrossOrigin ? { mode: 'no-cors' } : {}; return fetch(request, fetchOptions).then(fetchResponse => { // 检查响应是否有效 if (!fetchResponse || fetchResponse.status === 0) { // Opaque 响应或失败 return fetchResponse; } const responseToCache = fetchResponse.clone(); caches.open(IMAGE_CACHE).then(cache => { cache.put(request, responseToCache); }); return fetchResponse; }).catch(() => { // 返回占位图 return caches.match('/images/placeholder.png'); }); }) ); }});跨域缓存的注意事项1. Opaque 响应的存储成本// Opaque 响应的存储成本约为实际大小的 7 倍// 需要谨慎使用,避免超出存储配额async function checkOpaqueResponseSize() { const cache = await caches.open('cdn-cache'); const requests = await cache.keys(); let totalSize = 0; for (const request of requests) { const response = await cache.match(request); if (response.type === 'opaque') { // Opaque 响应无法获取实际大小 // 需要预估或限制数量 totalSize += 1024 * 1024; // 假设每个 1MB } } console.log(`预估 Opaque 响应总大小: ${totalSize / 1024 / 1024} MB`);}2. 缓存清理策略// 定期清理跨域缓存async function cleanCrossOriginCache() { const cache = await caches.open('cdn-cache'); const requests = await cache.keys(); // 只保留最近访问的 50 个资源 if (requests.length > 50) { const toDelete = requests.slice(0, requests.length - 50); await Promise.all( toDelete.map(request => cache.delete(request)) ); }}// 在 activate 事件中执行清理self.addEventListener('activate', event => { event.waitUntil(cleanCrossOriginCache());});3. 错误处理// 跨域请求的错误处理async function fetchWithFallback(request, isCrossOrigin) { try { const fetchOptions = isCrossOrigin ? { mode: 'no-cors' } : {}; const response = await fetch(request, fetchOptions); // Opaque 响应 status 为 0,需要特殊处理 if (response.type === 'opaque' || response.ok) { return response; } throw new Error(`HTTP ${response.status}`); } catch (error) { console.error('Fetch failed:', error); // 返回缓存或降级响应 const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // 返回默认响应 return new Response('Resource unavailable', { status: 503 }); }}最佳实践优先使用 CORS:如果服务器支持 CORS,优先使用 cors 模式限制 Opaque 响应数量:Opaque 响应占用更多存储空间合理设置缓存策略:跨域资源变化频繁时需要设置过期时间提供降级方案:跨域资源加载失败时提供替代内容监控存储使用:定期检查缓存大小,避免超出配额
阅读 0·3月7日 12:05

Spring Boot 的启动流程是怎样的?

Spring Boot 启动流程详解入口方法@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}启动流程总览SpringApplication.run()├── 1. 创建 SpringApplication 实例│ ├── 推断应用类型 (Servlet/Reactive/None)│ ├── 加载 BootstrapRegistryInitializer│ ├── 加载 ApplicationContextInitializer│ └── 加载 ApplicationListener│└── 2. 执行 run() 方法 ├── 2.1 启动计时器 ├── 2.2 创建 DefaultBootstrapContext ├── 2.3 配置 headless 模式 ├── 2.4 发布 Starting 事件 ├── 2.5 准备 Environment ├── 2.6 打印 Banner ├── 2.7 创建 ApplicationContext ├── 2.8 准备 ApplicationContext ├── 2.9 刷新 ApplicationContext ├── 2.10 执行 Runner └── 2.11 发布 Started 事件详细启动步骤第一步:创建 SpringApplication 实例public SpringApplication(Class<?>... primarySources) { // 资源加载器 this.resourceLoader = null; // 主配置类 this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); // 推断应用类型 this.webApplicationType = WebApplicationType.deduceFromClasspath(); // 加载 BootstrapRegistryInitializer this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories(); // 加载 ApplicationContextInitializer setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); // 加载 ApplicationListener setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); // 推断主类 this.mainApplicationClass = deduceMainApplicationClass();}应用类型推断逻辑:static WebApplicationType deduceFromClasspath() { // 存在 reactor.netty.http.server.HttpServer 且不存在 // org.springframework.web.servlet.DispatcherServlet if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null) && !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)) { return WebApplicationType.REACTIVE; } // 存在 javax.servlet.Servlet 或 jakarta.servlet.Servlet for (String className : SERVLET_INDICATOR_CLASSES) { if (ClassUtils.isPresent(className, null)) { return WebApplicationType.SERVLET; } } return WebApplicationType.NONE;}第二步:执行 run() 方法核心流程public ConfigurableApplicationContext run(String... args) { // 1. 启动计时器 StartupStep startupStep = this.applicationStartup.start("spring.boot.application.starting"); // 2. 创建 BootstrapContext DefaultBootstrapContext bootstrapContext = createBootstrapContext(); // 3. 配置 headless 模式 configureHeadlessProperty(); // 4. 获取 SpringApplicationRunListeners SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting(bootstrapContext, this.mainApplicationClass); try { // 5. 准备 ApplicationArguments ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); // 6. 准备 Environment ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments); configureIgnoreBeanInfo(environment); // 7. 打印 Banner Banner printedBanner = printBanner(environment); // 8. 创建 ApplicationContext context = createApplicationContext(); context.setApplicationStartup(this.applicationStartup); // 9. 准备 ApplicationContext prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); // 10. 刷新 ApplicationContext(核心) refreshContext(context); // 11. 刷新后处理 afterRefresh(context, applicationArguments); // 12. 启动完成 listeners.started(context); // 13. 执行 Runner callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, listeners); throw new IllegalStateException(ex); } try { listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, null); throw new IllegalStateException(ex); } return context;}第三步:准备 Environmentprivate ConfigurableEnvironment prepareEnvironment( SpringApplicationRunListeners listeners, DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) { // 创建或获取 Environment ConfigurableEnvironment environment = getOrCreateEnvironment(); // 配置 PropertySources 和 Profiles configureEnvironment(environment, applicationArguments.getSourceArgs()); // 发布 EnvironmentPrepared 事件 listeners.environmentPrepared(bootstrapContext, environment); // 绑定到 SpringApplication ConfigurationPropertySources.attach(environment); return environment;}配置文件加载顺序(优先级从高到低):命令行参数java:comp/env 的 JNDI 属性Java 系统属性(System.getProperties())操作系统环境变量RandomValuePropertySourcejar 包外部的 application-{profile}.propertiesjar 包内部的 application-{profile}.propertiesjar 包外部的 application.propertiesjar 包内部的 application.properties@PropertySource 注解定义的属性默认属性(SpringApplication.setDefaultProperties)第四步:创建 ApplicationContextprotected ConfigurableApplicationContext createApplicationContext() { return this.applicationContextFactory.apply(this.webApplicationType);}// 根据应用类型创建不同的上下文// Servlet -> AnnotationConfigServletWebServerApplicationContext// Reactive -> AnnotationConfigReactiveWebServerApplicationContext// None -> AnnotationConfigApplicationContext第五步:准备 ApplicationContextprivate void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { // 设置 Environment context.setEnvironment(environment); // 应用后处理器 postProcessApplicationContext(context); // 执行 ApplicationContextInitializer applyInitializers(context); // 发布 ContextPrepared 事件 listeners.contextPrepared(context); // 注册 Spring Boot 特殊 Bean ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); beanFactory.registerSingleton("springApplicationArguments", applicationArguments); if (printedBanner != null) { beanFactory.registerSingleton("springBootBanner", printedBanner); } // 加载主配置类 Set<Object> sources = getAllSources(); load(context, sources.toArray(new Object[0])); // 发布 ContextLoaded 事件 listeners.contextLoaded(context);}第六步:刷新 ApplicationContext(核心)private void refreshContext(ConfigurableApplicationContext context) { refresh(context); // 注册 ShutdownHook if (this.registerShutdownHook) { shutdownHook.registerApplicationContext(context); }}// 调用 AbstractApplicationContext.refresh()@Overridepublic void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // 1. 准备刷新 prepareRefresh(); // 2. 获取 BeanFactory ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // 3. 准备 BeanFactory prepareBeanFactory(beanFactory); try { // 4. 子类扩展 postProcessBeanFactory(beanFactory); // 5. 执行 BeanFactoryPostProcessor invokeBeanFactoryPostProcessors(beanFactory); // 6. 注册 BeanPostProcessor registerBeanPostProcessors(beanFactory); // 7. 初始化 MessageSource initMessageSource(); // 8. 初始化事件广播器 initApplicationEventMulticaster(); // 9. 子类扩展(WebServer 在此初始化) onRefresh(); // 10. 注册监听器 registerListeners(); // 11. 初始化所有非懒加载单例 Bean finishBeanFactoryInitialization(beanFactory); // 12. 完成刷新 finishRefresh(); } catch (BeansException ex) { destroyBeans(); closeBeanFactory(); throw ex; } }}WebServer 启动时机:// ServletWebServerApplicationContext.onRefresh()@Overrideprotected void onRefresh() { super.onRefresh(); try { // 创建并启动 WebServer(Tomcat/Jetty/Undertow) createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException("Unable to start web server", ex); }}第七步:执行 Runnerprivate void callRunners(ApplicationContext context, ApplicationArguments args) { List<Object> runners = new ArrayList<>(); // 获取所有 ApplicationRunner runners.addAll(context.getBeansOfType(ApplicationRunner.class).values()); // 获取所有 CommandLineRunner runners.addAll(context.getBeansOfType(CommandLineRunner.class).values()); // 排序并执行 AnnotationAwareOrderComparator.sort(runners); for (Object runner : new LinkedHashSet<>(runners)) { if (runner instanceof ApplicationRunner) { callRunner((ApplicationRunner) runner, args); } if (runner instanceof CommandLineRunner) { callRunner((CommandLineRunner) runner, args); } }}启动事件监听// 可以通过实现 ApplicationListener 监听启动过程@Componentpublic class MyApplicationListener implements ApplicationListener<ApplicationStartedEvent> { @Override public void onApplicationEvent(ApplicationStartedEvent event) { System.out.println("Application started!"); }}主要事件类型:| 事件 | 触发时机 ||------|---------|| ApplicationStartingEvent | run 方法开始执行时 || ApplicationEnvironmentPreparedEvent | Environment 准备完成时 || ApplicationContextInitializedEvent | Context 初始化完成时 || ApplicationPreparedEvent | Context 准备完成时 || ApplicationStartedEvent | Context 刷新完成时 || ApplicationReadyEvent | 所有 Runner 执行完成时 || ApplicationFailedEvent | 启动失败时 |总结Spring Boot 启动流程可以概括为:实例化阶段:推断应用类型,加载初始化器和监听器环境准备阶段:准备 Environment,加载配置文件上下文创建阶段:创建并配置 ApplicationContext刷新阶段:加载 Bean 定义,初始化 Bean,启动 WebServer启动完成阶段:执行 Runner,发布启动完成事件理解启动流程有助于排查启动问题、自定义启动逻辑和优化启动性能。
阅读 0·3月7日 12:04