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

面试题手册

WebGL 性能优化有哪些常用技巧?

WebGL 性能优化概述WebGL 性能优化是 3D Web 应用开发的关键。由于 JavaScript 和 GPU 之间的通信开销,以及移动设备的资源限制,合理的优化策略能显著提升渲染性能。1. 减少绘制调用(Draw Calls)问题每次 gl.drawArrays 或 gl.drawElements 都有 CPU 到 GPU 的通信开销。优化方案批量绘制(Batching)// 优化前:多次绘制调用for (let mesh of meshes) { gl.bindBuffer(gl.ARRAY_BUFFER, mesh.vbo); gl.drawArrays(gl.TRIANGLES, 0, mesh.vertexCount);}// 优化后:合并到一个缓冲区const mergedBuffer = mergeMeshes(meshes);gl.bindBuffer(gl.ARRAY_BUFFER, mergedBuffer);gl.drawArrays(gl.TRIANGLES, 0, totalVertexCount);实例化渲染(Instanced Rendering)// WebGL 2.0 原生支持// 一次绘制调用渲染多个相同几何体const instanceCount = 1000;gl.drawArraysInstanced(gl.TRIANGLES, 0, vertexCount, instanceCount);2. 减少状态切换问题频繁切换着色器程序、纹理、缓冲区等状态会造成性能开销。优化方案按状态排序// 按着色器程序排序meshes.sort((a, b) => a.program.id - b.program.id);// 按纹理排序meshes.sort((a, b) => a.texture.id - b.texture.id);let currentProgram = null;let currentTexture = null;for (let mesh of meshes) { // 只在需要时切换程序 if (mesh.program !== currentProgram) { gl.useProgram(mesh.program); currentProgram = mesh.program; } // 只在需要时切换纹理 if (mesh.texture !== currentTexture) { gl.bindTexture(gl.TEXTURE_2D, mesh.texture); currentTexture = mesh.texture; } mesh.draw();}使用纹理图集(Texture Atlas)// 将多个小纹理合并为一个大纹理// 减少纹理绑定切换次数const atlasTexture = createTextureAtlas([ 'texture1.png', 'texture2.png', 'texture3.png']);// 在着色器中使用纹理坐标偏移uniform vec2 u_atlasOffset;uniform vec2 u_atlasScale;vec2 atlasCoord = a_texCoord * u_atlasScale + u_atlasOffset;vec4 color = texture2D(u_texture, atlasCoord);3. 优化着色器顶点着色器优化// 优化前:在顶点着色器中进行复杂计算attribute vec3 a_position;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;void main() { // 每次顶点都进行矩阵乘法 mat4 mvp = u_projectionMatrix * u_viewMatrix * u_modelMatrix; gl_Position = mvp * vec4(a_position, 1.0);}// 优化后:在 CPU 预计算 MVP 矩阵uniform mat4 u_mvpMatrix;void main() { gl_Position = u_mvpMatrix * vec4(a_position, 1.0);}片段着色器优化// 优化前:复杂的逐像素计算void main() { vec3 lightDir = normalize(u_lightPos - v_worldPos); float diff = max(dot(v_normal, lightDir), 0.0); vec3 diffuse = diff * u_lightColor; // ... 更多计算}// 优化后:顶点着色器计算光照// 顶点着色器varying vec3 v_lightIntensity;void main() { // 在顶点级别计算光照 vec3 lightDir = normalize(u_lightPos - worldPos); float diff = max(dot(normal, lightDir), 0.0); v_lightIntensity = diff * u_lightColor;}// 片段着色器void main() { // 使用插值后的光照强度 gl_FragColor = vec4(v_lightIntensity * textureColor, 1.0);}使用适当精度// 高精度(highp)- 顶点位置、变换矩阵attribute highp vec3 a_position;uniform highp mat4 u_mvpMatrix;// 中精度(mediump)- 颜色、纹理坐标attribute mediump vec2 a_texCoord;varying mediump vec2 v_texCoord;// 低精度(lowp)- 光照计算结果varying lowp vec3 v_lightColor;4. 缓冲区优化使用 VAO 减少状态设置// WebGL 2.0 或扩展支持const vao = gl.createVertexArray();gl.bindVertexArray(vao);// 配置顶点属性(只执行一次)gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(0);gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(1);gl.bindVertexArray(null);// 绘制时只需绑定 VAOgl.bindVertexArray(vao);gl.drawArrays(gl.TRIANGLES, 0, count);使用索引绘制// 优化前:36 个顶点定义一个立方体const vertices = new Float32Array([ // 每个面 6 个顶点,共 6 个面 // 大量重复顶点数据]);// 优化后:8 个顶点 + 36 个索引const vertices = new Float32Array([ // 8 个唯一顶点]);const indices = new Uint16Array([ // 36 个索引定义 12 个三角形]);gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);使用 Interleaved Arrays// 优化前:分离的缓冲区const positions = new Float32Array([/* ... */]);const colors = new Float32Array([/* ... */]);const texCoords = new Float32Array([/* ... */]);// 优化后:交错的顶点数据const vertices = new Float32Array([ // x, y, z, r, g, b, u, v 0, 0, 0, 1, 0, 0, 0, 0, // 顶点 1 1, 0, 0, 0, 1, 0, 1, 0, // 顶点 2 // ...]);// 更好的缓存局部性5. 纹理优化纹理压缩// 使用压缩纹理格式const compressedExtension = gl.getExtension('WEBGL_compressed_texture_s3tc');// 上传压缩纹理数据gl.compressedTexImage2D( gl.TEXTURE_2D, 0, compressedExtension.COMPRESSED_RGBA_S3TC_DXT5_EXT, width, height, 0, compressedData);Mipmap 使用// 启用 mipmap 提高渲染质量和性能gl.generateMipmap(gl.TEXTURE_2D);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);纹理尺寸优化使用 2 的幂次尺寸(支持 mipmap)避免过大的纹理(内存和带宽开销)根据距离使用不同分辨率的纹理(LOD)6. 遮挡剔除和视锥剔除视锥剔除function isInFrustum(boundingBox, viewProjectionMatrix) { // 将包围盒转换到裁剪空间 // 检查是否在视锥体内 const corners = boundingBox.getCorners(); for (let corner of corners) { const clipPos = transformPoint(corner, viewProjectionMatrix); if (Math.abs(clipPos.x) <= clipPos.w && Math.abs(clipPos.y) <= clipPos.w && 0 <= clipPos.z && clipPos.z <= clipPos.w) { return true; } } return false;}// 只渲染在视锥体内的物体for (let object of scene.objects) { if (isInFrustum(object.boundingBox, vpMatrix)) { object.render(); }}遮挡查询(WebGL 2.0)const query = gl.createQuery();// 开始遮挡查询gl.beginQuery(gl.ANY_SAMPLES_PASSED, query);drawBoundingBox(object);gl.endQuery(gl.ANY_SAMPLES_PASSED);// 检查结果const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE);if (available) { const visible = gl.getQueryParameter(query, gl.QUERY_RESULT) > 0; if (visible) { drawDetailedMesh(object); }}7. 帧缓冲区优化减少分辨率// 在高 DPI 屏幕上使用适当分辨率const dpr = Math.min(window.devicePixelRatio, 2);canvas.width = canvas.clientWidth * dpr;canvas.height = canvas.clientHeight * dpr;延迟渲染优化// G-Buffer 优化:使用适当精度// 位置:RGB16F 或 RGBA16F// 法线:RGB10_A2 或 RGBA8// 材质:RGBA88. JavaScript 优化避免垃圾回收// 优化前:每帧创建新数组function update() { const matrix = new Float32Array(16); // 创建垃圾 // ...}// 优化后:重用数组const matrix = new Float32Array(16);function update() { // 重用 matrix,不创建新对象 mat4.identity(matrix); // ...}使用 TypedArrays// 使用 Float32Array 而不是普通数组const positions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]);gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);9. 移动端优化减少过度绘制// 从前到后绘制不透明物体gl.enable(gl.DEPTH_TEST);gl.depthFunc(gl.LEQUAL);opaqueObjects.sort((a, b) => b.distance - a.distance);for (let obj of opaqueObjects) { obj.draw();}使用适当精度// 移动端使用 mediump 优化性能precision mediump float;避免复杂着色器减少纹理采样次数避免动态分支简化光照计算10. 性能监控// 使用 EXT_disjoint_timer_query 测量 GPU 时间const ext = gl.getExtension('EXT_disjoint_timer_query');const query = ext.createQueryEXT();ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, query);drawScene();ext.endQueryEXT(ext.TIME_ELAPSED_EXT);// 获取结果const available = ext.getQueryObjectEXT(query, ext.QUERY_RESULT_AVAILABLE_EXT);if (available) { const timeElapsed = ext.getQueryObjectEXT(query, ext.QUERY_RESULT_EXT); console.log(`GPU time: ${timeElapsed / 1000000} ms`);}总结| 优化方向 | 主要技巧 ||----------|----------|| 绘制调用 | 批量绘制、实例化渲染 || 状态切换 | 状态排序、纹理图集、VAO || 着色器 | 预计算、适当精度、简化计算 || 缓冲区 | 索引绘制、交错数组 || 纹理 | 压缩、mipmap、合理尺寸 || 剔除 | 视锥剔除、遮挡查询 || JavaScript | 避免 GC、TypedArrays |
阅读 0·3月6日 21:57

WebGL 1.0 和 WebGL 2.0 有什么区别?

WebGL 版本概述WebGL 1.0 于 2011 年发布,基于 OpenGL ES 2.0。WebGL 2.0 于 2017 年发布,基于 OpenGL ES 3.0,带来了大量新功能和性能改进。主要区别对比| 特性 | WebGL 1.0 | WebGL 2.0 ||------|-----------|-----------|| 基础规范 | OpenGL ES 2.0 | OpenGL ES 3.0 || 发布年份 | 2011 | 2017 || 着色器版本 | GLSL ES 1.0 | GLSL ES 3.0 || 3D 纹理 | 需要扩展 | 原生支持 || 多重渲染目标(MRT) | 需要扩展 | 原生支持 || 实例化渲染 | 需要扩展 | 原生支持 || 变换反馈 | 不支持 | 支持 || 采样器对象 | 不支持 | 支持 || 顶点数组对象(VAO) | 需要扩展 | 原生支持 || 非2的幂次纹理 | 有限制 | 完全支持 |着色器语言差异WebGL 1.0 (GLSL ES 1.0)// 顶点着色器attribute vec3 a_position;attribute vec2 a_texCoord;uniform mat4 u_mvpMatrix;uniform sampler2D u_texture;varying vec2 v_texCoord;void main() { gl_Position = u_mvpMatrix * vec4(a_position, 1.0); v_texCoord = a_texCoord;}// 片段着色器precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture;void main() { gl_FragColor = texture2D(u_texture, v_texCoord);}WebGL 2.0 (GLSL ES 3.0)#version 300 es// 顶点着色器in vec3 a_position; // attribute → inin vec2 a_texCoord;uniform mat4 u_mvpMatrix;out vec2 v_texCoord; // varying → outvoid main() { gl_Position = u_mvpMatrix * vec4(a_position, 1.0); v_texCoord = a_texCoord;}// 片段着色器#version 300 esprecision mediump float;in vec2 v_texCoord; // varying → inuniform sampler2D u_texture;out vec4 fragColor; // gl_FragColor → out 变量void main() { fragColor = texture(u_texture, v_texCoord); // texture2D → texture}着色器语法变化| WebGL 1.0 | WebGL 2.0 | 说明 ||-----------|-----------|------|| attribute | in | 顶点输入 || varying | in/out | 顶点/片段间传递数据 || gl_FragColor | out 变量 | 片段着色器输出 || texture2D() | texture() | 2D 纹理采样 || textureCube() | texture() | 立方体纹理采样 || - | #version 300 es | 版本声明(必需) |WebGL 2.0 新增功能详解1. 3D 纹理// WebGL 2.0 原生支持 3D 纹理const texture3D = gl.createTexture();gl.bindTexture(gl.TEXTURE_3D, texture3D);gl.texImage3D( gl.TEXTURE_3D, // 目标 0, // 级别 gl.RGBA, // 内部格式 width, height, depth, // 深度 0, // 边框 gl.RGBA, gl.UNSIGNED_BYTE, data);#version 300 esuniform sampler3D u_volumeTexture;void main() { vec4 color = texture(u_volumeTexture, vec3(x, y, z));}2. 多重渲染目标(MRT)// 创建帧缓冲区,绑定多个颜色附件const framebuffer = gl.createFramebuffer();gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);// 附加多个纹理const textures = [];for (let i = 0; i < 4; i++) { const texture = gl.createTexture(); // ... 配置纹理 gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, // COLOR_ATTACHMENT0, COLOR_ATTACHMENT1, ... gl.TEXTURE_2D, texture, 0 ); textures.push(texture);}// 指定绘制到哪些附件gl.drawBuffers([ gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2, gl.COLOR_ATTACHMENT3]);#version 300 eslayout(location = 0) out vec4 color0;layout(location = 1) out vec4 color1;layout(location = 2) out vec4 color2;layout(location = 3) out vec4 color3;void main() { color0 = vec4(1.0, 0.0, 0.0, 1.0); color1 = vec4(0.0, 1.0, 0.0, 1.0); color2 = vec4(0.0, 0.0, 1.0, 1.0); color3 = vec4(1.0, 1.0, 1.0, 1.0);}3. 实例化渲染(Instanced Rendering)// WebGL 2.0 原生支持// 绘制 1000 个实例,每个实例使用不同的变换const instanceCount = 1000;gl.drawArraysInstanced(gl.TRIANGLES, 0, vertexCount, instanceCount);// 或使用索引绘制gl.drawElementsInstanced(gl.TRIANGLES, indexCount, gl.UNSIGNED_SHORT, 0, instanceCount);#version 300 esin vec3 a_position;in mat4 a_instanceMatrix; // 实例化矩阵void main() { gl_Position = u_projectionMatrix * u_viewMatrix * a_instanceMatrix * vec4(a_position, 1.0);}4. 变换反馈(Transform Feedback)// 创建变换反馈对象const transformFeedback = gl.createTransformFeedback();// 设置顶点着色器输出const vertexShaderSource = `#version 300 esin vec3 a_position;out vec3 v_newPosition; // 变换后的位置void main() { v_newPosition = a_position * 2.0; // 某种变换}`;// 配置变换反馈const program = gl.createProgram();// ... 编译链接着色器gl.transformFeedbackVaryings( program, ['v_newPosition'], // 要捕获的输出变量 gl.SEPARATE_ATTRIBS // 或 INTERLEAVED_ATTRIBS);// 执行变换反馈gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedback);gl.beginTransformFeedback(gl.POINTS);gl.drawArrays(gl.POINTS, 0, count);gl.endTransformFeedback();5. 采样器对象(Sampler Objects)// 将纹理参数从纹理对象分离const sampler = gl.createSampler();// 配置采样器参数gl.samplerParameteri(sampler, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);gl.samplerParameteri(sampler, gl.TEXTURE_MAG_FILTER, gl.LINEAR);gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);// 绑定采样器到纹理单元gl.bindSampler(0, sampler); // 绑定到纹理单元 06. 顶点数组对象(VAO)原生支持// WebGL 2.0 原生支持,无需扩展const vao = gl.createVertexArray();gl.bindVertexArray(vao);// 配置顶点属性(存储在 VAO 中)gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);gl.enableVertexAttribArray(0);gl.bindVertexArray(null);// 绘制时只需绑定 VAOgl.bindVertexArray(vao);gl.drawArrays(gl.TRIANGLES, 0, count);新纹理功能非2的幂次纹理完整支持// WebGL 2.0 中,非2的幂次纹理支持所有功能gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);gl.generateMipmap(gl.TEXTURE_2D); // 也支持 mipmap纹理数组const textureArray = gl.createTexture();gl.bindTexture(gl.TEXTURE_2D_ARRAY, textureArray);gl.texImage3D( gl.TEXTURE_2D_ARRAY, 0, gl.RGBA, width, height, layerCount, // 层数 0, gl.RGBA, gl.UNSIGNED_BYTE, data);如何检测 WebGL 2.0 支持function getWebGLContext(canvas) { // 优先尝试 WebGL 2.0 let gl = canvas.getContext('webgl2'); if (gl) { console.log('Using WebGL 2.0'); return gl; } // 回退到 WebGL 1.0 gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); if (gl) { console.log('Using WebGL 1.0'); return gl; } console.error('WebGL not supported'); return null;}浏览器支持情况| 浏览器 | WebGL 1.0 | WebGL 2.0 ||--------|-----------|-----------|| Chrome | ✓ | ✓ (56+) || Firefox | ✓ | ✓ (51+) || Safari | ✓ | ✓ (15+) || Edge | ✓ | ✓ (79+) || IE 11 | ✓ | ✗ |迁移建议渐进增强:先检测 WebGL 2.0 支持,不支持时回退到 1.0着色器版本:为两个版本准备不同的着色器代码功能检测:使用特定功能前检查是否可用性能考虑:WebGL 2.0 功能更强大,但 1.0 兼容性更好
阅读 0·3月6日 21:57

WebGL 中的着色器(Shader)是什么?顶点着色器和片段着色器有什么区别?

WebGL 着色器概述着色器(Shader)是在 GPU 上运行的小程序,用于处理图形渲染的各个阶段。WebGL 使用 GLSL(OpenGL Shading Language)编写着色器代码。着色器的类型WebGL 主要有两种着色器:顶点着色器(Vertex Shader)片段着色器(Fragment Shader)顶点着色器(Vertex Shader)功能处理每个顶点的数据负责坐标变换将 3D 坐标转换为裁剪空间坐标输入attribute:顶点属性(位置、颜色、法线、纹理坐标等)uniform:统一变量(变换矩阵、光照参数等)输出gl_Position:裁剪空间中的顶点位置(必需)varying:传递给片段着色器的插值数据代码示例// 顶点着色器attribute vec3 a_position;attribute vec3 a_color;attribute vec2 a_texCoord;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;varying vec3 v_color;varying vec2 v_texCoord;void main() { // 坐标变换:模型空间 → 世界空间 → 相机空间 → 裁剪空间 mat4 mvp = u_projectionMatrix * u_viewMatrix * u_modelMatrix; gl_Position = mvp * vec4(a_position, 1.0); // 传递数据给片段着色器 v_color = a_color; v_texCoord = a_texCoord;}片段着色器(Fragment Shader)功能处理每个片段(潜在的像素)计算最终像素颜色执行纹理采样、光照计算等输入varying:从顶点着色器插值而来的数据uniform:统一变量(纹理、材质参数等)输出gl_FragColor:最终像素颜色(WebGL 1.0)out vec4 fragColor:输出变量(WebGL 2.0)代码示例// 片段着色器precision mediump float;varying vec3 v_color;varying vec2 v_texCoord;uniform sampler2D u_texture;uniform float u_opacity;void main() { // 纹理采样 vec4 texColor = texture2D(u_texture, v_texCoord); // 混合顶点颜色和纹理颜色 vec3 finalColor = v_color * texColor.rgb; // 设置最终颜色 gl_FragColor = vec4(finalColor, texColor.a * u_opacity);}顶点着色器 vs 片段着色器对比| 特性 | 顶点着色器 | 片段着色器 ||------|------------|------------|| 执行频率 | 每个顶点执行一次 | 每个片段执行一次 || 主要任务 | 坐标变换 | 颜色计算 || 必需输出 | gl_Position | gl_FragColor || 典型操作 | 矩阵乘法、顶点光照 | 纹理采样、像素级光照 || 性能影响 | 顶点数量少时影响小 | 通常对性能影响更大 || 精度要求 | 通常使用 highp | 常用 mediump 优化性能 |着色器变量类型attribute(顶点着色器专用)attribute vec3 a_position; // 顶点位置attribute vec2 a_texCoord; // 纹理坐标attribute vec3 a_normal; // 法线向量uniform(两种着色器都可用)uniform mat4 u_matrix; // 变换矩阵uniform sampler2D u_texture; // 纹理采样器uniform vec3 u_lightColor; // 光照颜色varying(顶点→片段传递数据)// 顶点着色器varying vec2 v_texCoord;// 片段着色器varying vec2 v_texCoord; // 同名变量接收插值数据着色器编译和链接流程// 1. 创建着色器对象const vertexShader = gl.createShader(gl.VERTEX_SHADER);const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);// 2. 设置源码 gl.shaderSource(vertexShader, vertexSource);gl.shaderSource(fragmentShader, fragmentSource);// 3. 编译着色器gl.compileShader(vertexShader);gl.compileShader(fragmentShader);// 4. 创建程序对象const program = gl.createProgram();// 5. 附加着色器gl.attachShader(program, vertexShader);gl.attachShader(program, fragmentShader);// 6. 链接着色器程序gl.linkProgram(program);// 7. 使用程序gl.useProgram(program);性能优化建议减少 varying 变量数量:减少顶点与片段着色器间的数据传输在顶点着色器中计算:尽可能在顶点级别而非片段级别计算使用适当精度:移动端使用 mediump 或 lowp避免分支语句:GPU 不擅长处理条件分支预计算常量:将 uniforms 而非反复计算
阅读 0·3月6日 21:57

WebGL 中的纹理(Texture)如何使用?有哪些纹理参数需要配置?

WebGL 纹理概述纹理(Texture)是 WebGL 中用于给 3D 物体添加表面细节的图片。通过纹理映射,可以将 2D 图像应用到 3D 几何体表面,使渲染结果更加真实。纹理使用流程1. 创建纹理对象const texture = gl.createTexture();2. 绑定纹理// 绑定到 2D 纹理目标gl.bindTexture(gl.TEXTURE_2D, texture);3. 上传纹理数据// 方法1:从 Image 对象加载gl.texImage2D( gl.TEXTURE_2D, // 目标 0, // 细节级别(mipmap 级别) gl.RGBA, // 内部格式 gl.RGBA, // 源格式 gl.UNSIGNED_BYTE, // 数据类型 image // Image 对象);// 方法2:直接上传像素数据gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, width, // 宽度 height, // 高度 0, // 边框(必须为0) gl.RGBA, gl.UNSIGNED_BYTE, pixels // Uint8Array 像素数据);4. 配置纹理参数// 纹理环绕方式gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);// 纹理过滤方式gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);5. 生成 Mipmap(可选)gl.generateMipmap(gl.TEXTURE_2D);纹理参数详解纹理环绕方式(Texture Wrapping)控制当纹理坐标超出 [0, 1] 范围时的行为:| 参数值 | 说明 ||--------|------|| gl.REPEAT | 重复纹理(默认) || gl.CLAMP_TO_EDGE | 边缘像素延伸 || gl.MIRRORED_REPEAT | 镜像重复 |// S 轴(水平方向)gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);// T 轴(垂直方向)gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);可视化效果:REPEAT: CLAMP_TO_EDGE: MIRRORED_REPEAT:|ABCD|ABCD| |AAAA|ABCD|DDDD| |ABCD|DCBA|ABCD||ABCD|ABCD| |AAAA|ABCD|DDDD| |ABCD|DCBA|ABCD|纹理过滤方式(Texture Filtering)控制纹理采样时的插值方式:放大过滤(MAG_FILTER)当纹理被放大时(纹理像素 < 屏幕像素):| 参数值 | 说明 ||--------|------|| gl.NEAREST | 最近邻采样,像素化效果 || gl.LINEAR | 双线性插值,平滑效果(推荐) |缩小过滤(MIN_FILTER)当纹理被缩小时(纹理像素 > 屏幕像素):| 参数值 | 说明 ||--------|------|| gl.NEAREST | 最近邻采样 || gl.LINEAR | 双线性插值 || gl.NEAREST_MIPMAP_NEAREST | 最近 mipmap + 最近采样 || gl.LINEAR_MIPMAP_NEAREST | 最近 mipmap + 线性插值 || gl.NEAREST_MIPMAP_LINEAR | mipmap 间线性 + 最近采样 || gl.LINEAR_MIPMAP_LINEAR | 三线性过滤(质量最高) |// 放大时使用线性过滤gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);// 缩小时使用 mipmap 三线性过滤gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);完整纹理加载示例function loadTexture(gl, url) { const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); // 设置临时像素(在图片加载完成前使用) const level = 0; const internalFormat = gl.RGBA; const width = 1; const height = 1; const border = 0; const srcFormat = gl.RGBA; const srcType = gl.UNSIGNED_BYTE; const pixel = new Uint8Array([0, 0, 255, 255]); // 蓝色 gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, width, height, border, srcFormat, srcType, pixel); const image = new Image(); image.onload = function() { gl.bindTexture(gl.TEXTURE_D, texture); gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, srcFormat, srcType, image); // 检查图片尺寸是否为 2 的幂次 if (isPowerOf2(image.width) && isPowerOf2(image.height)) { // 是 2 的幂次,可以使用 mipmap gl.generateMipmap(gl.TEXTURE_2D); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); } else { // 不是 2 的幂次,关闭 mipmap,设置环绕方式为 CLAMP_TO_EDGE gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); } gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); }; image.src = url; return texture;}function isPowerOf2(value) { return (value & (value - 1)) == 0;}在着色器中使用纹理顶点着色器attribute vec3 a_position;attribute vec2 a_texCoord;uniform mat4 u_mvpMatrix;varying vec2 v_texCoord;void main() { gl_Position = u_mvpMatrix * vec4(a_position, 1.0); v_texCoord = a_texCoord; // 传递纹理坐标到片段着色器}片段着色器precision mediump float;varying vec2 v_texCoord;uniform sampler2D u_texture; // 纹理采样器void main() { gl_FragColor = texture2D(u_texture, v_texCoord);}JavaScript 代码// 激活纹理单元gl.activeTexture(gl.TEXTURE0);// 绑定纹理gl.bindTexture(gl.TEXTURE_2D, texture);// 设置 uniform 变量(告诉着色器使用哪个纹理单元)gl.uniform1i(textureLocation, 0); // 使用 TEXTURE0多纹理绑定// 加载多个纹理const texture1 = loadTexture(gl, 'texture1.jpg');const texture2 = loadTexture(gl, 'texture2.png');// 在绘制时绑定到不同纹理单元gl.activeTexture(gl.TEXTURE0);gl.bindTexture(gl.TEXTURE_2D, texture1);gl.uniform1i(texture1Location, 0);gl.activeTexture(gl.TEXTURE1);gl.bindTexture(gl.TEXTURE_2D, texture2);gl.uniform1i(texture2Location, 1);// 片段着色器中使用多个纹理uniform sampler2D u_texture1;uniform sampler2D u_texture2;varying vec2 v_texCoord;void main() { vec4 color1 = texture2D(u_texture1, v_texCoord); vec4 color2 = texture2D(u_texture2, v_texCoord); gl_FragColor = mix(color1, color2, 0.5); // 混合两个纹理}纹理坐标系统(0, 1) -------- (1, 1) | | | 纹理图像 | | |(0, 0) -------- (1, 0)注意:WebGL 纹理坐标系原点在左下角,而大多数图像格式原点在左上角。性能优化建议使用纹理图集(Texture Atlas):将多个小纹理合并为一个大纹理,减少绑定切换合理选择纹理尺寸:优先使用 2 的幂次尺寸(支持 mipmap)不要使用过大的纹理(内存和带宽开销)使用压缩纹理格式:如 DXT、ETC、PVRTC启用 mipmap:提高渲染质量和性能复用纹理:避免重复加载相同纹理
阅读 0·3月6日 21:57

WebGL 中的阴影(Shadow)是如何实现的?

WebGL 阴影概述阴影是 3D 渲染中增加真实感的重要技术。WebGL 中实现阴影的主要方法是阴影贴图(Shadow Mapping),它是一种基于图像的阴影技术。阴影贴图原理阴影贴图的核心思想:第一步:从光源视角渲染场景,生成深度贴图(Depth Map)第二步:从相机视角渲染场景,将每个片段转换到光源空间,比较深度值判断:如果片段在光源空间的深度大于深度贴图中的值,则该片段在阴影中光源视角渲染 → 深度贴图 ↓相机视角渲染 → 光源空间坐标 → 深度比较 → 阴影判断阴影贴图实现步骤第一步:生成深度贴图顶点着色器(深度生成)// shadow_vertex.glslattribute vec3 a_position;uniform mat4 u_lightSpaceMatrix; // 光源投影矩阵 × 光源视图矩阵 × 模型矩阵void main() { gl_Position = u_lightSpaceMatrix * vec4(a_position, 1.0);}片段着色器(深度生成)// shadow_fragment.glsl// WebGL 1.0 - 需要手动写入深度值precision mediump float;varying vec4 v_position;void main() { // 手动计算深度值并编码到颜色 float depth = v_position.z / v_position.w; depth = depth * 0.5 + 0.5; // 转换到 [0, 1] // 使用 RGBA 编码深度值(提高精度) vec4 encodedDepth; encodedDepth.r = depth; encodedDepth.g = fract(depth * 256.0); encodedDepth.b = fract(depth * 65536.0); encodedDepth.a = fract(depth * 16777216.0); gl_FragColor = encodedDepth;}// WebGL 2.0 - 可以直接使用深度附件// 片段着色器可以为空,深度自动写入JavaScript 代码// 创建深度贴图帧缓冲区function createDepthFramebuffer(gl, width, height) { const framebuffer = gl.createFramebuffer(); // 创建深度纹理 const depthTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, depthTexture); gl.texImage2D( gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, // WebGL 2.0 // gl.DEPTH_COMPONENT16, // WebGL 1.0 width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null ); // 设置纹理参数 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // 绑定到帧缓冲区 gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0 ); // WebGL 1.0 需要颜色附件,即使不写入颜色 if (!isWebGL2) { const colorBuffer = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, colorBuffer); gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA4, width, height); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, colorBuffer ); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); return { framebuffer, depthTexture };}// 渲染深度贴图function renderDepthMap(gl, shadowProgram, scene, lightSpaceMatrix) { // 绑定阴影帧缓冲区 gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFramebuffer); gl.viewport(0, 0, shadowMapSize, shadowMapSize); // 清除深度缓冲区 gl.clear(gl.DEPTH_BUFFER_BIT); // 使用深度着色器 gl.useProgram(shadowProgram); // 渲染场景中的每个物体 for (let object of scene.objects) { // 计算光源空间矩阵 const modelMatrix = object.getModelMatrix(); const lightSpaceMatrix = mat4.create(); mat4.multiply(lightSpaceMatrix, lightProjectionMatrix, lightViewMatrix); mat4.multiply(lightSpaceMatrix, lightSpaceMatrix, modelMatrix); gl.uniformMatrix4fv( gl.getUniformLocation(shadowProgram, 'u_lightSpaceMatrix'), false, lightSpaceMatrix ); object.draw(gl); } // 解绑 gl.bindFramebuffer(gl.FRAMEBUFFER, null);}第二步:渲染带阴影的场景顶点着色器attribute vec3 a_position;attribute vec3 a_normal;attribute vec2 a_texCoord;uniform mat4 u_modelMatrix;uniform mat4 u_viewMatrix;uniform mat4 u_projectionMatrix;uniform mat4 u_lightSpaceMatrix;varying vec3 v_worldPos;varying vec3 v_normal;varying vec2 v_texCoord;varying vec4 v_lightSpacePos;void main() { vec4 worldPos = u_modelMatrix * vec4(a_position, 1.0); v_worldPos = worldPos.xyz; v_normal = mat3(u_modelMatrix) * a_normal; v_texCoord = a_texCoord; // 转换到光源空间 v_lightSpacePos = u_lightSpaceMatrix * vec4(a_position, 1.0); gl_Position = u_projectionMatrix * u_viewMatrix * worldPos;}片段着色器precision mediump float;varying vec3 v_worldPos;varying vec3 v_normal;varying vec2 v_texCoord;varying vec4 v_lightSpacePos;uniform sampler2D u_shadowMap;uniform vec3 u_lightPos;uniform vec3 u_viewPos;uniform vec3 u_lightColor;// 解码深度值(WebGL 1.0)float decodeDepth(vec4 rgba) { const vec4 bitShift = vec4(1.0, 1.0/256.0, 1.0/65536.0, 1.0/16777216.0); return dot(rgba, bitShift);}// 计算阴影float calculateShadow(vec4 lightSpacePos) { // 透视除法 vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; // 转换到 [0, 1] 范围 projCoords = projCoords * 0.5 + 0.5; // 获取当前片段在光源视角的深度 float currentDepth = projCoords.z; // 获取深度贴图中的深度值 float closestDepth = texture2D(u_shadowMap, projCoords.xy).r; // 阴影偏移(解决阴影痤疮问题) float bias = 0.005; // 比较深度 float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0; // 检查是否在阴影贴图范围外 if (projCoords.z > 1.0) { shadow = 0.0; } return shadow;}// PCF 软阴影float calculateShadowPCF(vec4 lightSpacePos) { vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; projCoords = projCoords * 0.5 + 0.5; float currentDepth = projCoords.z; float bias = 0.005; float shadow = 0.0; // 3x3 PCF 采样 vec2 texelSize = 1.0 / vec2(1024.0, 1024.0); for (int x = -1; x <= 1; ++x) { for (int y = -1; y <= 1; ++y) { float pcfDepth = texture2D(u_shadowMap, projCoords.xy + vec2(x, y) * texelSize).r; shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0; } } shadow /= 9.0; if (projCoords.z > 1.0) { shadow = 0.0; } return shadow;}void main() { vec3 color = texture2D(u_texture, v_texCoord).rgb; // 环境光 vec3 ambient = 0.3 * color; // 漫反射 vec3 lightDir = normalize(u_lightPos - v_worldPos); vec3 normal = normalize(v_normal); float diff = max(dot(normal, lightDir), 0.0); vec3 diffuse = diff * u_lightColor; // 计算阴影 float shadow = calculateShadowPCF(v_lightSpacePos); // 组合光照(阴影区域只保留环境光) vec3 lighting = ambient + (1.0 - shadow) * (diffuse); gl_FragColor = vec4(lighting * color, 1.0);}阴影常见问题及解决方案1. 阴影痤疮(Shadow Acne)问题:表面出现条纹状自阴影伪影原因:深度精度限制和浮点数误差解决方案:// 添加阴影偏移float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;2. 彼得潘效应(Peter Panning)问题:阴影与物体分离,看起来像在漂浮原因:偏移值过大解决方案:只在渲染深度贴图时启用正面剔除使用较小的偏移值// 渲染深度贴图时启用正面剔除gl.enable(gl.CULL_FACE);gl.cullFace(gl.FRONT); // 剔除正面,只渲染背面// 渲染正常场景时恢复gl.cullFace(gl.BACK);3. 阴影锯齿问题:阴影边缘出现锯齿解决方案:使用 PCF(Percentage Closer Filtering)软阴影增加阴影贴图分辨率使用级联阴影贴图(CSM)PCF 软阴影实现float calculateShadowPCF(vec4 lightSpacePos, int sampleRadius) { vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; projCoords = projCoords * 0.5 + 0.5; float currentDepth = projCoords.z; float bias = 0.005; float shadow = 0.0; vec2 texelSize = 1.0 / vec2(textureSize(u_shadowMap, 0)); // 采样周围像素 for (int x = -sampleRadius; x <= sampleRadius; ++x) { for (int y = -sampleRadius; y <= sampleRadius; ++y) { vec2 offset = vec2(float(x), float(y)) * texelSize; float closestDepth = texture(u_shadowMap, projCoords.xy + offset).r; shadow += currentDepth - bias > closestDepth ? 1.0 : 0.0; } } int sampleCount = (sampleRadius * 2 + 1) * (sampleRadius * 2 + 1); shadow /= float(sampleCount); return shadow;}// Poisson Disk 采样(更自然的软阴影)vec2 poissonDisk[16] = vec2[]( vec2(-0.94201624, -0.39906216), vec2(0.94558609, -0.76890725), // ... 更多采样点);float calculateShadowPoisson(vec4 lightSpacePos) { vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; projCoords = projCoords * 0.5 + 0.5; float currentDepth = projCoords.z; float bias = 0.005; float shadow = 0.0; for (int i = 0; i < 16; i++) { float closestDepth = texture(u_shadowMap, projCoords.xy + poissonDisk[i] / 700.0).r; shadow += currentDepth - bias > closestDepth ? 1.0 : 0.0; } return shadow / 16.0;}级联阴影贴图(Cascaded Shadow Maps)对于大场景,使用多个不同分辨率的阴影贴图:// 创建多个级联const cascades = [ { near: 0.1, far: 10, resolution: 2048 }, { near: 10, far: 50, resolution: 1024 }, { near: 50, far: 200, resolution: 512 }];// 根据距离选择合适的级联function getCascadeIndex(viewSpaceZ) { for (let i = 0; i < cascades.length; i++) { if (viewSpaceZ < cascades[i].far) { return i; } } return cascades.length - 1;}点光源阴影(立方体贴图阴影)// 创建立方体贴图const depthCubemap = gl.createTexture();gl.bindTexture(gl.TEXTURE_CUBE_MAP, depthCubemap);for (let i = 0; i < 6; i++) { gl.texImage2D( gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, gl.DEPTH_COMPONENT, shadowSize, shadowSize, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null );}// 渲染 6 个面const shadowTransforms = [ // +X, -X, +Y, -Y, +Z, -Z 方向的投影矩阵];性能优化建议阴影贴图分辨率:移动端:512x512 或 1024x1024桌面端:2048x2048 或更高更新频率:静态光源可以只生成一次阴影贴图动态光源每帧更新阴影距离:限制阴影渲染距离使用级联阴影贴图软阴影性能:PCF 3x3 是性能和质量的平衡Poisson Disk 采样质量更好但性能开销大
阅读 0·3月6日 21:57

Service Worker 中的 Workbox 是什么?它提供了哪些功能?

Workbox 详解Workbox 是 Google 开发的一套 JavaScript 库,用于简化 Service Worker 的开发,提供了一系列强大的缓存策略和工具。Workbox 简介什么是 WorkboxWorkbox 是一套模块化的库,帮助开发者:简化 Service Worker 编写提供预置的缓存策略自动生成 Service Worker提供开发调试工具# 安装 Workboxnpm install workbox-sw workbox-cli --save-dev# 或使用 CDNimportScripts('https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-sw.js');核心模块| 模块 | 功能 ||------|------|| workbox-routing | 请求路由匹配 || workbox-strategies | 缓存策略 || workbox-precaching | 预缓存 || workbox-cacheable-response | 缓存响应管理 || workbox-expiration | 缓存过期控制 || workbox-background-sync | 后台同步 || workbox-google-analytics | Google Analytics 离线支持 |基础使用1. 使用 CDN 引入// sw.jsimportScripts('https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-sw.js');// Workbox 会自动加载核心模块workbox.setConfig({ debug: true // 开发模式});// 使用路由和策略workbox.routing.registerRoute( ({ request }) => request.destination === 'image', new workbox.strategies.CacheFirst());2. 使用 npm 和构建工具// sw.jsimport { precacheAndRoute } from 'workbox-precaching';import { registerRoute } from 'workbox-routing';import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';import { ExpirationPlugin } from 'workbox-expiration';// 预缓存precacheAndRoute(self.__WB_MANIFEST);// 图片使用 Cache First 策略registerRoute( ({ request }) => request.destination === 'image', new CacheFirst({ cacheName: 'images', plugins: [ new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 // 30天 }) ] }));核心功能详解1. 路由(Routing)import { registerRoute } from 'workbox-routing';// 方式1:字符串匹配registerRoute( '/api/users', new NetworkFirst());// 方式2:正则匹配registerRoute( new RegExp('/api/.*'), new NetworkFirst());// 方式3:回调函数匹配registerRoute( ({ url, request, event }) => { return url.pathname.startsWith('/api/') && request.method === 'GET'; }, new NetworkFirst());// 方式4:使用 Route 类import { Route } from 'workbox-routing';const apiRoute = new Route( ({ url }) => url.pathname.startsWith('/api/'), new NetworkFirst());registerRoute(apiRoute);2. 缓存策略(Strategies)import { CacheFirst, NetworkFirst, StaleWhileRevalidate, NetworkOnly, CacheOnly } from 'workbox-strategies';// Cache First - 缓存优先registerRoute( ({ request }) => request.destination === 'style' || request.destination === 'script', new CacheFirst({ cacheName: 'static-resources' }));// Network First - 网络优先registerRoute( ({ url }) => url.pathname.startsWith('/api/'), new NetworkFirst({ cacheName: 'api-cache', plugins: [ new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 5 * 60 // 5分钟 }) ] }));// Stale While Revalidate - 过期时重新验证registerRoute( ({ request }) => request.destination === 'image', new StaleWhileRevalidate({ cacheName: 'images', plugins: [ new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 }) ] }));3. 预缓存(Precaching)import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';// 方式1:手动指定资源precacheAndRoute([ { url: '/index.html', revision: '1.0.0' }, { url: '/styles.css', revision: '1.0.0' }, { url: '/app.js', revision: '1.0.0' }]);// 方式2:使用构建工具生成的清单// webpack 配置const { InjectManifest } = require('workbox-webpack-plugin');module.exports = { plugins: [ new InjectManifest({ swSrc: './src/sw.js', swDest: 'sw.js' }) ]};// sw.js 中使用precacheAndRoute(self.__WB_MANIFEST);// 清理过期缓存cleanupOutdatedCaches();4. 缓存过期控制import { ExpirationPlugin } from 'workbox-expiration';registerRoute( ({ request }) => request.destination === 'image', new CacheFirst({ cacheName: 'images', plugins: [ new ExpirationPlugin({ maxEntries: 50, // 最大条目数 maxAgeSeconds: 30 * 24 * 60 * 60, // 最大存活时间(30天) purgeOnQuotaError: true // 存储不足时自动清理 }) ] }));5. 缓存响应管理import { CacheableResponsePlugin } from 'workbox-cacheable-response';registerRoute( ({ url }) => url.pathname.startsWith('/api/'), new NetworkFirst({ cacheName: 'api-cache', plugins: [ new CacheableResponsePlugin({ statuses: [0, 200], // 只缓存状态码 0 和 200 headers: { 'X-Cache-Allowed': 'true' // 只缓存包含特定头的响应 } }) ] }));6. 后台同步import { BackgroundSyncPlugin } from 'workbox-background-sync';// 创建后台同步队列const bgSyncPlugin = new BackgroundSyncPlugin('api-queue', { maxRetentionTime: 24 * 60 // 24小时});registerRoute( ({ url }) => url.pathname.startsWith('/api/'), new NetworkFirst({ plugins: [bgSyncPlugin] }), 'POST');7. 范围请求支持import { RangeRequestsPlugin } from 'workbox-range-requests';registerRoute( ({ url }) => url.pathname.endsWith('.mp4'), new CacheFirst({ cacheName: 'videos', plugins: [ new RangeRequestsPlugin() // 支持视频范围请求 ] }));高级用法自定义策略import { Strategy } from 'workbox-strategies';class CustomStrategy extends Strategy { async _handle(request, handler) { // 尝试从缓存获取 const cachedResponse = await handler.cacheMatch(request); if (cachedResponse) { // 在后台更新缓存 handler.fetchAndCachePut(request).catch(() => {}); return cachedResponse; } // 缓存未命中,请求网络 return handler.fetchAndCachePut(request); }}registerRoute( '/custom-route', new CustomStrategy());插件开发// 自定义日志插件const loggingPlugin = { cacheWillUpdate: async ({ request, response }) => { console.log(`缓存更新: ${request.url}`); return response; }, cacheDidUpdate: async ({ request, cacheName }) => { console.log(`缓存已更新: ${request.url} in ${cacheName}`); }, fetchDidFail: async ({ request, error }) => { console.error(`请求失败: ${request.url}`, error); }};registerRoute( '/api/', new NetworkFirst({ plugins: [loggingPlugin] }));运行时缓存import { warmStrategyCache } from 'workbox-recipes';// 预热缓存warmStrategyCache({ urls: ['/about', '/contact'], strategy: new StaleWhileRevalidate({ cacheName: 'pages' })});// 离线回退import { offlineFallback } from 'workbox-recipes';offlineFallback({ pageFallback: '/offline.html', imageFallback: '/images/offline.png', fontFallback: false});配置选项全局配置import { setConfig } from 'workbox-core';setConfig({ debug: process.env.NODE_ENV === 'development', logger: customLogger});日志级别import { setLogLevel, LOG_LEVELS } from 'workbox-core';setLogLevel(LOG_LEVELS.debug); // debug, warn, error, silent与构建工具集成Webpack// webpack.config.jsconst { GenerateSW, InjectManifest } = require('workbox-webpack-plugin');module.exports = { plugins: [ // 自动生成 Service Worker new GenerateSW({ clientsClaim: true, skipWaiting: true, runtimeCaching: [ { urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/, handler: 'CacheFirst', options: { cacheName: 'images', expiration: { maxEntries: 60 } } } ] }) // 或注入到现有 Service Worker // new InjectManifest({ // swSrc: './src/sw.js', // swDest: 'sw.js' // }) ]};Vite// vite.config.jsimport { VitePWA } from 'vite-plugin-pwa';export default { plugins: [ VitePWA({ workbox: { runtimeCaching: [ { urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/, handler: 'CacheFirst', options: { cacheName: 'images' } } ] } }) ]};Workbox vs 原生 Service Worker| 特性 | Workbox | 原生 Service Worker ||------|---------|---------------------|| 学习曲线 | 低 | 高 || 代码量 | 少 | 多 || 缓存策略 | 预置多种 | 需手动实现 || 调试工具 | 完善 | 基础 || 灵活性 | 高 | 极高 || 包大小 | 增加约 20-50KB | 无额外开销 |最佳实践选择合适的策略:根据资源类型选择最佳缓存策略设置缓存限制:使用 ExpirationPlugin 防止存储溢出版本控制:使用预缓存时正确设置 revision错误处理:添加适当的错误处理和降级方案性能优化:避免缓存过多数据,定期清理
阅读 0·3月6日 21:56

什么是 axios 以及它与原生 fetch API 相比有哪些优势?

什么是 axiosAxios 是一个基于 Promise 的 HTTP 客户端,可以用在浏览器和 Node.js 环境中。它提供了简洁的 API 来处理 HTTP 请求和响应。Axios 相比 Fetch API 的优势1. 自动转换 JSON 数据// Axios 自动处理 JSONconst response = await axios.get('/api/users');console.log(response.data); // 已经是 JavaScript 对象// Fetch 需要手动转换const response = await fetch('/api/users');const data = await response.json(); // 需要额外调用2. 请求/响应拦截器// 添加请求拦截器axios.interceptors.request.use(config => { config.headers.Authorization = `Bearer ${token}`; return config;});// 添加响应拦截器axios.interceptors.response.use( response => response, error => { if (error.response.status === 401) { // 处理未授权 } return Promise.reject(error); });3. 请求取消功能const controller = new AbortController();axios.get('/api/data', { signal: controller.signal});// 取消请求controller.abort();4. 更好的错误处理Axios 在 HTTP 错误状态码(4xx, 5xx)时会自动 reject Promise提供了更详细的错误信息(response, request, config 等)5. 请求超时设置axios.get('/api/data', { timeout: 5000 });6. 支持请求进度监控axios.post('/api/upload', formData, { onUploadProgress: (progressEvent) => { const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); }});7. 浏览器和 Node.js 兼容浏览器端基于 XMLHttpRequestNode.js 端基于 http 模块8. 支持 CSRF 保护axios.defaults.xsrfCookieName = 'csrftoken';axios.defaults.xsrfHeaderName = 'X-CSRFToken';总结| 特性 | Axios | Fetch API ||------|-------|-----------|| JSON 自动转换 | ✅ | ❌ || 拦截器 | ✅ | ❌ || 请求取消 | ✅ | ✅ (AbortController) || 超时设置 | ✅ | ❌ || 进度监控 | ✅ | ❌ || 浏览器兼容 | IE11+ | 现代浏览器 || 体积 | ~13KB | 原生支持 |Axios 适合需要复杂 HTTP 处理的场景,而 Fetch API 适合简单的请求场景。
阅读 0·3月6日 21:56

什么是 DApp?请解释去中心化应用与传统应用的区别及开发架构

什么是 DApp?DApp(Decentralized Application,去中心化应用) 是运行在区块链网络上的应用程序,其后端代码运行在分布式节点上,而非中心化服务器。DApp 的核心特征根据以太坊官方定义,一个真正的 DApp 必须满足以下条件:| 特征 | 说明 | 重要性 ||------|------|--------|| 开源 | 代码公开透明,任何人可审查 | ⭐⭐⭐⭐⭐ || 去中心化 | 数据存储在区块链上,无单一控制点 | ⭐⭐⭐⭐⭐ || 激励机制 | 使用代币激励网络参与者 | ⭐⭐⭐⭐ || 共识机制 | 通过算法达成数据一致性 | ⭐⭐⭐⭐⭐ |DApp 与传统应用对比传统应用架构:┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ 前端 │ ←──→ │ 后端服务器 │ ←──→ │ 中心化数据库 ││ (Web/App) │ │ (API) │ │ (MySQL等) │└─────────────┘ └─────────────┘ └─────────────┘ ↑ 单一控制点,可被关闭DApp 架构:┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ 前端 │ ←──→ │ 智能合约 │ ←──→ │ 区块链网络 ││ (Web/App) │ │ (Solidity) │ │ (分布式节点) │└─────────────┘ └─────────────┘ └─────────────┘ ↑ 不可篡改,永久运行详细对比表| 维度 | 传统应用 | DApp ||------|---------|------|| 数据存储 | 中心化服务器 | 区块链分布式存储 || 控制权 | 公司/组织控制 | 社区治理,无单一控制 || 停机风险 | 服务器故障可停机 | 只要网络存在就运行 || 数据修改 | 管理员可修改 | 不可篡改 || 用户隐私 | 需信任第三方 | 自主控制数据 || 审查抗性 | 容易被审查封禁 | 抗审查 || 交易速度 | 快(毫秒级) | 慢(秒级到分钟级) || 用户体验 | 流畅 | 需支付 Gas 费,门槛高 || 开发成本 | 相对较低 | 智能合约审计成本高 |DApp 开发架构三层架构模型┌─────────────────────────────────────────┐│ 前端层 (Frontend) ││ • React/Vue/Next.js ││ • Web3.js / Ethers.js ││ • 钱包连接(MetaMask/WalletConnect) │└─────────────────────────────────────────┘ ↓┌─────────────────────────────────────────┐│ 交互层 (Interaction) ││ • 智能合约 ABI ││ • RPC 节点(Infura/Alchemy) ││ • IPFS(去中心化存储) │└─────────────────────────────────────────┘ ↓┌─────────────────────────────────────────┐│ 数据层 (Data Layer) ││ • 智能合约(Solidity/Rust) ││ • 区块链网络(Ethereum/Polygon) ││ • The Graph(索引协议) │└─────────────────────────────────────────┘核心技术栈前端开发:// Web3.js 连接钱包示例import Web3 from 'web3';const connectWallet = async () => { if (window.ethereum) { const web3 = new Web3(window.ethereum); await window.ethereum.request({ method: 'eth_requestAccounts' }); const accounts = await web3.eth.getAccounts(); return accounts[0]; }};// 调用智能合约const contract = new web3.eth.Contract(ABI, contractAddress);const result = await contract.methods.getBalance().call();智能合约开发:// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract MyDApp { mapping(address => uint256) public balances; event Deposit(address indexed user, uint256 amount); function deposit() public payable { balances[msg.sender] += msg.value; emit Deposit(msg.sender, msg.value); } function getBalance() public view returns (uint256) { return balances[msg.sender]; }}数据索引(The Graph):# subgraph/schema.graphqltype Deposit @entity { id: ID! user: Bytes! amount: BigInt! timestamp: BigInt!}DApp 开发流程1. 需求分析 ↓2. 智能合约设计 • 编写 Solidity 代码 • 本地测试(Hardhat/Truffle) ↓3. 安全审计 • 静态分析(Slither) • 人工审计 ↓4. 合约部署 • 测试网验证 • 主网部署 ↓5. 前端开发 • UI/UX 设计 • Web3 集成 ↓6. 测试上线 • 功能测试 • 安全测试主流 DApp 类型| 类型 | 代表应用 | 特点 ||------|---------|------|| DeFi | Uniswap、Aave | 去中心化金融协议 || NFT | OpenSea、Blur | 非同质化代币交易 || GameFi | Axie Infinity、StepN | 游戏+金融结合 || SocialFi | Lens Protocol | 去中心化社交 || DAO | Aragon、Snapshot | 去中心化自治组织 |面试要点理解 DApp 与传统应用的本质区别掌握 Web3.js/Ethers.js 的基本使用了解智能合约与前端交互的方式熟悉主流钱包连接方案知道 DApp 开发的安全注意事项了解 The Graph 等索引方案的作用
阅读 0·3月6日 21:56