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

服务端面试题手册

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

WebGL 中的立方体贴图(Cubemap)是什么?有哪些应用场景?

WebGL 立方体贴图概述立方体贴图(Cubemap)是一种特殊的纹理,由 6 张独立的 2D 纹理组成,分别对应立方体的 6 个面。它使用 3D 方向向量进行采样,常用于实现天空盒、环境反射和折射等效果。立方体贴图的结构立方体贴图由 6 个面组成: ┌─────────┐ │ +Y │ (Top)┌──────┼─────────┼──────┬─────────┐│ -X │ +Z │ +X │ -Z ││ Left │ Front │ Right│ Back │└──────┴─────────┴──────┴─────────┘ │ -Y │ (Bottom) └─────────┘创建立方体贴图基本创建流程// 创建立方体贴图const cubemap = gl.createTexture();gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap);// 6 个面的图片 URLconst faceImages = [ 'px.jpg', // +X (Right) 'nx.jpg', // -X (Left) 'py.jpg', // +Y (Top) 'ny.jpg', // -Y (Bottom) 'pz.jpg', // +Z (Front) 'nz.jpg' // -Z (Back)];// 加载 6 个面的图片const targets = [ gl.TEXTURE_CUBE_MAP_POSITIVE_X, gl.TEXTURE_CUBE_MAP_NEGATIVE_X, gl.TEXTURE_CUBE_MAP_POSITIVE_Y, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, gl.TEXTURE_CUBE_MAP_POSITIVE_Z, gl.TEXTURE_CUBE_MAP_NEGATIVE_Z];let loadedCount = 0;faceImages.forEach((src, index) => { const image = new Image(); image.onload = () => { gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap); gl.texImage2D( targets[index], 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image ); loadedCount++; if (loadedCount === 6) { // 所有面加载完成,生成 mipmap gl.generateMipmap(gl.TEXTURE_CUBE_MAP); } }; image.src = src;});// 设置纹理参数gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE);程序化生成立方体贴图// 创建纯色立方体贴图function createSolidColorCubemap(gl, color) { const cubemap = gl.createTexture(); gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap); const targets = [ gl.TEXTURE_CUBE_MAP_POSITIVE_X, gl.TEXTURE_CUBE_MAP_NEGATIVE_X, gl.TEXTURE_CUBE_MAP_POSITIVE_Y, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, gl.TEXTURE_CUBE_MAP_POSITIVE_Z, gl.TEXTURE_CUBE_MAP_NEGATIVE_Z ]; const size = 1; const data = new Uint8Array([ color[0] * 255, color[1] * 255, color[2] * 255, (color[3] || 1) * 255 ]); targets.forEach(target => { gl.texImage2D( target, 0, gl.RGBA, size, size, 0, gl.RGBA, gl.UNSIGNED_BYTE, data ); }); return cubemap;}在着色器中使用立方体贴图顶点着色器attribute vec3 a_position;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;varying vec3 v_worldPos;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); v_worldPos = worldPos.xyz; gl_Position = u_projectionMatrix * u_viewMatrix * worldPos;}片段着色器precision mediump float;varying vec3 v_worldPos;uniform vec3 u_cameraPos;uniform samplerCube u_cubemap;void main() { // 计算从相机指向片段的方向向量 vec3 direction = normalize(v_worldPos - u_cameraPos); // 使用方向向量采样立方体贴图 vec4 color = textureCube(u_cubemap, direction); gl_FragColor = color;}主要应用场景1. 天空盒(Skybox)天空盒用于渲染远处的环境背景,给人一种无限大的感觉。// 天空盒顶点着色器attribute vec3 a_position;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;varying vec3 v_texCoord;void main() { // 移除平移分量,只保留旋转 mat4 viewRotation = mat4(mat3(u_viewMatrix)); vec4 pos = u_projectionMatrix * viewRotation * vec4(a_position, 1.0); // 确保天空盒在远裁剪面 gl_Position = pos.xyww; // 使用位置作为纹理坐标 v_texCoord = a_position;}// 天空盒片段着色器precision mediump float;varying vec3 v_texCoord;uniform samplerCube u_skybox;void main() { gl_FragColor = textureCube(u_skybox, v_texCoord);}// 渲染天空盒function drawSkybox(gl, skyboxProgram, cubemap) { // 禁用深度写入 gl.depthMask(false); gl.useProgram(skyboxProgram); // 绑定立方体贴图 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap); gl.uniform1i(gl.getUniformLocation(skyboxProgram, 'u_skybox'), 0); // 绘制立方体 drawCube(gl); // 恢复深度写入 gl.depthMask(true);}2. 环境反射(Environment Reflection)使用立方体贴图模拟光滑表面的反射效果。// 反射顶点着色器attribute vec3 a_position;attribute vec3 a_normal;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;varying vec3 v_worldPos;varying vec3 v_normal;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); v_worldPos = worldPos.xyz; v_normal = mat3(u_modelMatrix) * a_normal; gl_Position = u_projectionMatrix * u_viewMatrix * worldPos;}// 反射片段着色器precision mediump float;varying vec3 v_worldPos;varying vec3 v_normal;uniform vec3 u_cameraPos;uniform samplerCube u_environmentMap;uniform float u_reflectivity;void main() { vec3 normal = normalize(v_normal); vec3 viewDir = normalize(u_cameraPos - v_worldPos); // 计算反射向量 vec3 reflectDir = reflect(-viewDir, normal); // 采样环境贴图 vec4 reflectionColor = textureCube(u_environmentMap, reflectDir); // 可以结合基础颜色和反射 vec3 baseColor = vec3(0.8, 0.8, 0.8); vec3 finalColor = mix(baseColor, reflectionColor.rgb, u_reflectivity); gl_FragColor = vec4(finalColor, 1.0);}3. 环境折射(Environment Refraction)模拟光线穿过透明物体的折射效果。// 折射片段着色器precision mediump float;varying vec3 v_worldPos;varying vec3 v_normal;uniform vec3 u_cameraPos;uniform samplerCube u_environmentMap;uniform float u_refractiveIndex; // 折射率,如 1.52 表示玻璃void main() { vec3 normal = normalize(v_normal); vec3 viewDir = normalize(u_cameraPos - v_worldPos); // 计算折射向量 // refract(I, N, eta) 其中 eta = 入射介质折射率 / 折射介质折射率 vec3 refractDir = refract(-viewDir, normal, 1.0 / u_refractiveIndex); // 采样环境贴图 vec4 refractionColor = textureCube(u_environmentMap, refractDir); gl_FragColor = refractionColor;}4. 菲涅尔反射(Fresnel Reflection)模拟真实世界中反射强度随视角变化的效应。// 菲涅尔反射片段着色器precision mediump float;varying vec3 v_worldPos;varying vec3 v_normal;uniform vec3 u_cameraPos;uniform samplerCube u_environmentMap;uniform float u_fresnelPower;void main() { vec3 normal = normalize(v_normal); vec3 viewDir = normalize(u_cameraPos - v_worldPos); // 计算反射向量 vec3 reflectDir = reflect(-viewDir, normal); vec4 reflectionColor = textureCube(u_environmentMap, reflectDir); // 计算菲涅尔因子 // 视线与法线夹角越大,反射越强 float fresnel = pow(1.0 - max(dot(viewDir, normal), 0.0), u_fresnelPower); // 基础颜色 vec3 baseColor = vec3(0.1, 0.2, 0.3); // 混合基础颜色和反射 vec3 finalColor = mix(baseColor, reflectionColor.rgb, fresnel); gl_FragColor = vec4(finalColor, 1.0);}5. 动态环境贴图(Dynamic Environment Mapping)实时生成立方体贴图用于反射。// 动态生成环境贴图function generateEnvironmentMap(gl, scene, centerPos, size = 256) { // 创建立方体贴图 const cubemap = gl.createTexture(); gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubemap); // 创建帧缓冲区 const framebuffer = gl.createFramebuffer(); // 6 个面的相机方向 const directions = [ { target: gl.TEXTURE_CUBE_MAP_POSITIVE_X, eye: [1, 0, 0], up: [0, -1, 0] }, { target: gl.TEXTURE_CUBE_MAP_NEGATIVE_X, eye: [-1, 0, 0], up: [0, -1, 0] }, { target: gl.TEXTURE_CUBE_MAP_POSITIVE_Y, eye: [0, 1, 0], up: [0, 0, 1] }, { target: gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, eye: [0, -1, 0], up: [0, 0, -1] }, { target: gl.TEXTURE_CUBE_MAP_POSITIVE_Z, eye: [0, 0, 1], up: [0, -1, 0] }, { target: gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, eye: [0, 0, -1], up: [0, -1, 0] } ]; // 设置投影矩阵(90度视野) const projectionMatrix = mat4.create(); mat4.perspective(projectionMatrix, Math.PI / 2, 1, 0.1, 1000); // 渲染 6 个面 directions.forEach(dir => { // 设置视图矩阵 const viewMatrix = mat4.create(); mat4.lookAt(viewMatrix, centerPos, [centerPos[0] + dir.eye[0], centerPos[1] + dir.eye[1], centerPos[2] + dir.eye[2]], dir.up ); // 渲染场景到帧缓冲区 renderSceneToCubemapFace(gl, scene, framebuffer, cubemap, dir.target, projectionMatrix, viewMatrix, size); }); return cubemap;}立方体贴图采样原理立方体贴图使用 3D 方向向量 (x, y, z) 进行采样,选择哪个面取决于哪个分量的绝对值最大:如果 |x| 最大: x > 0: 使用 +X 面,坐标 ( -z/x, -y/x ) x < 0: 使用 -X 面,坐标 ( z/x, -y/x )如果 |y| 最大: y > 0: 使用 +Y 面,坐标 ( x/y, z/y ) y < 0: 使用 -Y 面,坐标 ( x/y, -z/y )如果 |z| 最大: z > 0: 使用 +Z 面,坐标 ( x/z, -y/z ) z < 0: 使用 -Z 面,坐标 ( -x/z, -y/z )性能优化建议预过滤环境贴图:为不同粗糙度预计算模糊的立方体贴图使用 mipmap 级别存储不同粗糙度的反射立方体贴图压缩:使用压缩纹理格式减少内存占用DXT、ETC、PVRTC 等格式动态环境贴图优化:降低分辨率(如 128x128)减少更新频率(如每 10 帧更新一次)只更新可见物体的反射LOD 系统:远处物体使用低分辨率立方体贴图近处物体使用高分辨率立方体贴图常见问题接缝问题立方体贴图面与面之间可能出现接缝。解决方案:// 使用 CLAMP_TO_EDGE 环绕模式gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE);图片方向问题不同面的图片可能需要翻转。解决方案:使用图像编辑软件预先调整或在着色器中翻转纹理坐标// 翻转 Y 坐标vec2 flippedCoord = vec2(texCoord.x, 1.0 - texCoord.y);
阅读 0·3月7日 12:04

WebGL 中的缓冲区(Buffer)是什么?如何使用 VBO 和 VAO?

WebGL 缓冲区概述缓冲区(Buffer)是 GPU 内存中的一块区域,用于存储顶点数据、索引数据等图形渲染所需的信息。使用缓冲区可以高效地将数据从 CPU 传输到 GPU。缓冲区类型1. 顶点缓冲区对象(VBO - Vertex Buffer Object)存储顶点属性数据,如位置、颜色、法线、纹理坐标等。2. 索引缓冲区对象(IBO/EBO - Index/Element Buffer Object)存储顶点索引,用于定义图元的连接方式,减少重复顶点数据。3. 顶点数组对象(VAO - Vertex Array Object)存储顶点属性配置的状态,简化绘制调用前的设置。VBO(顶点缓冲区对象)详解创建和使用 VBO// 1. 创建缓冲区const vbo = gl.createBuffer();// 2. 绑定缓冲区(指定当前操作的缓冲区类型)gl.bindBuffer(gl.ARRAY_BUFFER, vbo);// 3. 上传数据到 GPUconst vertices = new Float32Array([ // 位置(x, y, z) 颜色(r, g, b) -0.5, -0.5, 0.0, 1.0, 0.0, 0.0, // 顶点1 0.5, -0.5, 0.0, 0.0, 1.0, 0.0, // 顶点2 0.0, 0.5, 0.0, 0.0, 0.0, 1.0 // 顶点3]);gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);// 4. 配置顶点属性指针gl.vertexAttribPointer( 0, // 属性位置(对应着色器中的 layout(location=0)) 3, // 每个属性的分量数(这里是3:x, y, z) gl.FLOAT, // 数据类型 false, // 是否归一化 6 * 4, // 步长(每个顶点的字节数:6个float * 4字节) 0 // 偏移量);gl.enableVertexAttribArray(0); // 启用位置属性// 配置颜色属性gl.vertexAttribPointer( 1, // 属性位置 3, // 分量数(r, g, b) gl.FLOAT, false, 6 * 4, // 步长 3 * 4 // 偏移量(跳过位置数据));gl.enableVertexAttribArray(1); // 启用颜色属性缓冲区使用模式// gl.STATIC_DRAW:数据不经常改变,多次绘制 gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);// gl.DYNAMIC_DRAW:数据经常改变,多次绘制gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);// gl.STREAM_DRAW:数据每次绘制都改变gl.bufferData(gl.ARRAY_BUFFER, data, gl.STREAM_DRAW);IBO/EBO(索引缓冲区对象)详解创建和使用 IBO// 顶点数据(4个顶点定义一个四边形)const vertices = new Float32Array([ -0.5, 0.5, 0.0, // 左上 (0) -0.5, -0.5, 0.0, // 左下 (1) 0.5, -0.5, 0.0, // 右下 (2) 0.5, 0.5, 0.0 // 右上 (3)]);// 索引数据(2个三角形 = 6个索引)const indices = new Uint16Array([ 0, 1, 2, // 第一个三角形 0, 2, 3 // 第二个三角形]);// 创建并绑定 VBOgl.bindBuffer(gl.ARRAY_BUFFER, vbo);gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);// 创建并绑定 IBOconst ibo = gl.createBuffer();gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);// 绘制(使用索引绘制)gl.drawElements( gl.TRIANGLES, // 绘制模式 6, // 索引数量 gl.UNSIGNED_SHORT, // 索引数据类型 0 // 偏移量);VAO(顶点数组对象)详解VAO 存储了所有顶点属性的配置状态,包括:启用的顶点属性每个属性的配置(大小、类型、步长、偏移)绑定的 VBOWebGL 2.0 / WebGL 1.0 + OESvertexarray_object 扩展// 创建 VAOconst vao = gl.createVertexArray(); // WebGL 2.0// const vao = ext.createVertexArrayOES(); // 使用扩展// 绑定 VAO(后续配置将存储在 VAO 中)gl.bindVertexArray(vao);// 配置顶点属性(这些配置会被 VAO 记录)gl.bindBuffer(gl.ARRAY_BUFFER, positionVBO);gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(0);gl.bindBuffer(gl.ARRAY_BUFFER, colorVBO);gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(1);gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);// 解绑 VAOgl.bindVertexArray(null);// 绘制时只需绑定 VAOgl.bindVertexArray(vao);gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);完整示例代码class Mesh { constructor(gl) { this.gl = gl; this.vertexCount = 0; this.indexCount = 0; // WebGL 2.0 创建 VAO this.vao = gl.createVertexArray(); this.vbo = gl.createBuffer(); this.ibo = gl.createBuffer(); } setVertices(vertices, attributes) { const gl = this.gl; gl.bindVertexArray(this.vao); // 上传顶点数据 gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 配置属性 let offset = 0; const stride = attributes.reduce((sum, attr) => sum + attr.size, 0) * 4; attributes.forEach((attr, index) => { gl.vertexAttribPointer( index, attr.size, gl.FLOAT, false, stride, offset ); gl.enableVertexAttribArray(index); offset += attr.size * 4; }); this.vertexCount = vertices.length / (stride / 4); } setIndices(indices) { const gl = this.gl; gl.bindVertexArray(this.vao); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); this.indexCount = indices.length; } draw() { const gl = this.gl; gl.bindVertexArray(this.vao); if (this.indexCount > 0) { gl.drawElements(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_SHORT, 0); } else { gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount); } }}// 使用示例const mesh = new Mesh(gl);mesh.setVertices( new Float32Array([/* 顶点数据 */]), [ { size: 3 }, // 位置 { size: 3 }, // 颜色 { size: 2 } // 纹理坐标 ]);mesh.setIndices(new Uint16Array([/* 索引数据 */]));mesh.draw();性能优化建议减少状态切换:使用 VAO 减少绘制前的配置开销合并缓冲区:将多个小网格合并到一个大缓冲区使用索引绘制:减少顶点数据重复选择合适的绘制模式:STATIC_DRAW:静态几何体DYNAMIC_DRAW:频繁更新的数据STREAM_DRAW:每帧都更新的数据批量绘制:使用实例化渲染(Instanced Rendering)绘制多个相同对象
阅读 0·3月7日 12:04

WebGL 中的雾效(Fog)是如何实现的?

WebGL 雾效概述雾效(Fog)是一种模拟大气散射效果的渲染技术,使远处的物体逐渐融入到背景颜色中。雾效不仅能增加场景的真实感,还能隐藏远处的裁剪边界,优化性能。雾效的基本原理雾效的核心思想是根据物体与相机的距离,在物体颜色和雾颜色之间进行线性或指数插值:最终颜色 = 物体颜色 × (1 - 雾因子) + 雾颜色 × 雾因子雾的类型1. 线性雾(Linear Fog)雾的浓度随距离线性增加。公式:fogFactor = (end - distance) / (end - start)start:雾开始距离end:雾完全覆盖距离distance:片段到相机的距离// 顶点着色器varying float v_fogDepth;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); vec4 viewPos = u_viewMatrix * worldPos; // 计算视图空间深度(正值) v_fogDepth = -viewPos.z; gl_Position = u_projectionMatrix * viewPos;}// 片段着色器uniform vec3 u_fogColor;uniform float u_fogStart;uniform float u_fogEnd;varying float v_fogDepth;void main() { vec4 objectColor = texture2D(u_texture, v_texCoord); // 计算线性雾因子 float fogFactor = (u_fogEnd - v_fogDepth) / (u_fogEnd - u_fogStart); fogFactor = clamp(fogFactor, 0.0, 1.0); // 混合物体颜色和雾颜色 vec3 finalColor = mix(u_fogColor, objectColor.rgb, fogFactor); gl_FragColor = vec4(finalColor, objectColor.a);}2. 指数雾(Exponential Fog)雾的浓度随距离指数增加,效果更自然。公式:fogFactor = exp(-density × distance)uniform vec3 u_fogColor;uniform float u_fogDensity;varying float v_fogDepth;void main() { vec4 objectColor = texture2D(u_texture, v_texCoord); // 计算指数雾因子 float fogFactor = exp(-u_fogDensity * v_fogDepth); fogFactor = clamp(fogFactor, 0.0, 1.0); vec3 finalColor = mix(u_fogColor, objectColor.rgb, fogFactor); gl_FragColor = vec4(finalColor, objectColor.a);}3. 指数平方雾(Exp2 Fog)雾的浓度随距离平方指数增加,效果更加柔和。公式:fogFactor = exp(-(density × distance)²)void main() { vec4 objectColor = texture2D(u_texture, v_texCoord); // 计算指数平方雾因子 float fogFactor = exp(-pow(u_fogDensity * v_fogDepth, 2.0)); fogFactor = clamp(fogFactor, 0.0, 1.0); vec3 finalColor = mix(u_fogColor, objectColor.rgb, fogFactor); gl_FragColor = vec4(finalColor, objectColor.a);}完整的雾效实现顶点着色器attribute vec3 a_position;attribute vec2 a_texCoord;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;varying vec2 v_texCoord;varying float v_fogDepth;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); vec4 viewPos = u_viewMatrix * worldPos; v_texCoord = a_texCoord; v_fogDepth = length(viewPos.xyz); // 使用实际距离而非仅 Z 值 gl_Position = u_projectionMatrix * viewPos;}片段着色器precision mediump float;uniform sampler2D u_texture;uniform vec3 u_fogColor;uniform float u_fogDensity;uniform int u_fogType; // 0: 线性, 1: 指数, 2: 指数平方uniform float u_fogStart;uniform float u_fogEnd;varying vec2 v_texCoord;varying float v_fogDepth;float calculateFogFactor() { float fogFactor = 0.0; if (u_fogType == 0) { // 线性雾 fogFactor = (u_fogEnd - v_fogDepth) / (u_fogEnd - u_fogStart); } else if (u_fogType == 1) { // 指数雾 fogFactor = exp(-u_fogDensity * v_fogDepth); } else if (u_fogType == 2) { // 指数平方雾 fogFactor = exp(-pow(u_fogDensity * v_fogDepth, 2.0)); } return clamp(fogFactor, 0.0, 1.0);}void main() { vec4 objectColor = texture2D(u_texture, v_texCoord); float fogFactor = calculateFogFactor(); // mix(fogColor, objectColor, fogFactor) vec3 finalColor = mix(u_fogColor, objectColor.rgb, fogFactor); gl_FragColor = vec4(finalColor, objectColor.a);}JavaScript 控制class Fog { constructor(gl, program) { this.gl = gl; this.program = program; // 获取 uniform 位置 this.fogColorLoc = gl.getUniformLocation(program, 'u_fogColor'); this.fogDensityLoc = gl.getUniformLocation(program, 'u_fogDensity'); this.fogTypeLoc = gl.getUniformLocation(program, 'u_fogType'); this.fogStartLoc = gl.getUniformLocation(program, 'u_fogStart'); this.fogEndLoc = gl.getUniformLocation(program, 'u_fogEnd'); // 默认设置 this.color = [0.7, 0.8, 0.9]; // 淡蓝色雾 this.density = 0.02; this.type = 2; // 指数平方雾 this.start = 10.0; this.end = 50.0; } apply() { const gl = this.gl; gl.uniform3fv(this.fogColorLoc, this.color); gl.uniform1f(this.fogDensityLoc, this.density); gl.uniform1i(this.fogTypeLoc, this.type); gl.uniform1f(this.fogStartLoc, this.start); gl.uniform1f(this.fogEndLoc, this.end); } // 设置雾的类型 setLinear(start, end) { this.type = 0; this.start = start; this.end = end; } setExponential(density) { this.type = 1; this.density = density; } setExponentialSquared(density) { this.type = 2; this.density = density; }}// 使用示例const fog = new Fog(gl, program);fog.setExponentialSquared(0.015);fog.apply();高度雾(Height Fog)模拟根据高度变化的雾效,如山谷中的雾。// 顶点着色器varying vec3 v_worldPos;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); v_worldPos = worldPos.xyz; gl_Position = u_projectionMatrix * u_viewMatrix * worldPos;}// 片段着色器uniform float u_fogHeight;uniform float u_fogHeightFalloff;varying vec3 v_worldPos;float calculateHeightFog() { // 基于高度的雾密度 float heightFactor = (u_fogHeight - v_worldPos.y) * u_fogHeightFalloff; heightFactor = clamp(heightFactor, 0.0, 1.0); // 结合距离雾 float distanceFactor = exp(-u_fogDensity * length(v_worldPos - u_cameraPos)); return distanceFactor * (1.0 - heightFactor);}雾效与光照结合void main() { // 计算光照 vec3 ambient = ...; vec3 diffuse = ...; vec3 specular = ...; vec3 lighting = ambient + diffuse + specular; vec4 texColor = texture2D(u_texture, v_texCoord); vec3 objectColor = lighting * texColor.rgb; // 应用雾效 float fogFactor = calculateFogFactor(); vec3 finalColor = mix(u_fogColor, objectColor, fogFactor); gl_FragColor = vec4(finalColor, texColor.a);}雾效的性能优化1. 顶点级雾效计算当顶点数较少时,可以在顶点着色器中计算雾因子:// 顶点着色器varying float v_fogFactor;void main() { // ... 计算位置 float fogDepth = length(viewPos.xyz); v_fogFactor = exp(-u_fogDensity * fogDepth); v_fogFactor = clamp(v_fogFactor, 0.0, 1.0);}// 片段着色器varying float v_fogFactor;void main() { vec3 finalColor = mix(u_fogColor, objectColor, v_fogFactor);}2. 使用深度纹理在后处理阶段应用雾效:// 1. 渲染场景到颜色纹理和深度纹理renderSceneToTextures();// 2. 后处理阶段应用雾效applyFogPostProcess();// 后处理雾效着色器uniform sampler2D u_colorTexture;uniform sampler2D u_depthTexture;void main() { vec3 color = texture2D(u_colorTexture, v_texCoord).rgb; float depth = texture2D(u_depthTexture, v_texCoord).r; // 将深度转换为世界空间距离 float linearDepth = linearizeDepth(depth); // 计算雾因子 float fogFactor = exp(-u_fogDensity * linearDepth); vec3 finalColor = mix(u_fogColor, color, fogFactor); gl_FragColor = vec4(finalColor, 1.0);}不同类型的雾效对比| 雾类型 | 公式 | 特点 ||--------|------|------|| 线性雾 | (end - d) / (end - start) | 简单,雾边界明显 || 指数雾 | exp(-density × d) | 自然,适合大多数场景 || 指数平方雾 | exp(-(density × d)²) | 更柔和,雾边界不明显 || 高度雾 | 结合 Y 轴 | 适合山谷、水面等场景 |实际应用建议选择合适的雾类型:大多数场景使用指数或指数平方雾需要精确控制雾边界时使用线性雾调整雾的颜色:通常与天空盒或背景色一致可以随时间变化模拟昼夜效果性能考虑:移动端建议使用顶点级雾效复杂场景可以使用后处理雾效与其他效果结合:雾效可以与体积光、大气散射结合注意雾效对透明物体的影响
阅读 0·3月7日 12:04

区块链中使用了哪些密码学技术?详解哈希函数、数字签名和 Merkle Tree

密码学(Cryptography) 是区块链安全性的基石,通过数学算法保证数据的机密性、完整性、真实性和不可抵赖性。1. 哈希函数(Hash Function)定义与特性哈希函数是将任意长度输入转换为固定长度输出的单向函数。关键特性:| 特性 | 说明 | 区块链应用 || -------- | ---------------- | ----- || 确定性 | 相同输入总是产生相同输出 | 数据验证 || 单向性 | 无法从哈希值反推原始数据 | 保护隐私 || 抗碰撞性 | 难以找到两个不同输入产生相同输出 | 防篡改 || 雪崩效应 | 输入微小变化导致输出巨大差异 | 检测篡改 |常用哈希算法SHA-256(比特币使用):输入:"Hello Blockchain"输出:a8f5f167f44f4964e6c998dee827110c9a0c5e1e7a5b6e5f8d9c2b1a4e7f3d6(固定 256 位 / 64 个十六进制字符)Keccak-256(以太坊使用):SHA-3 标准的变种抗长度扩展攻击用于生成以太坊地址区块链中的应用区块结构中的哈希应用:┌─────────────────────────────────────┐│ 区块头 (Block Header) │├─────────────────────────────────────┤│ 前一区块哈希 (Previous Hash) │ ← 链接区块,形成链条│ Merkle Root │ ← 交易数据完整性验证│ 时间戳 (Timestamp) ││ Nonce │└─────────────────────────────────────┘区块哈希 = SHA256(SHA256(区块头))2. 数字签名(Digital Signature)非对称加密基础密钥对生成:┌──────────────┐ ┌──────────────┐│ 私钥 (SK) │ ←─────→ │ 公钥 (PK) ││ (保密) │ 数学关联 │ (公开) │└──────────────┘ └──────────────┘ ↓ ↓ 用于签名 用于验证常用算法:ECDSA(Elliptic Curve Digital Signature Algorithm):比特币、以太坊使用EdDSA(Edwards-curve Digital Signature Algorithm):更快更安全签名与验证流程交易签名过程:1. 交易数据准备 {"from": "0xabc...", "to": "0xdef...", "value": 100}2. 计算交易哈希 txHash = Keccak256(交易数据)3. 使用私钥签名 signature = ECDSA_Sign(私钥, txHash)4. 广播交易 交易数据 + 签名 + 公钥验证过程:ECDSA_Verify(公钥, txHash, signature) → true/false以太坊地址生成1. 生成私钥(256 位随机数) 私钥 = 随机数2. 通过椭圆曲线计算公钥 公钥 = 私钥 × G(曲线基点)3. 计算公钥哈希 hash = Keccak256(公钥)4. 取后 20 字节作为地址 地址 = "0x" + hash[12:32]3. Merkle Tree(默克尔树)结构与原理Merkle Tree 结构: Root Hash / \ Hash 1-2 Hash 3-4 / \ / \ Hash 1 Hash 2 Hash 3 Hash 4 | | | | Tx 1 Tx 2 Tx 3 Tx 4(每个 Hash = SHA256(子节点哈希拼接))"默克尔证明(Merkle Proof)轻节点验证交易:验证 Tx 3 是否存在于区块中:需要数据:- Tx 3 的哈希- Hash 4(兄弟节点)- Hash 1-2(叔节点)- Root Hash(区块头中)验证步骤:1. Hash 3 = SHA256(Tx 3)2. Hash 3-4 = SHA256(Hash 3 + Hash 4)3. 计算 Root = SHA256(Hash 1-2 + Hash 3-4)4. 比较计算的 Root 与区块头中的 Root区块链中的应用交易完整性验证:快速验证大量交易轻节点同步:SPV(简单支付验证)只需下载区块头状态证明:以太坊的状态树使用 Merkle Patricia Tree4. 零知识证明(Zero-Knowledge Proof)基本概念定义:证明者向验证者证明某个陈述为真,而不透露任何有用信息。zk-SNARKs(以太坊 ZK Rollup 使用):Succinct:证明体积小Non-interactive:无需交互ARgument of Knowledge:知识论证应用场景隐私交易示例:┌─────────────────────────────────────┐│ 证明:我知道一个秘密 x,使得 ││ hash(x) = 0xabc... ││ ││ 不透露:x 的具体值 │└─────────────────────────────────────┘面试要点理解哈希函数的单向性和抗碰撞性掌握公钥私钥的关系和数字签名原理能够解释 Merkle Tree 如何高效验证数据了解零知识证明的基本概念和应用知道不同区块链使用的具体算法(SHA-256 vs Keccak-256)
阅读 0·3月7日 12:04