面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

前端阅读 05月28日 05:32

如何在 JavaScript 中操作 SVG?核心方法与常见坑

用 JavaScript 操控 SVG,本质就是操作 DOM——只不过多了个命名空间的坑。SVG 元素挂在 DOM 树上,所以 querySelector、addEventListener 这些老朋友都能用,但创建元素时必须用 createElementNS,这是新手最容易栽的地方。本文覆盖 SVG 元素选择、属性修改、事件绑定、动画实现、坐标换算、拖拽交互这些核心操作,顺带聊几个实际开发中踩过的坑。命名空间:第一个坑HTML 元素用 document.createElement('div') 就行,SVG 不行——你必须指定命名空间:const SVG_NS = 'http://www.w3.org/2000/svg';const circle = document.createElementNS(SVG_NS, 'circle');忘掉 NS 后缀会怎样?浏览器不会报错,但创建出来的元素不属于 SVG 命名空间,渲染不出来,调试半天才发现是这个原因。这类 bug 特征是:元素确实被插入了 DOM,但页面上什么都看不到。另一个容易忽略的是 xlink:href 属性。SVG 的 <use>、<image> 等元素引用外部资源时用的是 xlink:href,它有自己的命名空间:const XLINK_NS = 'http://www.w3.org/1999/xlink';useEl.setAttributeNS(XLINK_NS, 'xlink:href', '#icon');新规范中 href 已经可以直接用 setAttribute 设置,但兼容旧浏览器时还是得走 xlink。选择元素:和 HTML 一样SVG 元素的选择没有特殊之处,标准 DOM API 直接用:const circle = document.getElementById('myCircle');const allCircles = document.querySelectorAll('svg circle');const filledElements = document.querySelectorAll('[fill="red"]');需要注意的是,如果你用 <img> 标签引入 SVG,JavaScript 是无法访问内部元素的。必须用内联 SVG(直接写在 HTML 中)或 <object> / <iframe> 加载,才能用 JS 操作。修改属性和样式SVG 元素的属性分为两类:呈现属性(如 fill、stroke、r)和 样式属性。两者都可以改,但走不同的路:// 方式一:setAttribute 修改呈现属性circle.setAttribute('fill', 'red');circle.setAttribute('r', '60');// 方式二:style 对象修改样式circle.style.fill = 'green';circle.style.opacity = '0.5';一个常见的困惑是:setAttribute('fill', 'red') 和 style.fill = 'red' 有什么区别?CSS 样式的优先级高于呈现属性,所以 style.fill 会覆盖 setAttribute('fill', ...)。这和 CSS 层叠规则一致。带连字符的属性(如 stroke-width、font-size)不能用点语法赋值,circle.stroke-width = 4 会报错。必须用 setAttribute 或驼峰写法 style.strokeWidth。事件绑定SVG 元素天然支持 DOM 事件,点击、悬停、拖拽都能绑定。唯一要注意的是键盘事件——SVG 元素默认不可聚焦,需要手动加 tabindex:circle.setAttribute('tabindex', '0');circle.addEventListener('keydown', (e) => { if (e.key === 'Enter') { circle.setAttribute('fill', 'red'); }});对于大量同类元素(比如数据可视化中的几十个柱子),逐个绑定事件很浪费内存,用事件委托更合理:svg.addEventListener('click', (e) => { const bar = e.target.closest('rect.bar'); if (bar) { highlightBar(bar); }});动画:CSS 过渡 vs requestAnimationFrame简单动画用 CSS 过渡就够了,改个属性值浏览器自动补间:circle.style.transition = 'all 0.3s ease';circle.setAttribute('r', '80');需要精确控制的动画(比如沿路径运动、物理模拟)则要用 requestAnimationFrame。一个容易犯的错是用 setInterval——它不跟浏览器刷新率同步,动画会卡顿。requestAnimationFrame 的回调在浏览器下一次重绘前执行,能保证流畅:let angle = 0;function animate() { angle += 0.02; const x = 150 + Math.cos(angle) * 100; const y = 150 + Math.sin(angle) * 100; circle.setAttribute('cx', x); circle.setAttribute('cy', y); requestAnimationFrame(animate);}requestAnimationFrame(animate);性能优化有一条核心原则:尽量动画 transform 和 opacity,别动布局属性。transform: translate() 走 GPU 合成,不触发重排;改 cx、cy 则会触发重排。当元素数量多时差距明显。获取鼠标在 SVG 中的坐标鼠标的 clientX/clientY 是页面坐标,要换算成 SVG 内部坐标需要做矩阵变换:function getSVGPoint(svg, event) { const pt = svg.createSVGPoint(); pt.x = event.clientX; pt.y = event.clientY; return pt.matrixTransform(svg.getScreenCTM().inverse());}svg.addEventListener('click', (e) => { const { x, y } = getSVGPoint(svg, e); console.log(`SVG 坐标: ${x}, ${y}`);});getScreenCTM() 返回 SVG 坐标系到屏幕坐标系的变换矩阵,.inverse() 取逆矩阵,就能从屏幕坐标映射回 SVG 坐标。如果 SVG 做过 viewBox 缩放或 transform,这一步是必须的。拖拽实现拖拽是把鼠标坐标换算和事件监听组合起来的典型场景:let dragging = null;let offset = { x: 0, y: 0 };function getMousePos(svg, e) { const CTM = svg.getScreenCTM(); return { x: (e.clientX - CTM.e) / CTM.a, y: (e.clientY - CTM.f) / CTM.d };}svg.addEventListener('mousedown', (e) => { dragging = e.target; const pos = getMousePos(svg, e); offset.x = pos.x - parseFloat(dragging.getAttribute('cx')); offset.y = pos.y - parseFloat(dragging.getAttribute('cy'));});svg.addEventListener('mousemove', (e) => { if (!dragging) return; const pos = getMousePos(svg, e); dragging.setAttribute('cx', pos.x - offset.x); dragging.setAttribute('cy', pos.y - offset.y);});svg.addEventListener('mouseup', () => { dragging = null; });触摸设备上要把 mousedown/mousemove/mouseup 换成 touchstart/touchmove/touchend,或者用 Pointer Events 统一处理。什么时候该用 SVG,什么时候该用 Canvas?这不是本文主题,但做 SVG 开发迟早会遇到这个问题,简单说下判断依据:用 SVG:需要交互(每个元素可点击/悬停)、需要无障碍访问、图形数量在几千以内、需要 CSS 动画用 Canvas:大量元素(超过 3000 个)、像素级操作、实时游戏渲染、不需要单个元素的交互实际项目中,图表用 SVG(D3.js / ECharts 的 SVG 模式),游戏用 Canvas,这是比较成熟的选型。几个实际开发中的坑innerHTML 可以用但别滥用。svg.innerHTML = '<circle cx="50" cy="50" r="40"/>' 在现代浏览器中能工作,但它不经过命名空间检查,序列化时可能出问题。动态创建元素还是老老实实用 createElementNS。getBBox() 获取元素边界。想知道一个 SVG 元素实际占了多大空间,用 getBBox() 返回 { x, y, width, height },这个值不受 transform 影响,是元素自身的原始尺寸。SMIL 动画(<animate> 标签)正在被边缘化。Chrome 曾一度要移除 SMIL 支持,虽然后来撤回了,但趋势是尽量用 CSS 动画或 JavaScript 替代 SMIL。SVGO 压缩 SVG。从设计工具导出的 SVG 通常包含大量冗余属性(编辑器元数据、无用空白等),用 SVGO 压缩可以减小 30%-70% 的体积,线上必须走一遍。
服务端阅读 05月27日 15:48

SVG 和 Canvas 有什么区别?什么时候用哪个?

SVG 和 Canvas 都能在网页上绘图,但底层原理完全不同:SVG 是矢量图,基于 DOM。 每个图形都是一个独立的 DOM 节点,可以用 CSS 设样式、用 JS 绑事件,浏览器负责渲染和重绘。放大缩小永远清晰,因为存的是数学描述而非像素点。Canvas 是位图,基于像素。 你通过 JS 调用绘图 API 在画布上逐像素绘制,画完浏览器就不管了——它不记得你画了什么,只保存最终那张位图。要改东西就得清空重画。这个根本差异决定了它们在性能、交互、可访问性上的所有不同。7 个维度的详细对比1. 渲染机制SVG 绘制的每个元素都保留在 DOM 树中。你画了一个圆,它就是一个 <circle> 节点,属性改了浏览器自动重绘。Canvas 只有一个 <canvas> 标签,内部全靠 JS 维护状态。你画了一万个圆,DOM 里还是只有一个元素。<!-- SVG:每个图形是独立节点 --><svg width="200" height="200"> <circle cx="100" cy="100" r="50" fill="red" /></svg><!-- Canvas:只有一个标签,图形全靠 JS 绘制 --><canvas id="c" width="200" height="200"></canvas><script> const ctx = document.getElementById('c').getContext('2d'); ctx.beginPath(); ctx.arc(100, 100, 50, 0, Math.PI * 2); ctx.fillStyle = 'red'; ctx.fill();</script>2. 性能表现这是面试中最常被追问的点:SVG 性能与元素数量强相关。 元素少的时候没问题,一旦到几千个节点,DOM 操作和重绘的开销急剧上升。实际测试中,3000-5000 个元素是个常见的瓶颈区间。Canvas 性能与画布尺寸强相关,与绘制对象数量关系不大。 画一万个点和画一百个点,只要画布尺寸相同,帧率差异不大。Canvas 不维护对象模型,所以没有 DOM 操作的开销。简单判断:图形少用 SVG,图形多用 Canvas。3. 事件交互SVG 天然支持 DOM 事件。每个 <circle>、<path> 都能直接绑 click、mouseenter,跟操作普通 HTML 元素一样。Canvas 只有整个画布能接收事件。想判断点击了哪个图形,需要自己做碰撞检测——记录每个图形的坐标和边界,点击时遍历计算。库如 Konva.js、Fabric.js 帮你在 Canvas 上模拟了对象模型,本质上还是在做碰撞检测。// Canvas 碰撞检测示例canvas.addEventListener('click', (e) => { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // 遍历所有图形判断点击了哪个 for (const shape of shapes) { if (isPointInShape(x, y, shape)) { handleClick(shape); break; } }});4. 缩放与分辨率SVG 是矢量图,任意缩放都不失真,特别适合需要高分辨率输出的场景(打印、Retina 屏)。Canvas 是位图,放大就模糊。要做高清适配,需要手动处理设备像素比(devicePixelRatio),设置更大的画布尺寸再 scale 下来:const dpr = window.devicePixelRatio || 1;canvas.width = width * dpr;canvas.height = height * dpr;canvas.style.width = width + 'px';canvas.style.height = height + 'px';ctx.scale(dpr, dpr);5. 可访问性与 SEOSVG 内容是 DOM 节点,屏幕阅读器可以读取,搜索引擎可以索引文字内容。可以添加 <title> 和 <desc> 标签增强无障碍支持。Canvas 对屏幕阅读器不可见。要支持无障碍,需要额外写 ARIA 标签或在画布外提供替代文本。搜索引擎也无法抓取 Canvas 中的内容。如果页面内容需要被搜索到,SVG 是更好的选择。6. 动画实现SVG 动画可以用 CSS 动画、SMIL 或 JS 操纵 DOM 属性。简单动画实现起来很直观:/* SVG 元素直接用 CSS 动画 */circle { transition: r 0.3s ease;}circle:hover { r: 60;}Canvas 动画需要用 requestAnimationFrame 手动实现帧循环,每帧清空画布重绘。复杂度高,但对帧率有完全控制权,适合游戏和粒子系统。7. 内存管理SVG 的内存占用随元素数量线性增长,每个节点都是一个完整 DOM 对象。大量 SVG 元素会导致内存压力。Canvas 内存占用主要取决于画布尺寸(width × height × 4 bytes),与绘制内容复杂度无关。一张 1000×1000 的画布固定占约 4MB 内存。决策矩阵| 场景 | 选 SVG | 选 Canvas ||------|--------|-----------|| 图标、Logo | ✅ 矢量清晰,交互方便 | || 简单图表(<3000 数据点) | ✅ 事件绑定简单,可访问 | || 大数据可视化(万级数据点) | | ✅ 性能稳定 || 2D 游戏 | | ✅ 帧率可控 || 图像编辑(裁剪、滤镜) | | ✅ 像素级操作 || 需要缩放/打印 | ✅ 矢量不失真 | || SEO 重要 | ✅ 可被索引 | || 粒子效果/物理模拟 | | ✅ 高性能渲染 || 需要交互的地图 | ✅ 事件绑定天然支持 | |混合方案实际项目中,两者经常配合使用:Canvas 负责高性能渲染(粒子背景、热力图),SVG 负责交互层(标注点、悬浮提示)。很多现代图表库已经内置了这种混合策略。D3.js 以 SVG 为主,适合中小规模数据可视化;ECharts 默认使用 Canvas,适合大数据量图表;Konva.js 在 Canvas 上模拟了类似 SVG 的对象模型,兼顾性能和交互。面试回答建议先一句话说清本质区别:SVG 是基于 DOM 的矢量图,Canvas 是基于像素的位图。 然后从性能、交互、缩放、可访问性四个维度展开。最后给出选择依据:图形少、要交互、要缩放选 SVG;图形多、要性能、要像素控制选 Canvas。这个回答结构清晰,覆盖面试官可能追问的所有方向。
服务端阅读 05月27日 15:44

SVG 动画有哪些实现方式?它们之间有什么区别?

前端开发中,SVG 动画主要有三种实现方式:SMIL 动画、CSS 动画和 JavaScript 动画。三种方式各有适用场景,理解它们的差异是选择技术方案的关键。SMIL 动画(原生 SVG 动画)SMIL(Synchronized Multimedia Integration Language)是 SVG 规范内建的动画语法,直接在 SVG 标签中声明动画行为,无需额外引入 CSS 或 JavaScript。核心元素<animate>:对数值型属性做插值动画,如 cx、r、opacity<animateTransform>:控制 transform 变换(平移、旋转、缩放、倾斜)<animateMotion>:让元素沿指定路径运动<set>:对非数值属性做瞬时切换,如 visibility代码示例<svg width="200" height="200"> <circle cx="50" cy="50" r="20" fill="red"> <animate attributeName="cx" from="50" to="150" dur="2s" repeatCount="indefinite" /> <animate attributeName="fill" values="red;blue;red" dur="2s" repeatCount="indefinite" /> </circle></svg>优势声明式语法,动画定义与 SVG 结构一体化,代码自包含不依赖 CSS 或 JavaScript,即使脚本被禁用也能运行可用于 <img> 标签或 CSS 背景图场景支持动画链和同步控制(begin 属性可以引用其他动画的结束事件)劣势Chrome 曾宣布弃用 SMIL(后撤回弃用计划,但兼容性风险仍在)交互能力有限,无法根据用户输入动态改变动画参数调试工具支持较弱,DevTools 对 SMIL 的可视化编辑不如 CSS 动画友好Safari 对部分 SMIL 特性的支持存在差异CSS 动画通过 CSS 的 @keyframes、animation 和 transition 属性驱动 SVG 元素动画,是日常开发中使用最广泛的方式。代码示例<svg width="200" height="200"> <style> .circle { animation: move 2s infinite alternate; } .circle:hover { fill: blue; transition: fill 0.3s; } @keyframes move { from { transform: translateX(0); } to { transform: translateX(100px); } } </style> <circle class="circle" cx="50" cy="50" r="20" fill="red" /></svg>优势浏览器兼容性最好,标准成熟稳定transform 和 opacity 动画可触发 GPU 合成层,性能优异DevTools 支持完善,可实时调试和调整动画参数天然支持 :hover、:focus 等伪类交互样式与结构分离,便于复用和维护劣势只能动画 CSS 可识别的属性,SVG 独有属性(如 d、cx、points)在部分浏览器中不支持 CSS 动画Safari 不支持通过 CSS 动画化 <path> 的 d 属性,形状变形动画受限复杂序列动画需要大量 @keyframes 和时间计算,代码可读性下降无法实现条件逻辑或基于用户输入的动态控制CSS 动画化 SVG 属性的兼容性现状| 属性 | Chrome | Firefox | Safari ||------|--------|---------|--------|| transform | 支持 | 支持 | 支持 || opacity | 支持 | 支持 | 支持 || cx / cy / r | 支持 | 支持 | 部分支持 || d(路径变形) | 支持 | 支持 | 不支持 || fill / stroke | 支持 | 支持 | 支持 |JavaScript 动画通过 JavaScript 直接操作 SVG DOM,或借助动画库实现复杂效果。灵活性最高,适合交互密集的场景。原生 JavaScript 示例const circle = document.querySelector('circle');let position = 50;function animate() { position += 1; circle.setAttribute('cx', position); if (position < 150) { requestAnimationFrame(animate); }}animate();GSAP 示例gsap.to('circle', { attr: { cx: 150 }, duration: 2, repeat: -1, yoyo: true});优势完全控制动画的每一个细节,可动画任何 SVG 属性可根据用户输入、滚动位置、数据变化等动态调整动画动画库(GSAP、Anime.js、Motion One)提供缓动函数、时间轴、交错动画等高级能力可与业务逻辑深度集成,实现数据驱动的可视化动画劣势代码量较大,维护成本高于声明式方案性能依赖实现质量,低效的 DOM 操作会导致卡顿依赖 JavaScript 运行环境,脚本被禁用时动画失效增加第三方库会增加打包体积Web Animations API浏览器原生提供的 element.animate() 方法,兼具 CSS 动画的性能和 JavaScript 的灵活性:const circle = document.querySelector('circle');circle.animate( [ { transform: 'translateX(0)' }, { transform: 'translateX(100px)' } ], { duration: 2000, iterations: Infinity, direction: 'alternate' });Web Animations API 可以在不引入第三方库的情况下获得接近 CSS 的性能,同时保留 JavaScript 的动态控制能力。但浏览器兼容性(特别是 Safari)需要注意。三种方式核心对比| 维度 | SMIL | CSS | JavaScript ||------|------|-----|------------|| 学习成本 | 中 | 低 | 高 || 灵活性 | 低 | 中 | 高 || 性能 | 好 | 最好 | 取决于实现 || 交互能力 | 弱 | 中 | 强 || 浏览器兼容 | 有风险 | 最好 | 好 || 可调试性 | 弱 | 强 | 中 || 适用场景 | 独立 SVG 文件 | 简单动画、UI反馈 | 复杂交互、数据驱动 |如何选择简单属性动画和 UI 反馈(按钮缩放、图标旋转、淡入淡出):优先 CSS 动画,性能最优、代码最少独立 SVG 文件中的自包含动画(图标、加载动画):SMIL 仍可用,但需评估兼容性风险复杂交互和数据驱动动画(图表、游戏、滚动动画):JavaScript + 动画库,GSAP 是目前最成熟的选择需要兼顾性能和动态控制:Web Animations API 是折中方案,但要做好兼容性降级实际项目中,三种方式并非互斥。常见做法是用 CSS 处理简单过渡,用 JavaScript 库处理复杂序列,必要时在独立 SVG 中使用 SMIL。关键是根据动画复杂度、交互需求和兼容性要求做出权衡。
服务端阅读 05月27日 15:43

SVG 性能优化有哪些常用方法?

为什么需要优化 SVGSVG 是前端开发中常用的矢量图形格式,但未经优化的 SVG 文件往往包含大量冗余代码,文件体积是实际所需的 2-5 倍。在实际项目中,一个从设计工具导出的图标 SVG 可能有 3KB,经过优化后不到 500 字节,压缩率可达 60%-80%。SVG 文件过大会拖慢页面加载速度,直接影响 LCP(最大内容绘制)指标;渲染复杂度过高则会影响 INP(交互延迟)和 CLS(布局偏移)等 Core Web Vitals 指标。一、精简 SVG 代码移除编辑器元数据设计工具导出的 SVG 通常携带大量无用信息:<title>Created with Figma</title> 这类声明<desc> 描述标签编辑器自定义属性(data-name、sketch:type 等)XML 注释和空行Inkscape / Illustrator 特有的命名空间声明这些内容对渲染毫无帮助,却占用了大量字节。手动清理费时费力,推荐使用 SVGO 自动处理。移除默认值属性SVG 有许多属性的默认值是可以省略的:fill="black" — fill 默认就是 blackstroke-width="1" — 默认值即为 1stroke-linecap="butt" — 默认对齐方式font-style="normal" — 默认正常样式display="inline" — 默认显示方式省略这些属性不仅能减小文件体积,还能让代码更简洁。简化路径数据路径(<path>)通常是 SVG 中体积最大的部分,优化路径数据的效果最明显:使用相对坐标:相对命令(h、v、l、c)比绝对命令(H、V、L、C)更短,因为只需要记录偏移量降低小数精度:50.123456 缩短为 50.12,在视觉上几乎无差异,但大幅减少字符数合并相邻同类命令:两个连续的 l 命令可以合并参数使用简写命令:水平线用 h 代替 l,垂直线用 v 代替 l<!-- 优化前:绝对坐标 + 高精度 --><path d="M10.000000 20.000000 L30.000000 40.000000 L50.000000 20.000000 Z"/><!-- 优化后:相对坐标 + 低精度 --><path d="M10 20l20 20 20-20z"/>二、压缩与传输优化SVGO 工具SVGO 是目前最主流的 SVG 优化工具,基于 Node.js,支持插件化配置,能自动完成上述所有代码层面的优化:# 单文件优化npx svgo input.svg -o output.svg# 批量优化整个目录npx svgo -f ./icons -o ./optimized# 指定精度为 2 位小数npx svgo input.svg -o output.svg --precision 2SVGO 默认插件包括移除元数据、移除注释、合并路径、转换样式等,大多数场景直接使用默认配置即可获得 50%-70% 的体积缩减。SVGOMG 在线工具如果不想安装命令行工具,SVGOMG 是 SVGO 的 Web 界面版本,可以在浏览器中实时预览优化效果,逐项开关插件并查看体积变化,适合偶尔使用或快速验证。服务器压缩SVG 是纯文本的 XML 格式,gzip 和 Brotli 压缩效果极好:gzip 压缩通常可再减小 60%-70%Brotli 比 gzip 再额外节省 10%-15%配置 Nginx 开启 Brotli 后,一个 12KB 的 SVG 传输时可能只有 2-3KB# Nginx 开启 gzip 压缩 SVGgzip on;gzip_types image/svg+xml;# Brotli(需安装模块)brotli on;brotli_types image/svg+xml;三、SVG Sprite 与复用当页面中有多个 SVG 图标时,逐个加载会产生大量 HTTP 请求。SVG Sprite 是解决这个问题的标准方案。symbol + use 模式将所有图标定义在 <symbol> 元素中,通过 <use> 引用,只需一次 HTTP 请求:<!-- 定义 Sprite --><svg style="display:none"> <symbol id="icon-home" viewBox="0 0 24 24"> <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/> </symbol> <symbol id="icon-user" viewBox="0 0 24 24"> <path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/> </symbol></svg><!-- 使用图标 --><svg><use href="#icon-home"/></svg><svg><use href="#icon-user"/></svg>这种模式下,所有图标共享一个 SVG 文件,浏览器只需请求一次,后续通过 <use> 引用时直接从缓存读取。defs 复用元素对于页面中重复出现的图形元素(渐变、形状等),用 <defs> 定义一次,多次引用:<svg> <defs> <linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" style="stop-color:#f00"/> <stop offset="100%" style="stop-color:#00f"/> </linearGradient> </defs> <rect fill="url(#grad1)" width="100" height="50"/> <circle fill="url(#grad1)" cx="150" cy="25" r="25"/></svg>四、渲染性能优化内联关键 SVG首屏需要立即显示的 SVG(如 Logo、关键图标)建议直接内联到 HTML 中,省去 HTTP 请求,加快首次渲染。非首屏的 SVG 则应使用外部文件引用,以便浏览器缓存。使用 viewBox 实现响应式为 SVG 设置 viewBox 而非固定的 width/height,通过 CSS 控制显示尺寸,实现响应式适配:<svg viewBox="0 0 24 24" width="24" height="24"> <path d="..."/></svg>设置 viewBox 后,SVG 会在任何尺寸下保持清晰,同时浏览器能提前计算布局空间,避免 CLS(累积布局偏移)。减少元素与嵌套层级合并能合并的路径,减少 DOM 节点数用 <g> 分组替代多个独立元素去掉不必要的嵌套 <g> 包裹对于纯展示的元素,设置 pointer-events="none" 跳过事件检测DOM 节点越少,浏览器解析和渲染越快,这在大量 SVG 图标的页面上差异尤为明显。优化 SVG 动画动画性能的关键是选择正确的属性:优先使用 transform 和 opacity:这两个属性可以被 GPU 加速,不会触发重排避免动画 width、height、left、top、x、y:这些属性会触发布局重计算,性能开销大CSS 动画通常比 SMIL 动画性能更好,且兼容性更可控/* 推荐:GPU 加速 */.icon:hover { transform: scale(1.2); opacity: 0.8;}/* 避免:触发重排 */.icon:hover { width: 30px; height: 30px;}降低渲染复杂度减少滤镜(filter)的使用,尤其是 blur 和 drop-shadow,它们消耗大量 GPU 资源限制渐变数量,合并重复的渐变定义使用 shape-rendering="optimizeSpeed" 替代抗锯齿渲染,在图标等小尺寸场景下差异不大但性能更好用 fill-opacity/stroke-opacity 替代整体 opacity,前者不会创建合成层五、构建工具集成在实际项目中,SVG 优化应该集成到构建流程中,而不是手动处理。Webpack 配置npm install svgo svgo-loader --save-dev// webpack.config.jsmodule.exports = { module: { rules: [ { test: /\.svg$/, use: ['@svgr/webpack', 'svgo-loader'] } ] }}Vite 配置npm install vite-plugin-svgr --save-dev// vite.config.jsimport svgr from 'vite-plugin-svgr';export default { plugins: [svgr()]}构建工具集成后,每次构建都会自动优化 SVG,无需手动干预。性能验证优化完成后,需要实际验证效果:Lighthouse:检测页面整体性能,关注 LCP 和 FCP 指标Chrome DevTools Coverage:查看 SVG 文件的实际使用率,找出未使用的代码Network 面板:对比优化前后的传输大小(注意查看压缩后体积)Performance 面板:录制 SVG 渲染过程,检查是否有长任务优化一个 SVG 图标从 3KB 降到 500B 看似微小,但当页面有 20-30 个图标时,总体节省可达 50-70KB,对首屏加载速度的影响不可忽视。
服务端阅读 05月27日 15:43

SVG 如何与 CSS 结合使用?8 种方式从基础到高级动画

SVG 不仅是矢量图形格式,它和 CSS 的结合才是真正释放 SVG 威力的关键。内联 SVG 是 DOM 的一部分,每个形状、路径、文字都可以被 CSS 选中并施加样式、过渡和动画——这是 PNG、WebP 等位图永远做不到的。内联 SVG 是前提只有内联 SVG(直接写在 HTML 中的 <svg> 标签)才能被 CSS 完整控制。通过 <img> 引入的 SVG,外部 CSS 无法选中其内部元素,伪类和动画也会失效。所以如果需要用 CSS 操控 SVG,必须用内联方式。1. 用 CSS 属性替代 SVG 属性SVG 元素支持通过 CSS 设置视觉属性,fill、stroke、stroke-width、opacity 等都可以写在 CSS 规则里,和设置 HTML 元素的 color、background 没有本质区别:.icon-circle { fill: #3b82f6; stroke: #1e40af; stroke-width: 2; opacity: 0.9;}<svg viewBox="0 0 100 100" width="100" height="100"> <circle class="icon-circle" cx="50" cy="50" r="40" /></svg>注意:CSS 属性会覆盖 SVG 元素上的同名属性(style 优先级高于 presentation attributes),所以把样式集中到 CSS 里更好维护。2. 用类名和选择器精确控制SVG 元素和 HTML 一样支持 class、id,CSS 的各种选择器都能用:/* 类选择器 */.logo-path { fill: #111; }/* 后代选择器 */.nav-icon .highlight { fill: #f59e0b; }/* 属性选择器 */circle[data-state="active"] { fill: #10b981; }/* :nth-child */.chart-bar:nth-child(odd) { fill: #6366f1; }.chart-bar:nth-child(even) { fill: #818cf8; }灵活运用选择器,可以避免给每个 SVG 元素加类名,减少标记冗余。3. 伪类实现交互反馈:hover、:focus、:active 对 SVG 元素完全有效,配合 transition 就能做出丝滑的交互效果:.btn-icon { fill: #64748b; transition: fill 0.2s, transform 0.2s; cursor: pointer;}.btn-icon:hover { fill: #3b82f6; transform: scale(1.15);}.btn-icon:focus-visible { outline: 2px solid #3b82f6; outline-offset: 3px;}<svg viewBox="0 0 24 24" width="24" height="24"> <path class="btn-icon" tabindex="0" d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.56 5.82 22 7 14.14l-5-4.87 6.91-1.01z" /></svg>tabindex="0" 让 SVG 元素可聚焦,配合 :focus-visible 提升键盘可访问性。实际项目中,图标 hover 变色、按钮按下缩放都是这么做的。4. CSS 过渡与关键帧动画过渡(transition)过渡适合状态切换——hover 时变色、展开时位移,简单高效:.sidebar-arrow { transition: transform 0.3s ease; transform-origin: center;}.sidebar-arrow.open { transform: rotate(90deg);}JavaScript 切换 .open 类名即可,不需要操作 SVG 属性。关键帧动画(@keyframes)需要持续或循环的效果用 @keyframes:.spinner { animation: spin 1s linear infinite; transform-origin: center;}@keyframes spin { to { transform: rotate(360deg); }}.pulse-dot { animation: pulse 1.5s ease-in-out infinite;}@keyframes pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.3); opacity: 0.6; }}加载旋转、呼吸闪烁,这些是最常见的 SVG CSS 动画场景。5. 描边动画:stroke-dasharray 与 stroke-dashoffset这是 SVG CSS 动画里最出效果的一招。原理很简单:先让 stroke-dasharray 等于路径总长度,整条线变成虚线且间距等于线长,视觉上不可见;然后通过 stroke-dashoffset 从线长过渡到 0,线就"画"出来了。.draw-path { stroke-dasharray: 300; stroke-dashoffset: 300; animation: draw 2s ease forwards;}@keyframes draw { to { stroke-dashoffset: 0; }}<svg viewBox="0 0 200 100" width="200"> <path class="draw-path" d="M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80" fill="none" stroke="#3b82f6" stroke-width="3" stroke-linecap="round" /></svg>路径总长度可以通过 JavaScript 的 path.getTotalLength() 获取。实际开发中,Logo 描边动画、数据可视化图表的绘制效果,都是这个技术。6. CSS 变量动态控制样式CSS 变量让 SVG 样式变得可配置,一套图形换个主题色只需改几个变量::root { --icon-primary: #3b82f6; --icon-stroke: #1e40af; --icon-hover: #ef4444;}.themed-icon { fill: var(--icon-primary); stroke: var(--icon-stroke); transition: fill 0.2s;}.themed-icon:hover { fill: var(--icon-hover);}在暗色模式下覆盖变量即可,不用写重复的选择器:@media (prefers-color-scheme: dark) { :root { --icon-primary: #60a5fa; --icon-stroke: #93c5fd; --icon-hover: #f87171; }}7. 外部样式表与样式分离小型项目可以在 SVG 的 <style> 标签里写 CSS,但项目规模大了以后,把 SVG 样式抽到外部 CSS 文件更合理——和 HTML 样式统一管理,方便复用和压缩:<!-- HTML --><link rel="stylesheet" href="svg-styles.css" /><svg viewBox="0 0 24 24" class="icon"> <path class="icon-path" d="..." /></svg>/* svg-styles.css */.icon { width: 24px; height: 24px; }.icon-path { fill: currentColor; transition: fill 0.2s; }.icon:hover .icon-path { fill: #3b82f6; }currentColor 是个实用技巧——SVG 的 fill 继承父元素的 color,这样改文字颜色就能同步改图标颜色。8. 响应式 SVG 与媒体查询SVG 配合 viewBox 和 CSS 媒体查询,可以实现真正的响应式图形:<svg viewBox="0 0 400 200" width="100%"> <rect class="responsive-rect" x="10" y="10" width="180" height="180" rx="8" /> <text class="label" x="100" y="105" text-anchor="middle">Hello</text></svg>.responsive-rect { fill: #3b82f6; transition: fill 0.3s;}.label { fill: white; font-size: 16px;}@media (max-width: 600px) { .responsive-rect { fill: #ef4444; } .label { font-size: 12px; }}viewBox 让 SVG 自适应容器宽度,媒体查询根据屏幕尺寸调整样式,两者配合不需要 JavaScript。性能注意事项优先用 transform 和 opacity 做动画,这两个属性不触发重排(reflow),GPU 加速友好。fill、stroke 等属性的变化会触发重绘(repaint),大量元素同时动画时可能掉帧。避免对大量 SVG 元素同时施加复杂动画,可以用 will-change: transform 提示浏览器提前优化,但不要滥用。<img> 引入的 SVG 无法用外部 CSS 控制,需要交互和动画时必须内联。但内联 SVG 会增加 DOM 节点,大型图表类 SVG(数百个节点)要考虑虚拟滚动或懒加载。stroke-dashoffset 动画在低端设备上可能卡顿,路径越长越明显,必要时用 requestAnimationFrame 替代纯 CSS 方案。核心就一点:内联 SVG 的每个元素都是 DOM 节点,CSS 能对 HTML 做的事,对 SVG 照样做。掌握选择器、过渡、关键帧、描边动画这四样,基本覆盖日常开发 90% 的需求。
服务端阅读 05月27日 15:42

SVG 与其他图形格式有什么区别?各有什么优劣?

在前端开发中,选择合适的图形格式直接影响页面性能和用户体验。SVG 作为唯一的 Web 原生矢量格式,与 PNG、JPG、Canvas、WebP 等有着本质区别。理解这些差异是前端面试的高频考点,也是实际项目选型的关键。SVG 与位图格式(PNG/JPG)的本质区别SVG 是基于 XML 的矢量图形,用数学公式描述图形的点和路径;PNG 和 JPG 则是位图,由固定数量的像素点组成。这个根本差异带来了以下不同:缩放表现——SVG 无限放大依然清晰,位图放大后出现锯齿和模糊。一个 1KB 的 SVG 图标在 4K 屏幕上和 1080p 屏幕上显示效果一致,而 PNG 需要提供 @2x、@3x 多个版本才能适配。文件体积——简单图形(图标、logo、几何图形)SVG 体积远小于 PNG。但复杂图像(如照片)用 SVG 描述反而更大,因为每个像素都需要用路径节点表示。可操作性——SVG 可以直接用 CSS 修改颜色、添加动画、响应事件,也能被搜索引擎索引;位图一旦生成就是静态像素,无法单独操作内部元素。适用边界——照片、渐变复杂的图像不适合用 SVG,此时应选 JPG(有损压缩,体积小)或 PNG(无损压缩,支持透明)。SVG 与 Canvas 的核心差异SVG 和 Canvas 都能在浏览器中绘制图形,但工作方式截然不同:渲染模式——SVG 采用保留模式(Retained Mode),每个图形元素都是 DOM 节点,浏览器负责维护整个场景树;Canvas 采用立即模式(Immediate Mode),通过 JavaScript 逐帧绘制像素,画完之后不保留图形对象。<!-- SVG:声明式,每个元素可独立操作 --><svg width="200" height="200"> <circle cx="100" cy="100" r="50" fill="blue" id="myCircle"/></svg><script>// Canvas:命令式,逐帧绘制const canvas = document.getElementById("myCanvas");const ctx = canvas.getContext("2d");ctx.beginPath();ctx.arc(100, 100, 50, 0, Math.PI * 2);ctx.fillStyle = "blue";ctx.fill();</script>事件处理——SVG 的每个元素天然支持 click、hover 等事件,因为它们就是 DOM 节点;Canvas 需要手动计算坐标碰撞检测来实现交互。性能拐点——当图形元素少于 1000 个时,SVG 的 DOM 操作更直观高效;超过 1000 个元素后,SVG 的 DOM 重绘开销急剧上升,Canvas 的像素操作反而更快。数据可视化中的散点图(上万个点)用 Canvas,少量图元的交互图表用 SVG。内存占用——SVG 的 DOM 节点会持续占用内存,复杂场景可能导致页面卡顿;Canvas 只占用像素缓冲区,内存可控。SVG 与 WebP/AVIF 的选择WebP 和 AVIF 是面向照片类图像的现代格式,和 SVG 解决的不是同一个问题:SVG 解决矢量图形的缩放和交互问题WebP 比 JPG 小 25%-35%,支持透明和动画,适合替代 JPG/PNG 做照片展示AVIF 基于 AV1 编解码器,比 WebP 再小 20%-50%,但编码速度慢,适合预生成的静态资源实际项目中,图标和 UI 元素用 SVG,产品图片用 WebP(AVIF 做渐进增强):<picture> <source srcset="photo.avif" type="image/avif"> <source srcset="photo.webp" type="image/webp"> <img src="photo.jpg" alt="产品图片"></picture>SVG 与图标字体的取舍图标字体(如 Font Awesome)曾经是图标方案的主流,但 SVG 图标在多个维度更优:多色支持——字体图标只能是单色,SVG 支持渐变和多色定位精度——字体图标依赖字体的 baseline 对齐,可能出现像素级偏移;SVG 坐标系统精确可控无字体加载问题——字体加载失败时图标显示方框,SVG 内联不存在这个问题字体图标的优势在于兼容老旧浏览器和加载方式简单。新项目建议直接用 SVG sprite 或 SVG 组件方案:<!-- SVG Sprite 方案 --><svg class="icon"><use href="#icon-home"/></svg><!-- React 组件方案 --><HomeIcon size={24} color="currentColor" />如何做出正确的选型决策根据场景做选择,而不是追求统一方案:| 场景 | 推荐格式 | 原因 ||------|----------|------|| 图标、Logo | SVG | 矢量缩放、可交互、体积小 || 产品照片 | WebP/AVIF | 高压缩率、色彩丰富 || 数据图表(少量图元) | SVG | DOM 交互、可访问性 || 数据可视化(海量数据点) | Canvas/WebGL | 渲染性能 || 游戏画面 | Canvas/WebGL | 逐帧控制、GPU 加速 || 简单循环动画 | CSS + SVG | 流畅、可控 || 需要打印的文档 | PDF | 跨平台一致性 |核心原则:能用 SVG 的地方优先用 SVG(缩放无损、可交互、SEO 友好),照片类内容用 WebP/AVIF(压缩率高、加载快),高频重绘场景用 Canvas(性能可控)。三者不是互斥关系,一个页面中同时使用三种方案是常见做法。
服务端阅读 05月27日 15:38

SVG 的坐标系统和 viewBox 变换原理是什么?

SVG 坐标系的基本模型SVG 使用笛卡尔坐标系,但和数学课本上的不同:原点 (0,0) 在画布左上角,x 轴向右为正,y 轴向下为正。这个设定和浏览器渲染一致,也意味着"向上移动"对应的 y 值是负数。理解 SVG 坐标系要抓住两个层次:视口(viewport):由 <svg> 的 width 和 height 决定,是 SVG 在页面上占据的实际像素区域。width="200" height="200" 就是 200x200 像素的画布。用户坐标系(user coordinate system):SVG 内部绘图使用的逻辑坐标空间。默认一个用户单位等于一个像素,但 viewBox 会改变这个映射关系。单位方面,SVG 支持 px、em、rem、cm、mm、% 等,无单位数字默认等同于 px。实际开发中绝大多数场景用无单位数字就够了。viewBox 做了什么?viewBox 是 SVG 里最关键的属性之一,它定义内部逻辑坐标系的范围,再将该范围映射到视口上。语法为 viewBox="min-x min-y width height"。<svg viewBox="0 0 100 100" width="200" height="200"> <circle cx="50" cy="50" r="40" fill="red" /></svg>这里 viewBox="0 0 100 100" 声明逻辑坐标系为 100x100,视口为 200x200 像素。浏览器计算缩放比:水平 200/100=2,垂直 200/100=2,所以逻辑坐标中 1 个单位等于 2 个像素。圆心在逻辑坐标 (50,50),实际渲染在视口的 (100,100) 像素位置。viewBox 的三个核心作用:解耦绘图尺寸与渲染尺寸:图标设计常用 viewBox="0 0 24 24",因为 24 的网格便于对齐和计算,实际显示大小由外部 CSS 控制。实现响应式缩放:设置 width="100%" 配合 viewBox,SVG 自动适配容器大小,内部坐标无需改动。控制可视区域:调整 min-x 和 min-y 可以平移可视区域,类似"镜头移动"。viewBox="-50 -50 200 200" 相当于将坐标系原点向右下偏移 50 个单位,让你看到原点左上方的内容。preserveAspectRatio 如何处理宽高比不一致?当 viewBox 的宽高比和视口不一致时,preserveAspectRatio 决定 SVG 内容如何适配视口。语法为 preserveAspectRatio="align meetOrSlice":align:对齐方式,由 x 方向(xMin / xMid / xMax)和 y 方向(YMin / YMid / YMax)组合,共 9 种。meetOrSlice:缩放策略,meet 保持比例完整显示(可能留白),slice 保持比例填充区域(可能裁切)。<!-- 居中完整显示,保持比例(默认值) --><svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" width="300" height="200"> <rect x="0" y="0" width="100" height="100" fill="blue" /></svg><!-- 居中裁切填充,保持比例 --><svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice" width="300" height="200"> <rect x="0" y="0" width="100" height="100" fill="blue" /></svg><!-- 拉伸变形,忽略原始比例 --><svg viewBox="0 0 100 100" preserveAspectRatio="none" width="300" height="200"> <rect x="0" y="0" width="100" height="100" fill="blue" /></svg>日常开发中 xMidYMid meet 是最常用的默认配置。none 一般避免使用,除非确实需要拉伸效果(比如背景图案)。一个典型的 slice 场景是全屏背景图:你希望图片铺满容器,宁可裁切也不留白。SVG transform 有哪些变换类型?SVG 的 transform 属性支持以下变换函数:translate — 平移将元素沿 x 和 y 方向移动指定距离。只写一个值时 y 方向默认为 0。<rect x="10" y="10" width="50" height="50" fill="red" transform="translate(100, 80)" />注意:SVG 的 translate 是相对于 SVG 画布当前坐标系原点,而非元素自身位置。CSS 的 translate 则是相对于元素自身的,这点容易混淆。rotate — 旋转rotate(angle, cx, cy) 指定旋转角度(度)和旋转中心坐标。省略旋转中心时默认围绕当前坐标系原点 (0,0) 旋转。<!-- 围绕元素中心旋转 45 度 --><rect x="0" y="0" width="50" height="50" fill="blue" transform="rotate(45, 25, 25)" /><!-- 围绕坐标系原点旋转 30 度 --><rect x="100" y="100" width="50" height="50" fill="green" transform="rotate(30)" />这是 SVG transform 和 CSS transform 的一个重要差异:CSS 默认以元素中心为旋转原点(transform-origin: 50% 50%),SVG 默认以坐标系原点。在 SVG 中想让元素绕自身中心旋转,必须手动指定 cx, cy,或者用 translate + rotate + translate 三步模拟。scale — 缩放scale(sx, sy) 分别指定水平和垂直方向的缩放倍数。只写一个值时 sy 等于 sx。<!-- 等比放大 2 倍 --><rect x="50" y="50" width="30" height="30" fill="green" transform="scale(2)" /><!-- 水平放大 1.5 倍,垂直缩小到 0.5 倍 --><rect x="50" y="50" width="30" height="30" fill="yellow" transform="scale(1.5, 0.5)" />关键细节:scale 以坐标系原点为缩放中心,同时会改变元素的坐标位置。transform="scale(2)" 会让位于 (50,50) 的元素实际渲染到 (100,100)。想让元素在原位放大,需要先 translate 到原点,再 scale,再 translate 回去。skewX / skewY — 倾斜沿 x 轴或 y 轴方向倾斜指定角度。<rect x="50" y="50" width="50" height="50" fill="purple" transform="skewX(30)" /><rect x="50" y="50" width="50" height="50" fill="orange" transform="skewY(20)" />倾斜在日常开发中用得较少,但在制作平行四边形、梯形等几何效果时会派上用场。matrix — 矩阵变换所有变换最终都归结为矩阵运算。matrix(a, b, c, d, e, f) 对应变换矩阵:| a c e || b d f || 0 0 1 |其中 a 和 d 控制缩放,b 和 c 控制倾斜,e 和 f 控制平移。其他变换函数本质上是 matrix 的语法糖:translate(tx, ty) = matrix(1, 0, 0, 1, tx, ty)scale(s) = matrix(s, 0, 0, s, 0, 0)rotate(a) = matrix(cos(a), sin(a), -sin(a), cos(a), 0, 0)直接用 matrix 的场景不多,但在需要高性能批量变换(如 Canvas 导出 SVG、复杂动画插值)时更高效。组合变换多个变换函数写在同一个 transform 属性中,按从右到左的顺序依次应用(矩阵乘法的右乘规则):<rect x="0" y="0" width="50" height="50" fill="red" transform="translate(100, 100) rotate(45) scale(1.5)" />变换顺序非常重要:translate -> rotate -> scale 和 rotate -> translate -> scale 的结果完全不同。每次变换都在修改当前坐标系,后续变换基于已修改的坐标系执行。实践中推荐"先缩放、再旋转、最后平移"的顺序(SRT),这样平移的方向不受旋转影响,缩放也不影响平移距离。SVG transform 和 CSS transform 有什么区别?这是面试中的高频混淆点:| 对比项 | SVG transform | CSS transform ||--------|--------------|---------------|| 变换原点 | 默认为当前坐标系原点 (0,0) | 默认为元素中心 (50% 50%) || 语法 | 属性写在元素上:transform="rotate(45)" | 样式写在 CSS 中:transform: rotate(45deg) || 单位 | rotate 不需要单位 | rotate 必须带 deg/rad 等单位 || 坐标系 | 相对于 SVG 当前用户坐标系 | 相对于元素自身的包含块 || transform-origin | 不支持(需手动 translate 模拟) | 支持 transform-origin 属性 |在 SVG 2 规范中,SVG 的 transform 属性和 CSS transform 属性正在统一。现代浏览器已支持在 SVG 元素上使用 CSS transform,这意味着你可以用 transform-origin: center 来简化旋转操作。但在需要兼容旧浏览器时,仍要注意区别。另一个容易忽略的细节:给 SVG 元素加 CSS transform 时,变换原点仍然默认是 (0,0) 而非元素中心,这和普通 HTML 元素不同。需要显式设置 transform-origin 才能改变行为。嵌套坐标系的运作方式<g> 元素可以创建局部坐标系,其 transform 属性会影响所有子元素。绘制复杂图形时,把一组元素看作整体进行变换比逐个操作高效得多。<svg viewBox="0 0 200 200"> <g transform="translate(50, 50)"> <circle cx="0" cy="0" r="20" fill="red" /> <circle cx="50" cy="0" r="20" fill="blue" /> <circle cx="25" cy="43" r="20" fill="green" /> </g></svg><g> 上的 transform="translate(50, 50)" 为所有子元素建立新的局部坐标系,原点偏移到 (50,50)。三个圆的坐标都相对于这个新原点。嵌套可以多层叠加,外层变换会传递到内层:<g transform="translate(50, 50)"> <g transform="rotate(30)"> <rect x="0" y="0" width="40" height="40" fill="teal" /> </g></g>矩形先在父 <g> 的平移坐标系中旋转 30 度,再随父 <g> 整体平移。这和 transform="translate(50,50) rotate(30)" 写在同一个元素上效果一致。实际项目中,嵌套坐标系最常见的应用是组件化图形:比如数据可视化中,每个图表模块用一个 <g> 包裹,通过外层 translate 定位,内部元素用相对坐标绘制,互不干扰。动画中的坐标变换注意事项SVG 坐标变换在动画场景有几个容易踩的坑:CSS 动画和 SVG 属性动画的坐标系不同:用 CSS @keyframes 做 transform 动画时,变换原点遵循 CSS 规则;用 SVG 的 <animateTransform> 时遵循 SVG 规则。两者混用会导致意外行为。transform 不可叠加:SVG 的 <animateTransform> 的 additive 属性默认为 replace,多个动画会互相覆盖。需要 additive="sum" 才能叠加。缩放动画的坐标偏移:对带有 x、y 属性的元素做 scale 动画,元素会向原点方向移动(因为坐标值也被缩放了)。解决方案是用 <g> 包裹,对 <g> 做动画。实际开发中的核心要点始终设置 viewBox:让 SVG 具备响应式能力,避免硬编码固定尺寸。图标用 viewBox="0 0 24 24" 或 viewBox="0 0 16 16",通过 CSS 控制实际显示大小。用 viewBox 而非 width/height 控制缩放:设置 width="100%" 或不设宽高,通过 CSS 控制尺寸,viewBox 负责逻辑坐标映射。注意 transform 顺序:不同顺序产生不同结果,推荐 SRT 顺序(Scale -> Rotate -> Translate)。用 <g> 组织和变换元素组:减少重复代码,提高可维护性,也便于动画控制。SVG 旋转需指定中心点:CSS 中可以 transform-origin: center,SVG 中必须手动写 rotate(angle, cx, cy) 或用 translate 模拟。避免在 SVG 内部使用百分比坐标:百分比在 SVG 中的计算规则复杂,优先使用 viewBox 内的绝对坐标值。CSS transform 和 SVG 属性 transform 不要混用:在同一元素上同时设置两者,行为在不同浏览器中可能不一致。选一种方式贯彻到底。
服务端阅读 05月27日 15:35

SVG 的 clipPath 裁剪和 mask 蒙版有什么区别?怎么用?

SVG 里有两套视觉裁切机制:clipPath 和 mask。前者做硬边缘裁剪,后者做透明度遮罩。前端面试经常考两者的区别,实际开发中圆形头像、文字镂空、渐变淡出也都靠它们实现。这篇文章从语法、属性、CSS 联动到实战案例,逐一拆解。clipPath:硬边缘裁剪clipPath 的逻辑很简单:定义一个封闭区域,区域内的内容保留,区域外的内容直接消失,不存在半透明过渡。基本语法<svg width="200" height="200"> <defs> <clipPath id="circleClip"> <circle cx="100" cy="100" r="80" /> </clipPath> </defs> <rect width="200" height="200" fill="#4A90D9" clip-path="url(#circleClip)" /></svg>clipPath 内部放什么形状,就按什么形状裁。矩形、圆形、多边形、文字都可以。clipPathUnits 属性这个属性决定了裁剪路径的坐标系,默认值是 userSpaceOnUse。userSpaceOnUse:裁剪路径使用元素所在的用户坐标系。clipPath 内的坐标值是绝对坐标,跟被裁剪元素的位置无关。objectBoundingBox:裁剪路径使用被裁剪元素的包围盒作为参考系,坐标值范围 0~1,表示相对比例。<defs> <!-- objectBoundingBox 模式:0.5 表示元素宽度/高度的 50% --> <clipPath id="halfClip" clipPathUnits="objectBoundingBox"> <rect x="0" y="0" width="0.5" height="1" /> </clipPath></defs><rect x="20" y="20" width="160" height="160" fill="#E74C3C" clip-path="url(#halfClip)" />objectBoundingBox 适合做"裁掉左半边/下半边"这类比例裁切,不用算具体像素。clip-rule 属性clip-rule 控制路径内部的填充判定规则,和 SVG 的 fill-rule 一致:nonzero(默认):非零环绕规则,适合普通形状evenodd:奇偶规则,适合有镂空的复合形状<defs> <clipPath id="ringClip"> <!-- evenodd 让环形区域被裁剪保留,内部圆被镂空 --> <path clip-rule="evenodd" d="M100,20 A80,80 0 1,1 99.9,20 Z M100,50 A50,50 0 1,0 99.9,50 Z" /> </clipPath></defs><rect width="200" height="200" fill="#9B59B6" clip-path="url(#ringClip)" />用文字做裁剪路径把文字作为 clipPath 的形状,可以让背景图片或渐变只在文字轮廓内显示,这种效果在 banner 设计中很常见。<svg width="400" height="120"> <defs> <linearGradient id="textGrad" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" stop-color="#FF6B6B" /> <stop offset="100%" stop-color="#4ECDC4" /> </linearGradient> <clipPath id="textClip"> <text x="200" y="85" font-size="72" font-weight="bold" text-anchor="middle">SVG</text> </clipPath> </defs> <rect width="400" height="120" fill="url(#textGrad)" clip-path="url(#textClip)" /></svg>裁剪图片实现圆形头像<svg width="160" height="160"> <defs> <clipPath id="avatarClip"> <circle cx="80" cy="80" r="76" /> </clipPath> </defs> <image href="/avatar.jpg" x="0" y="0" width="160" height="160" clip-path="url(#avatarClip)" /></svg>mask:透明度遮罩mask 和 clipPath 最大的区别在于:mask 不是非黑即白的,它用灰度值控制透明度。白色区域完全显示,黑色区域完全隐藏,中间灰度值对应半透明。基本语法<svg width="200" height="200"> <defs> <mask id="holeMask"> <rect width="200" height="200" fill="white" /> <circle cx="100" cy="100" r="50" fill="black" /> </mask> </defs> <rect width="200" height="200" fill="#4A90D9" mask="url(#holeMask)" /></svg>白色背景 + 黑色圆 = 蓝色矩形中间被挖了一个圆洞。mask-type 属性:luminance 还是 alphamask-type 决定了 mask 的计算方式:luminance(默认):根据颜色的亮度值计算透明度。白色=不透明,黑色=透明,灰色=半透明alpha:直接使用颜色的 alpha 通道,忽略颜色本身<defs> <!-- alpha 模式:只看 alpha 通道,颜色不重要 --> <mask id="alphaMask" mask-type="alpha"> <rect width="200" height="200" fill="rgba(255,0,0,1)" /> <circle cx="100" cy="100" r="60" fill="rgba(0,0,0,0)" /> </mask></defs>CSS 中对应的是 mask-mode 属性(match-source | luminance | alpha)。实际开发中 alpha 模式更直觉——直接设置 rgba 的透明度就好,不用去算灰度。maskUnits 和 maskContentUnits这是两个容易混淆的属性:maskUnits(默认 objectBoundingBox):控制 mask 元素自身定位框的坐标系。决定 mask 的 x、y、width、height 参照什么maskContentUnits(默认 userSpaceOnUse):控制 mask 内部子元素的坐标系。决定你画的那些 rect、circle 的坐标参照什么<defs> <!-- mask 自身定位按用户坐标,内容也按用户坐标 --> <mask id="m1" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse"> <rect x="0" y="0" width="200" height="200" fill="white" /> </mask></defs>大多数场景下用默认值就够了,只有在需要 mask 跟随元素尺寸自动缩放时才需要调整。渐变蒙版实现淡出效果这是 mask 最典型的应用场景——让元素从一侧渐变消失,clipPath 做不到这种软边缘。<svg width="300" height="100"> <defs> <linearGradient id="fadeGrad" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" stop-color="white" /> <stop offset="100%" stop-color="black" /> </linearGradient> <mask id="fadeMask"> <rect width="300" height="100" fill="url(#fadeGrad)" /> </mask> </defs> <rect width="300" height="100" fill="#2ECC71" mask="url(#fadeMask)" /></svg>用径向渐变做聚光灯效果<svg width="300" height="200"> <defs> <radialGradient id="spotGrad" cx="50%" cy="50%" r="50%"> <stop offset="0%" stop-color="white" /> <stop offset="100%" stop-color="black" /> </radialGradient> <mask id="spotMask"> <rect width="300" height="200" fill="url(#spotGrad)" /> </mask> </defs> <image href="/scene.jpg" width="300" height="200" mask="url(#spotMask)" /></svg>clipPath 和 mask 的核心区别| 对比项 | clipPath | mask ||--------|----------|------|| 裁切方式 | 硬边缘,非显即隐 | 支持透明度渐变 || 透明度控制 | 无 | 白=显示,黑=隐藏,灰=半透明 || 事件响应 | 裁剪区域外不响应事件 | 遮罩透明区域仍可响应事件 || 性能 | 更好,计算简单 | 较重,需要逐像素计算 || 典型场景 | 形状裁剪、头像、镂空文字 | 渐变淡出、聚光灯、阴影遮罩 |面试时记住一句话:clipPath 是剪刀,mask 是滤镜。剪刀只有剪和不剪,滤镜可以调透明度。clipPath 能否嵌套?mask 能否叠加?clipPath 嵌套:一个元素只能有一个 clip-path,但 clipPath 内部可以放多个形状,取并集作为裁剪区域。如果需要交集裁剪,可以把裁剪结果包一层 group 再裁剪。mask 叠加:一个元素只能有一个 mask,多个 mask 需要手动合并到同一个 <mask> 元素内,通过叠加绘制实现组合效果。CSS clip-path 与 SVG clipPath 的联动CSS 的 clip-path 属性可以直接引用 SVG 中定义的 clipPath,这让 SVG 裁剪能作用于普通 HTML 元素。<!-- 定义 SVG 裁剪路径 --><svg width="0" height="0"> <defs> <clipPath id="starClip"> <polygon points="50,5 20,95 95,35 5,35 80,95" /> </clipPath> </defs></svg><!-- 在 HTML 元素上引用 --><div style="width:200px;height:200px;background:#E74C3C;clip-path:url(#starClip)"></div>CSS 也支持直接使用基本形状函数,不需要定义 SVG:.avatar { clip-path: circle(50%);}.banner { clip-path: polygon(0 0, 100% 0, 100% 80%, 0 100%);}但自定义复杂路径仍然需要 SVG clipPath。两者配合使用是最灵活的方案。CSS mask 属性同样可以引用 SVG mask,还可以用图片做遮罩:.card { mask-image: linear-gradient(to bottom, white, transparent); -webkit-mask-image: linear-gradient(to bottom, white, transparent);}组合使用:圆形裁剪 + 渐变淡出<svg width="200" height="200"> <defs> <clipPath id="circleClip"> <circle cx="100" cy="100" r="80" /> </clipPath> <linearGradient id="maskGrad" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" stop-color="white" /> <stop offset="100%" stop-color="black" /> </linearGradient> <mask id="fadeMask"> <rect width="200" height="200" fill="url(#maskGrad)" /> </mask> </defs> <rect width="200" height="200" fill="#4A90D9" clip-path="url(#circleClip)" mask="url(#fadeMask)" /></svg>先用 clipPath 把矩形裁成圆形,再用 mask 让圆形从左到右渐变消失。动态裁剪与动画通过 JavaScript 修改 clipPath 内元素的属性,可以实现动画效果:<svg width="200" height="200"> <defs> <clipPath id="dynamicClip"> <circle id="clipCircle" cx="100" cy="100" r="50" /> </clipPath> </defs> <rect width="200" height="200" fill="#4A90D9" clip-path="url(#dynamicClip)" /></svg><script>const circle = document.getElementById('clipCircle');let r = 50, growing = true;function animate() { r += growing ? 0.5 : -0.5; if (r >= 80) growing = false; if (r <= 30) growing = true; circle.setAttribute('r', r); requestAnimationFrame(animate);}animate();</script>如果只需要 CSS 动画,也可以用 CSS clip-path 配合 @keyframes:.box { clip-path: circle(30%); animation: breathe 3s ease-in-out infinite alternate;}@keyframes breathe { to { clip-path: circle(80%); }}CSS 动画方案更轻量,适合简单的缩放裁切动画。需要路径变形等复杂动画时,才需要回到 JavaScript 操作 SVG 节点。浏览器兼容性clipPath 和 mask 在现代浏览器中支持良好,但有几个坑要注意:Firefox 对 clipPath 内使用 <use> 引用外部形状有历史 bugSafari 对 CSS mask 需要加 -webkit- 前缀clipPathUnits="objectBoundingBox" 在老版本 WebKit 中可能有精度问题CSS clip-path 作用于 HTML 元素时,Firefox 早期版本需要额外的 SVG 引用处理生产环境中建议在关键路径上做特性检测或提供降级方案,Safari 的 -webkit- 前缀别忘了加。实际业务中怎么选选型其实不复杂,记住两条原则:只要不需要透明度过渡,就用 clipPath。性能好,语义清晰,浏览器计算快。需要渐变、半透明、软边缘时,才上 mask。代价是渲染开销更大。几个典型业务场景的选型参考:用户头像裁成圆形 → clipPath,硬边缘就够了文字 banner 镂空渐变 → clipPath 做文字裁剪 + 渐变填充卡片底部淡出效果 → mask,需要从实到虚的渐变鼠标跟随聚光灯 → mask + 径向渐变斜切/波浪形分割线 → clipPath 或 CSS clip-path: polygon(),不需要半透明如果项目已经大量使用 CSS clip-path 和 mask-image,SVG 定义可以作为复杂路径的补充,两者并不冲突。
服务端阅读 05月27日 15:31

SVG 路径命令怎么用?M/C/Q/A 每个命令的语法和原理详解

SVG 路径(<path>)是 SVG 中功能最强大的绘图元素,通过 d 属性里的一组命令来描述任意形状。无论是简单直线还是复杂曲线,都可以用路径命令精确表达。本文按命令类别逐一讲解每条命令的语法、坐标规则和实际绘制效果。路径命令的基本规则路径命令由一个字母加上若干数字组成,写在 <path> 元素的 d 属性中:大写字母表示绝对坐标,数值参照 SVG 画布原点。小写字母表示相对坐标,数值参照当前画笔位置(即上一条命令的终点)。连续使用同一命令时,命令字母可省略,只写参数。命令之间的空格和逗号可以省略,但保留空格有助于可读性。可以把路径命令想象成一支虚拟画笔:M 把笔尖移到某个位置但不落笔,L、C、A 等命令则从当前位置画线到新位置,Z 把笔尖拉回起点闭合路径。移动命令 M / mM 是每条路径的起点,把画笔移动到指定坐标,不产生线条。M 50 50这表示将画笔移到绝对坐标 (50, 50)。小写 m 10 0 则表示从当前位置向右移动 10 个单位。路径必须以 M 或 m 开头,否则浏览器无法确定起始位置。直线命令 L / H / VL —— 画直线到指定点L x y 从当前位置画一条直线到 (x, y),是最常用的画线命令。M 10 10 L 90 90这条路径从 (10, 10) 画直线到 (90, 90)。小写 l dx dy 表示相对偏移,l 80 80 与上面的效果相同。H / V —— 画水平线或垂直线当只需要沿一个轴画线时,用 H 或 V 比 L 更简洁:H x:画水平线到 x 坐标,y 坐标不变。V y:画垂直线到 y 坐标,x 坐标不变。M 10 10 H 90 V 90 H 10 Z这个路径画了一个矩形:从 (10,10) 水平画到 (90,10),垂直画到 (90,90),水平画到 (10,90),最后 Z 闭合回起点。三次贝塞尔曲线 C / SC —— 两个控制点的曲线三次贝塞尔曲线是路径中最常用的曲线命令,语法为:C x1 y1, x2 y2, x y它需要两个控制点 (x1, y1) 和 (x2, y2) 以及一个终点 (x, y)。起点是上一条命令的终点。控制点不在曲线上,它们像磁铁一样把曲线拉向自己的方向:第一个控制点 (x1, y1) 决定曲线离开起点时的切线方向。第二个控制点 (x2, y2) 决定曲线进入终点时的切线方向。M 10 80 C 40 10, 65 10, 95 80这条曲线从 (10, 80) 出发,被第一个控制点 (40, 10) 向左上方拉,又被第二个控制点 (65, 10) 从右上方拉向终点 (95, 80),形成一个 S 形弧线。S —— 平滑三次贝塞尔曲线当需要连续画多段曲线并保持衔接处平滑时,用 S 可以省略一个控制点:S x2 y2, x yS 会自动将前一段曲线的第二个控制点关于当前起点做对称,作为本段曲线的第一个控制点。这样就保证了连接处的切线方向一致,曲线不会出现尖角。M 10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80第二段曲线的第一个控制点自动取 (125, 150)——即前一段第二控制点 (65, 10) 关于 (95, 80) 的对称点。如果 S 不是跟在 C 或 S 后面,它的第一个控制点会和起点重合,退化为二次贝塞尔曲线的效果。二次贝塞尔曲线 Q / TQ —— 一个控制点的曲线二次贝塞尔曲线只需一个控制点,语法更简单:Q x1 y1, x y控制点 (x1, y1) 同时影响起点和终点的切线方向。曲线弯曲程度比三次贝塞尔曲线弱,适合画简单的弧线。M 10 80 Q 95 10, 180 80T —— 平滑二次贝塞尔曲线和 S 的思路一致,T 自动推算控制点:T x yT 会取前一段 Q 或 T 的控制点关于当前起点的对称点作为新的控制点,保证平滑衔接。M 10 80 Q 95 10, 180 80 T 350 80如果 T 不是跟在 Q 或 T 后面使用,控制点会和终点重合,画出直线。椭圆弧命令 A椭圆弧是路径命令中最复杂的一个,语法为:A rx ry x-axis-rotation large-arc-flag sweep-flag x y七个参数的含义:| 参数 | 含义 ||------|------|| rx, ry | 椭圆的 x 方向和 y 方向半径 || x-axis-rotation | 椭圆的旋转角度(度数) || large-arc-flag | 0 画小弧(小于 180°),1 画大弧(大于 180°) || sweep-flag | 0 逆时针方向,1 顺时针方向 || x, y | 弧线终点坐标 |理解椭圆弧的关键在于:给定起点、终点和椭圆半径,实际上存在四条可能的弧线。large-arc-flag 决定选大弧还是小弧,sweep-flag 决定画弧的方向,两个标志组合起来唯一确定一条弧线。M 80 80 A 45 45 0 0 0 125 125这条弧线以 45×45 的圆(rx=ry 即为圆弧),不旋转,选择小弧、逆时针方向,从 (80,80) 画到 (125,125)。如果把 large-arc-flag 改为 1,会画出同一起终点之间大于 180° 的那段弧:M 80 80 A 45 45 0 1 0 125 125闭合路径 ZZ 或 z 从当前位置画一条直线回到路径起点,闭合整条路径。大小写效果相同。M 10 10 L 90 10 L 50 90 Z这画了一个三角形,Z 自动从 (50, 90) 连回 (10, 10)。闭合路径对于绘制填充图形(fill 不为 none)尤为重要——未闭合的路径在填充时浏览器会自动补一条闭合线,但可能出现渲染差异,建议显式闭合。命令速查表| 命令 | 名称 | 参数 | 说明 ||------|------|------|------|| M/m | 移动 | x y | 移动画笔,不画线 || L/l | 直线 | x y | 画直线到指定点 || H/h | 水平线 | x | 画水平线 || V/v | 垂直线 | y | 画垂直线 || C/c | 三次贝塞尔 | x1 y1, x2 y2, x y | 两个控制点的曲线 || S/s | 平滑三次贝塞尔 | x2 y2, x y | 自动推算第一控制点 || Q/q | 二次贝塞尔 | x1 y1, x y | 一个控制点的曲线 || T/t | 平滑二次贝塞尔 | x y | 自动推算控制点 || A/a | 椭圆弧 | rx ry rot large sweep x y | 椭圆弧线 || Z/z | 闭合 | 无 | 回到起点 |常见问题相对坐标什么时候用? 当路径需要平移或复用时,相对坐标更方便——只需改 M 的起点,后续命令自动跟随。手写简单图形时绝对坐标更直观。S 和 T 在什么时候会退化? 如果 S 不是紧跟 C 或 S,第一个控制点会与起点重合;T 不是紧跟 Q 或 T 时同理,画出来是直线。弧线的 large-arc-flag 和 sweep-flag 怎么选? 先确定需要大弧还是小弧(看弧线是否超过半圆),再确定绘制方向。两个标志各有两种取值,共四种组合,只有一种符合你要的弧线。
服务端阅读 05月27日 15:28

前端面试常问:SVG 怎么做才能让屏幕阅读器也能看懂?

做前端的同学对 SVG 肯定不陌生——图标、图表、动画,哪哪都是它。但面试官一问"SVG 的可访问性怎么做",很多人就卡壳了。这块确实容易被忽略,毕竟视觉上看着没问题就行,谁会去想屏幕阅读器怎么读它?但 WCAG 合规已经在很多地区变成法规要求,不理解这块真说不过去。## 先搞清楚问题在哪SVG 默认对辅助技术不太友好。一个 <svg> 标签丢在页面上,屏幕阅读器可能直接跳过,也可能报一串乱七八糟的路径数据——总之用户体验很糟糕。核心问题就三个:没描述、没角色、没键盘支持。挨个解决就行。## 给 SVG 加上文字描述最基础的做法是在 SVG 内部放 <title> 和 <desc> 元素,然后通过 aria-labelledby 关联上去:svg<svg width="200" height="200" role="img" aria-labelledby="chart-title chart-desc"> <title id="chart-title">季度销售柱状图</title> <desc id="chart-desc">显示2024年四个季度的销售数据,Q1为100万,Q2为150万,Q3为120万,Q4为180万</desc> <rect x="20" y="80" width="40" height="100" fill="blue" /> <rect x="80" y="50" width="40" height="130" fill="green" /></svg>````<title>` 写简要名称,`<desc>` 写详细说明。屏幕阅读器会先读标题再读描述,用户就能理解这张图在讲什么。如果 SVG 是通过 `<img>` 引入的,直接写 `alt` 属性就行:`<img src="chart.svg" alt="2024年季度销售柱状图">`。有个常见的坑:有些人会同时写 `aria-labelledby` 和 `aria-label`,觉得双保险。实际上 `aria-label` 优先级更高,会把 `title` 和 `desc` 的内容直接覆盖掉,白写了。二选一就好。## 用 ARIA 角色告诉辅助技术"这是什么东西"SVG 元素本身没有明确的语义角色,需要我们手动指定。常用的就两种场景:**信息性 SVG**(图标、图表、插图)用 `role="img"`:svg **纯装饰性 SVG**(背景花纹、分隔线装饰)用 `role="presentation"` 加 `aria-hidden="true"`:svg 装饰性 SVG 千万别加描述,否则屏幕阅读器会读出一堆无意义的内容,反而干扰用户。这个在 WebAIM 的年度调查里是高频错误——很多页面上几十个装饰图标全被读出来,用户听得一头雾水。## 交互式 SVG 必须支持键盘如果 SVG 有点击、拖拽等交互,就必须让键盘用户也能操作。核心就两步:让它可聚焦,让它可触发。**可聚焦**用 `tabindex="0"`:svg **可触发**就是监听 `keydown` 事件,处理 Enter 和空格键:jsconst svg = document.querySelector('svg[role="button"]');svg.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); // 执行交互逻辑 }});焦点样式也别忘了,否则键盘用户根本不知道当前焦点在哪:csssvg[tabindex]:focus { outline: 3px solid #005fcc; outline-offset: 2px;}一个更推荐的做法是直接在 SVG 内部嵌套原生 `<button>` 或 `<a>` 元素,它们自带键盘行为和 ARIA 语义,省去不少额外代码。## 颜色对比度和信息传达WCAG 2.1 要求文本和图形的对比度至少达到 4.5:1(普通文本)或 3:1(大文本和图形元素)。SVG 里的颜色也得遵守这个标准。但比对比度更容易踩坑的是:只用颜色传达信息。比如图表里红绿两色分别代表增长和下降,色盲用户完全分不清。正确做法是加上形状、纹理或文字标签作为第二重区分:css.trend-up { fill: #2e7d32; stroke: #000; stroke-width: 1; stroke-dasharray: none; /* 实线 = 增长 /}.trend-down { fill: #c62828; stroke: #000; stroke-width: 1; stroke-dasharray: 4 2; / 虚线 = 下降 */}## 复杂图表的语义化处理简单图标加个 `aria-label` 就够了,但复杂图表(比如折线图、饼图)光靠一段文字描述很难说清楚。这时候要用分组和角色来构建语义结构:svg 月度销售趋势 折线图显示1月到6月销售持续增长 用 `role="list"` 和 `role="listitem"` 把数据点组织成列表,屏幕阅读器会逐个播报每个数据点的含义,比一段笼统的描述强得多。## 响应式文本也别忽略SVG 里的文字要保证放大后依然可读。用 `viewBox` 配合百分比宽度就行:svg 可读的文本 ``关键是viewBox要设,width用100%,height用auto`。这样用户放大页面时文字跟着缩放,不会出现溢出或截断。## 测试才是最终的检验标准代码写得再规范,不上屏幕阅读器跑一遍心里都没底。常见测试组合:- macOS: VoiceOver(按 Cmd+F5 开启)- Windows: NVDA(免费)或 JAWS- 移动端: iOS VoiceOver / Android TalkBack重点关注这几个场景:装饰性 SVG 是否被正确跳过、信息性 SVG 的描述是否准确完整、交互式 SVG 能否用键盘正常操作。自动化工具如 Lighthouse 和 axe 能扫出大部分基础问题,但语义是否准确还得人工验证。面试里被问到 SVG 可访问性,按照"描述 → 角色 → 键盘 → 对比度 → 测试"这个思路答,基本就覆盖了核心考点。实际项目里记得把这些实践落实到组件库和代码规范中,别让可访问性变成上线前才补的债。
服务端阅读 05月27日 14:41

SVG 中的文字怎么排版?text、tspan、textPath 各自解决什么问题

网页里的 SVG 图标大家都用过,但一提到 SVG 里放文字,很多人就犯难:换行怎么做?沿曲线排列怎么搞?中文字体加载怎么保证?这几个问题背后,对应的是 SVG 文本体系里三个核心元素——<text>、<tspan>、<textPath>,以及一整套定位和对齐属性。理解它们的分工,SVG 文字排版就不再靠猜。text:SVG 文本的基础容器<text> 是 SVG 里唯一原生的文本渲染元素。它和 HTML 里的文本最大区别是——不会自动换行。你写多少字符,它就渲染成一行,超出的部分直接溢出容器。<svg width="400" height="60"> <text x="20" y="40" font-size="24" fill="#333">这段文字不会自动换行</text></svg>定位属性:x / y 与 dx / dyx 和 y 是绝对坐标,指定文本起始点在 SVG 画布上的位置。注意 y 指的是文字基线(baseline)的纵坐标,不是文字顶部,所以新手经常会发现文字比预期位置偏下——这是基线定位导致的。dx 和 dy 是相对偏移,从"当前文本位置"出发做增量。在 <text> 上单独使用时,效果和 x/y 类似,但在 <tspan> 里配合使用时,才是它真正的价值所在——后面会展开。rotate:逐字符旋转rotate 接受一组角度值,按顺序分配给每个字符:<text x="20" y="40" rotate="0 10 20 30 40 50">ROTATE</text>每个字母会被旋转对应的度数,适合做装饰性的文字效果。如果值的数量少于字符数,最后一个值会重复应用到剩余字符。textLength 与 lengthAdjust:强行伸缩文本textLength 让你指定文本渲染后的目标宽度,lengthAdjust 控制怎么凑到这个宽度——spacing 只调间距,spacingAndGlyphs 连字形一起拉伸。用在需要精确对齐的场景,比如图表刻度标签。tspan:分行与局部样式的关键<text> 不能换行,但 <tspan> 可以模拟换行。它是 <text> 的子元素,能独立设置坐标和样式,同时保持和父 <text> 的文本流关系。用 tspan 实现多行文本<text x="20" y="30" font-size="18" fill="#333"> <tspan x="20" dy="0">第一行文字</tspan> <tspan x="20" dy="1.4em">第二行文字</tspan> <tspan x="20" dy="1.4em">第三行文字</tspan></text>这里每个 <tspan> 都重新指定了 x="20",确保每行从同一个左边距开始;dy="1.4em" 控制行距。如果不重新设置 x,后续 <tspan> 会紧跟前一个的文本末尾继续排列,而不是换行——这是很多人踩的坑。局部样式覆盖<tspan> 可以单独设置颜色、字号、字重等,不影响兄弟节点:<text x="20" y="40" font-size="16" fill="#333"> 普通 text 里 <tspan fill="red" font-weight="bold">红色加粗的部分</tspan> 恢复普通样式</text>dx / dy 在 tspan 中的妙用在 <tspan> 上用 dx/dy 是基于前一个字符位置的偏移,适合做下标、上标或微调间距:<text x="20" y="40" font-size="20"> H<tspan dy="5" font-size="14">2</tspan><tspan dy="-5">O</tspan></svg>注意 dy 是累积的,第二个 <tspan> 需要 dy="-5" 把基线拉回来,否则后面的文字会一直偏移下去。textPath:沿路径排列文字<textPath> 让文字沿着任意 SVG 路径排列,这是 SVG 文本最独特的能力——HTML CSS 做不到这件事。<svg width="300" height="150"> <defs> <path id="curve" d="M 30,100 C 80,20 220,20 270,100" fill="none" /> </defs> <text font-size="16" fill="#333"> <textPath href="#curve">文字沿曲线排列的效果</textPath> </text></svg>startOffset:控制起始位置startOffset 决定文本从路径的哪个位置开始排列,支持百分比和绝对长度:<textPath href="#curve" startOffset="50%">从路径中间开始</textPath>配合 text-anchor 可以实现居中对齐——startOffset="50%" + text-anchor="middle" 是最常用的居中方案。method 与 spacing 属性method="align"(默认):每个字形独立对齐到路径,字符间距不均匀。method="stretch":字形会被拉伸以贴合路径曲率,间距更均匀但字形可能变形。spacing="auto":浏览器自动调整间距;spacing="exact":严格按字符宽度计算,曲率大的地方可能出现重叠。路径方向与文字朝向路径的绘制方向决定了文字的朝向。如果路径从右向左画,文字就会倒过来。遇到这种情况,要么调整路径方向,要么对文字加 transform="scale(1,-1)" 翻转。文本对齐:text-anchor 与 dominant-baselineSVG 文本的对齐控制比 HTML 更细粒度,但也更容易让人困惑。text-anchor:水平对齐text-anchor 决定 x 坐标对应文本的哪个位置:start(默认):x 是文本左端middle:x 是文本中心end:x 是文本右端做居中对齐时,text-anchor="middle" 配合 x="50%" 比手动算坐标简单得多。dominant-baseline:垂直对齐dominant-baseline 控制 y 坐标对应文本的哪条基线,常用值:| 值 | 效果 ||---|---|| auto | 默认,通常等同于 alphabetic || alphabetic | y 对齐到西文字母底部基线 || middle | y 对齐到文字垂直中心 || hanging | y 对齐到悬挂基线(印度语系常用) || central | y 对齐到 em 方框中心 || ideographic | y 对齐到表意文字底部 |最容易踩的坑:默认 alphabetic 基线下,中文文字看起来会比预期偏下。在圆形中心放文字时,用 dominant-baseline="central" + text-anchor="middle" 是最靠谱的组合。alignment-baseline 与 baseline-shiftalignment-baseline 控制 <tspan> 相对父元素的基线对齐方式,baseline-shift 做上下偏移——用来做上标下标很方便。不过 baseline-shift 正在被 CSS vertical-align 替代,新项目建议直接用 CSS。字体引用:@font-face 与 foreignObjectSVG 里的字体加载和 HTML 共享同一套机制,但有细微差别。@font-face 在 SVG 中的使用在 HTML 页面里内联的 SVG,直接使用页面的 @font-face 声明即可,字体加载没有额外问题。但如果 SVG 作为 <img> 标签引用,浏览器出于安全限制会阻止加载外部字体——这是最常见的坑。解决方案:把字体文件转成 Base64 嵌入 SVG 内部的 <style> 中:<svg xmlns="http://www.w3.org/2000/svg"> <style> @font-face { font-family: 'CustomFont'; src: url('data:font/woff2;base64,...') format('woff2'); } text { font-family: 'CustomFont', sans-serif; } </style> <text x="20" y="40" font-size="24">自定义字体文本</text></svg>foreignObject 引入 HTML 文本<foreignObject> 允许在 SVG 中嵌入 HTML 片段,从而直接使用 CSS 的 word-wrap、line-height 等属性实现自动换行:<foreignObject x="20" y="20" width="300" height="200"> <div xmlns="http://www.w3.org/1999/xhtml" style="font-size:16px; line-height:1.6;"> 这段文字可以自动换行,支持完整的 CSS 排版能力。 </div></foreignObject>但 <foreignObject> 有明显限制:作为 <img> 引用的 SVG 不支持 <foreignObject>跨浏览器渲染差异大,Safari 尤其容易出问题导出为 PNG/SVG 图片时,<foreignObject> 内容经常丢失所以它更适合内联在 HTML 页面中的 SVG,不适合导出场景。多行文本的策略选择SVG 文本不自动换行,多行文本需要根据场景选方案:| 方案 | 适用场景 | 优点 | 缺点 ||---|---|---|---|| 多个 <tspan> + dy | 固定行数的标签、标题 | 纯 SVG,兼容性好 | 需手动分行,不能自动换行 || 多个 <text> 元素 | 需要独立定位的多段文字 | 每行独立控制 | 不在同一文本流中 || <foreignObject> + HTML | 长文本、需要自动换行 | CSS 排版能力强 | 不支持导出,兼容性差 || JavaScript 动态拆行 | 不确定文本长度、需要导出 | 自动化,兼容性好 | 实现复杂 |对于图表标签、图例这类固定短文本,<tspan> 方案最稳妥。对于用户输入的长文本,要么用 <foreignObject>(仅限内联 SVG),要么用 JS 按字符宽度计算拆行点。文本选择与复制SVG 文本默认可以被鼠标选中并复制,前提是 SVG 内联在 HTML 中。但实际体验比 HTML 文本差很多:选区高亮经常和文字位置对不上,尤其在有 transform 的情况下<textPath> 里的文字选择体验最差,选区是沿路径弯曲的,但复制出来的文本是正常的跨 <tspan> 的选择在某些浏览器中会中断作为 <img> 或 CSS background-image 引用的 SVG,文本完全不可选如果需要保证文本可复制,SVG 必须内联到 HTML 中,且避免复杂的 transform 变换。可访问性SVG 文本的可访问性比 HTML 差一截,但可以补救:<svg role="img" aria-labelledby="chart-title chart-desc"> <title id="chart-title">月度销售趋势</title> <desc id="chart-desc">1月至6月的销售数据折线图,整体呈上升趋势</desc> <!-- 图表内容 --> <text x="20" y="40">1月</text></svg>关键做法:<svg> 加 role="img" 和 aria-labelledby,引用内部的 <title> 和 <desc>装饰性文字加 aria-hidden="true" 防止屏幕阅读器重复朗读屏幕阅读器对 SVG 内 <text> 的支持不一致,JAWS 只读 aria-labelledby 指向的内容,NVDA 的行为不稳定如果文字信息很重要,在 SVG 外部用 HTML 提供一份完整的文字描述是最安全的做法中文字体处理中文字体文件动辄数 MB,在 SVG 中使用有几个特殊问题:字体子集化用 fonttools 或 pyftsubset 只提取用到的字符,把字体文件从几 MB 压缩到几十 KB:pyftsubset NotoSansSC-Regular.ttf --text-file=chars.txt --output-file=NotoSansSC-subset.woff2 --flavor=woff2导出 SVG 图片时,子集化几乎是必须的,否则要么字体加载失败,要么文件体积爆炸。SVG 作为图片引用时的中文字体问题作为 <img> 标签引用时,SVG 无法加载外部字体,中文字符会回退到浏览器默认字体。两种解法:Base64 嵌入子集字体:把子集化后的字体 Base64 编码写进 SVG 的 <style>,兼容性最好文字转路径:用设计工具或 text2path 工具把文字轮廓转为 <path>,彻底消除字体依赖,但文本不再可选、不可编辑、文件体积也会增大最小字号问题Chrome 在非 Retina 屏幕下会强制将小于 12px 的中文字体渲染为 12px,这在 SVG 里同样存在。如果确实需要更小的中文字,可以用 transform="scale(0.8)" 配合较大的 font-size 来绕过,但会牺牲清晰度。各属性的浏览器兼容性速查| 属性/元素 | Chrome | Firefox | Safari | 备注 ||---|---|---|---|---|| <text> 基础属性 | 全支持 | 全支持 | 全支持 | — || <tspan> | 全支持 | 全支持 | 全支持 | — || <textPath> | 全支持 | 全支持 | 全支持 | href 替代 xlink:href || textLength / lengthAdjust | 支持 | 支持 | 部分 | Safari 对 lengthAdjust 支持不完整 || dominant-baseline | 支持 | 支持 | 部分缺失 | Safari 某些值不生效 || <foreignObject> | 支持 | 支持 | 有限制 | 导出场景不可靠 |SVG 文本排版看起来属性多、坑不少,但理清 text(定位容器)、tspan(分行与局部样式)、textPath(路径排列)三者的分工,再掌握 text-anchor/dominant-baseline 对齐、字体加载策略和中文字体处理,大部分排版需求都能应对。遇到自动换行需求时,先确认 SVG 是内联还是导出场景——这决定了你能用 <foreignObject> 还是必须回到 <tspan> 手动分行。
服务端阅读 05月27日 14:40

SVG 有哪些基本形状元素,它们各自的属性和用法是什么

为什么需要了解 SVG 基本形状用 CSS 画一个圆角矩形要写一堆 border-radius,用 Canvas 画一个多边形要手动管理路径状态。SVG 不同——它为常见图形提供了专门的元素,写法直观,浏览器直接渲染,还能无损缩放。理解这六个基本形状和 path 元素,是用好 SVG 的前提。六种基本形状rect:矩形<rect> 画矩形,是最常用的形状元素之一。核心属性:x / y:矩形左上角的坐标,默认 0width / height:宽和高rx / ry:圆角半径<rect x="10" y="10" width="200" height="100" rx="8" ry="8" fill="#4A90D9" stroke="#2C5F8A" stroke-width="2"/>关于 rx 和 ry 有几个要点:只设 rx 时,ry 默认等于 rx,四个角均匀圆角;同时设置 rx 和 ry 可以分别控制水平和垂直方向的圆角弧度,形成椭圆角;如果 rx + rx > width,浏览器会自动按比例缩小半径,不会出错。当 rx 等于 width/2、ry 等于 height/2 时,矩形退化为椭圆。circle:圆形<circle> 画圆,只需要圆心和半径。核心属性:cx / cy:圆心坐标,默认 0r:半径<circle cx="100" cy="100" r="50" fill="#E74C3C" stroke="#C0392B" stroke-width="2"/>圆形没有宽高概念,尺寸完全由 r 决定。注意 cx/cy 默认是 0,如果不设置,圆心会落在 SVG 画布左上角,大部分圆会显示不全。ellipse:椭圆<ellipse> 是 circle 的扩展版,x 和 y 方向的半径可以不同。核心属性:cx / cy:圆心坐标rx:x 轴半径ry:y 轴半径<ellipse cx="150" cy="80" rx="120" ry="50" fill="#2ECC71" stroke="#27AE60" stroke-width="2"/>当 rx === ry 时,椭圆就是圆。椭圆常用于按钮背景、进度条轨道等需要水平拉伸的场景。line:直线<line> 画一条线段,从 (x1,y1) 到 (x2,y2)。核心属性:x1 / y1:起点坐标x2 / y2:终点坐标<line x1="0" y1="0" x2="200" y2="150" stroke="#8E44AD" stroke-width="3"/>直线没有填充,只有描边。如果忘记写 stroke,线段不可见——这是初学者最常踩的坑。polyline:折线<polyline> 画一系列连续线段,不自动闭合。核心属性:points:点序列,格式为 "x1,y1 x2,y2 x3,y3 …"<polyline points="10,80 40,20 70,60 100,10 130,50 160,30" fill="none" stroke="#F39C12" stroke-width="2"/>折线默认有填充,如果不想要填充效果需要显式写 fill="none",否则浏览器会按"首尾连线形成封闭区域"来填充颜色,出来的效果通常不是你想要的。polygon:多边形<polygon> 和 polyline 几乎一样,区别在于最后一个点会自动连回第一个点,形成闭合图形。核心属性:points:点序列<polygon points="100,10 190,80 160,170 40,170 10,80" fill="#1ABC9C" stroke="#16A085" stroke-width="2"/>三角形、五角星、六边形等封闭图形用 polygon 比 polyline 方便,不用手动把起点写在末尾。path:终极形状元素path 是 SVG 中功能最强的元素,上面六种基本形状都能用 path 画出来,还能画贝塞尔曲线、弧线等基本形状画不了的图形。path 的核心是 d 属性(data 的缩写),里面是一串命令序列,每条命令由字母加参数组成。d 属性的命令体系大小写有区别:大写字母用绝对坐标,小写字母用相对坐标(相对于上一个命令的终点)。移动与直线| 命令 | 含义 | 参数 ||------|------|------|| M | 移动画笔到指定位置 | x,y || L | 画直线到指定位置 | x,y || H | 画水平线 | x || V | 画垂直线 | y || Z | 闭合路径,回到起点 | 无 |用 M + L + Z 就能画多边形:<path d="M 100,10 L 190,80 L 160,170 L 40,170 L 10,80 Z" fill="#1ABC9C"/>这和上面 polygon 的 points 五边形效果完全相同。曲线| 命令 | 含义 | 参数 ||------|------|------|| Q | 二次贝塞尔曲线 | 控制点x,y 终点x,y || T | 平滑二次贝塞尔(自动镜像上一个控制点) | 终点x,y || C | 三次贝塞尔曲线 | 控制点1x,y 控制点2x,y 终点x,y || S | 平滑三次贝塞尔(自动镜像上一个控制点2) | 控制点2x,y 终点x,y |三次贝塞尔示例:<path d="M 10,80 C 40,10 160,10 190,80" fill="none" stroke="#E74C3C" stroke-width="3"/>两个控制点(40,10)和(160,10)把曲线往上方拉,形成一条向上拱起的弧线。S 命令省略第一个控制点,浏览器自动取上一个 C/S 的第二控制点关于当前点的镜像,用来画连续光滑曲线很方便。弧线| 命令 | 含义 | 参数 ||------|------|------|| A | 椭圆弧线 | rx ry x-rotation large-arc-flag sweep-flag x,y |A 命令参数最多,拆解一下:rx, ry:椭圆的 x 和 y 半径x-rotation:椭圆的旋转角度large-arc-flag:0 选小弧,1 选大弧sweep-flag:0 逆时针,1 顺时针x,y:终点坐标<path d="M 10,80 A 90,50 0 0,1 190,80" fill="none" stroke="#9B59B6" stroke-width="3"/>A 命令画圆弧时设 rx === ry 即可。它和 circle 的区别在于:A 画的是两点之间的弧段,不是完整圆。通用样式属性所有形状都共享这些样式属性:fill:填充颜色,默认黑色。设 none 不填充,设 url(#gradientId) 用渐变填充stroke:描边颜色,默认 none(不可见)stroke-width:描边宽度,默认 1stroke-linecap:线段端点样式,butt(默认)/ round / squarestroke-linejoin:折点连接样式,miter(默认)/ round / bevelstroke-dasharray:虚线模式,如 "5,3" 表示 5px 实线 3px 间隔opacity / fill-opacity / stroke-opacity:整体或分项透明度形状组合与变换g 元素分组<g> 把多个形状打包成一组,可以统一设置样式和变换:<g fill="#3498DB" stroke="#2980B9" stroke-width="2"> <rect x="10" y="10" width="60" height="40"/> <circle cx="100" cy="30" r="20"/></g>transform 变换所有形状都支持 transform 属性,常用值:translate(dx, dy):平移rotate(angle, cx, cy):旋转,角度单位为度scale(sx, sy):缩放skewX(angle) / skewY(angle):倾斜<rect x="0" y="0" width="40" height="40" transform="translate(50,50) rotate(45)" fill="#E67E22"/>变换的书写顺序影响结果——translate 在 rotate 前面意味着先平移再旋转,效果和反过来不同。从基本形状到 path 的转换在实际开发中,把基本形状转成 path 有几个常见场景:需要对形状做路径动画(如 stroke-dashoffset 描边动画)、需要做形状变形(morphing,两个 path 之间插值)、需要导出给只支持 path 的工具(如某些 CNC 切割机)。转换规则:rect 转 path:四个角用 L 或 A(有圆角时)连接。无圆角的矩形 M x,y H x+w V y+h H x Z;有圆角的需要在四个角用 A 命令画弧circle 转 path:用两个 A 命令拼成完整圆。M cx+r,cy A r,r 0 1,1 cx-r,cy A r,r 0 1,1 cx+r,cy Zellipse 转 path:同 circle,把 r 换成 rx/ryline 转 path:M x1,y1 L x2,y2polyline 转 path:M 到第一个点,然后 L 到后续每个点polygon 转 path:同 polyline,末尾加 Z前端可以用 Jarek Foksa 的 path-data polyfill 或在线工具(如 SVG Shape to Path Converter)完成批量转换。怎么选择合适的形状元素简单规则:能用基本形状就用基本形状,语义更清晰、代码更短。需要曲线或复杂图形时才用 path。圆角矩形用 rect 的 rx/ry 比用 path 手拼 A 命令简单得多。需要做路径动画或变形时,再考虑把基本形状转成 path。
服务端阅读 05月27日 14:38

SVG 在移动端开发中需要注意哪些性能和兼容性问题

移动端对 SVG 的态度一直很矛盾:它矢量缩放不失真、文件小、能交互,看起来是图标和简单图形的理想选择。但实际落地时,渲染卡顿、内存溢出、Android 4.x 白屏、React Native 里性能断崖——这些问题足以让团队在技术选型时犹豫。这篇文章把移动端 SVG 开发中真正会遇到的坑和对应的解法梳理清楚。移动端 SVG 渲染性能的瓶颈在哪SVG 是基于 XML 的矢量格式,浏览器和 WebView 需要解析 DOM、计算路径、光栅化后再绘制。这个流程在桌面端几乎无感,但在移动端有三个明显的性能瓶颈。路径复杂度是第一杀手。 一个包含数百个 path 命令的 SVG 图标,在 Android 上首次渲染可能需要 2-3ms,如果一屏出现几十个这样的图标,滚动时掉帧几乎是必然的。实测数据:简单图标(5-10 条路径命令)渲染时间 <0.5ms,中等图标(15-30 条)约 1ms,复杂图标(50+ 条路径)可达 3ms 甚至更高。在 60fps 的要求下,每帧预算只有 16ms,十几个复杂 SVG 就可能吃掉大半。滤镜和阴影效果是第二杀手。 <filter> 中的 feGaussianBlur、feDropShadow 在移动 GPU 上开销极大,尤其是应用在大面积元素上时。一个带模糊阴影的 SVG 在 iPhone 上可能流畅,在中低端 Android 设备上直接卡成幻灯片。移动端应尽量避免 SVG 内嵌滤镜,改用 CSS box-shadow 或 filter 属性——CSS 滤走由 GPU 合成层处理,通常比 SVG 滤镜高效。重绘和重排频率是第三杀手。 SVG 作为 DOM 节点,任何属性变化都会触发浏览器的重绘流程。频繁修改 SVG 属性(比如动画中不断改变 d 属性)在移动端性能损耗远大于使用 CSS transform 做同样的变换。原则:能用 CSS transform/opacity 实现的效果,不要去操作 SVG 的几何属性。优化策略总结:单个图标控制在 30 条路径命令以内,文件体积 <5KB用 SVGO 自动清理元数据、合并路径、简化变换对不需要交互的 SVG 元素设置 pointer-events: none,减少事件解析开销视口外的 SVG 使用懒加载,避免首屏渲染压力内存占用:SVG 并非总是更省很多人选择 SVG 的理由是"文件更小",但文件小不等于内存占用低。SVG 的内存消耗来自两个阶段:解析阶段和光栅化阶段。解析阶段,浏览器需要将 XML 文本解析为 DOM 树,复杂 SVG 的 DOM 节点可能达到数千个。光栅化阶段,浏览器将矢量图形渲染为位图缓存,缓存的位图大小取决于 SVG 的渲染尺寸而非文件大小。一个 2KB 的 SVG 图标如果渲染为 200x200dp,在 3x 设备上会生成 600x600 像素的位图,占用约 1.4MB 内存。Android 的 VectorDrawable 机制更直白:首次绘制时生成缓存位图,不同尺寸分别缓存。如果你在列表中为同一图标使用了 3 种不同尺寸,就会产生 3 份位图缓存。iOS 的 PDF 矢量资源也是类似逻辑。关键结论:图标数量多、尺寸变化多的场景,SVG 的内存总占用可能超过等价的 PNG @1x/@2x/@3x 方案。 这在低端设备上尤其明显,内存吃紧时系统会回收缓存,导致反复光栅化,形成性能恶性循环。实操建议:列表场景中大量重复的小图标,优先用 Icon Font 或雪碧图同一图标只使用一种尺寸,通过 CSS transform: scale() 调整视觉大小,减少缓存份数超过 100 个 SVG 图标的页面,用 Chrome DevTools 的 Memory 面板实测内存占用Android 4.x 兼容性:历史包袱怎么处理Android 对 SVG 的原生支持从 5.0(API 21)才开始,4.x 及以下版本完全不认识 VectorDrawable。但截至 2026 年,仍有一些 App 的 minSdkVersion 低于 21,或需要在内置 WebView 中展示 SVG 内容。VectorDrawable 的向后兼容方案Android Support Library 23.2+ 提供了 VectorDrawableCompat,支持 API 7+ 渲染矢量图。使用时有几个必须注意的点:布局中用 app:srcCompat 代替 android:src。后者在 4.x 上会直接报错,因为系统不认识矢量资源类型。构建时自动生成 PNG 回退。在 build.gradle 中配置 vectorDrawables.useSupportLibrary = true 可以禁用自动 PNG 生成,减小包体积,但前提是所有地方都用了 compat 方式加载。如果不全用 compat,就不要开这个选项。4.x 上 VectorDrawable 支持的 XML 属性有限。<vector> 只支持 width、height、viewportWidth、viewportHeight、alpha;<group> 只支持 rotation、pivotX 等。更复杂的属性(如 trimPathStart)在低版本上被静默忽略。WebView 中的 SVG 兼容性Android 4.x 的 WebView 基于旧版 Chromium,对 SVG 的支持存在不少缺陷:<use> 引用外部 SVG 文件的 xlink:href 可能无法解析;CSS 动画作用于 SVG 元素时可能闪烁;部分滤镜效果完全不渲染。如果 App 必须在 4.x WebView 中展示 SVG,最稳妥的方式是内联 SVG(inline SVG),不要用 <img> 或 <object> 引用外部文件,也不要依赖 CSS 动画驱动 SVG 变化。SVG 在 React Native 中的表现React Native 不原生支持 SVG,社区方案 react-native-svg 是事实标准。它的原理是用原生组件模拟 SVG 元素,而非通过 WebView 渲染。这意味着性能特征和 Web 环境完全不同。性能问题react-native-svg 的主要性能瓶颈在桥接开销。每个 SVG 元素(<Path>、<Circle>、<G> 等)都是一个 React Native 组件,状态更新时需要通过 Bridge 传递序列化数据。一个包含 50 个元素的 SVG,每次更新要传递 50 份 props。在动画场景下,这个开销会导致明显掉帧。社区提出的优化方案:用 SvgCss 组件代替多个独立组件。将整个 SVG 作为字符串一次性传递,减少桥接次数,性能提升显著。复杂动画场景用 react-native-skia。Skia 直接在 native 层绘制,绕过 Bridge,适合需要实时重绘的场景。代价是额外的包体积和内存开销。静态图标直接转 PNG。不涉及交互和动画的图标,在 React Native 中用 PNG 比 SVG 性能更好,渲染也更快。平台差异react-native-svg 在 iOS 和 Android 上的渲染结果可能不一致。已知问题包括:iOS 上某些 stroke 颜色设置后性能断崖式下降;Android 上复杂 clipPath 渲染异常;两个平台对 mask 和 filter 的支持程度不同。建议在两个平台上都做真机测试,不要依赖模拟器。WebView 中的 SVG 处理混合开发中经常遇到在 WebView 里展示 SVG 的需求,比如图表、地图、复杂插图。WebView 的 SVG 渲染依赖系统浏览器内核,iOS 是 WKWebView(Nitro 引擎),Android 是 Chromium 内核。iOS WebView 的坑WKWebView 对 SVG 的支持总体良好,但有几个边缘问题:跨域加载 SVG 时,CORS 策略可能阻止渲染。解决方案是将 SVG 内联或同域部署。SVG 中的 <text> 元素引用的 Web Font 如果未加载完成,会显示为系统字体回退,导致布局偏移。需要用 font-display: block 或将文本转为路径。大面积 SVG(比如全屏地图)在 WKWebView 中滚动时可能出现光栅化延迟,表现为短暂的白块。可通过 will-change: transform 提示浏览器预合成来缓解。Android WebView 的坑Android WebView 的 SVG 行为取决于系统 WebView 版本。Android 5+ 默认 Chromium 内核对 SVG 支持良好,但低版本 Android 上问题较多:<use> 元素的 href 属性需要用 xlink:href 才能兼容;SVG 动画的 SMIL 支持在 Chrome 45 后被标记为废弃;<foreignObject> 在部分国产 ROM 的 WebView 中不渲染。一个通用建议:WebView 中尽量减少 SVG 的 DOM 复杂度。如果一个页面需要渲染上百个 SVG 节点,考虑用 Canvas 替代,或者将 SVG 预渲染为 PNG 后在 WebView 中展示。触摸交互优化SVG 在移动端的交互优势在于每个子元素都可以独立响应事件,但这也带来了性能隐患。hit test 开销SVG 的子元素(path、rect、circle 等)默认都参与事件分发。浏览器在每次触摸事件时需要遍历 SVG 树做 hit testing,元素越多开销越大。一个包含 200 个 path 的交互式地图,在低端设备上点击响应可能有 100-200ms 的延迟。优化方法:对不需要交互的子元素设置 pointer-events: none,这能让 hit test 跳过它们将交互区域和视觉区域分离:视觉用复杂 SVG 渲染,交互用叠加在上层的简单透明 rect 元素用 event delegation:在 SVG 根元素上监听事件,通过 event.target 判断点击对象,避免在每个子元素上绑定事件触摸精度移动端手指触摸的精度远低于鼠标点击,SVG 的细小交互区域(如小图标、细线条)需要扩大可点击区域。做法是在交互元素外包一层透明的 rect 作为触摸热区,或者使用 CSS padding 扩大元素的可交互范围。WCAG 建议移动端可触摸区域最小 44x44 CSS 像素。SVG 字体在移动端的问题SVG 字体(.svg 格式的字体文件,通过 @font-face 引入)是字体方案中最不推荐的选择。浏览器支持已全面放弃。 Chrome 已移除 SVG 字体支持,Firefox 从未支持过,Safari 在较新版本中也已弃用。SVG 字体规范的最后一个版本停留在 2011 年的 CSS Fonts Module Level 3 草案中,此后再无更新。渲染质量差。 SVG 字体不包含 hinting 信息,在小字号下(12-16px)渲染效果明显差于 TrueType/OpenType 字体,在移动端高 DPI 屏幕上表现为笔画粗细不均、细节丢失。但 SVG 内嵌文本是另一回事。 SVG 文件中的 <text> 元素仍然被广泛支持,问题在于它引用的字体必须在目标设备上可用。移动端的系统字体与桌面端不同,font-family: Arial, sans-serif 在 iOS 上回退到 Helvetica,Android 上回退到 Roboto,可能导致排版偏移。如果 SVG 中的文本对布局精度有要求(比如 Logo),将文本转为 <path> 是最稳妥的做法。图标方案选择:SVG vs PNG vs Icon Font这是移动端项目中最常见的选型决策,三种方案各有适用场景。SVG适合: 需要多色图标、需要 CSS 控制颜色/动画、图标数量少且需要精确交互的场景。不适合: 列表中大量重复渲染的小图标、需要兼容 Android 4.x 的原生应用、对渲染帧率要求极高的滚动列表。移动端注意事项: 内联 SVG 不产生额外 HTTP 请求,但增加 HTML 体积;外部 SVG 引用有缓存优势,但首屏加载慢。图标库场景下,按需引入比全量引入更合理。PNG适合: 简单静态图标、对渲染性能要求高、需要兼容老旧设备的场景。不适合: 需要多分辨率适配(1x/2x/3x)的项目——打包体积随分辨率递增,且无法通过 CSS 改变颜色。移动端注意事项: 23 个优化过的 SVG 图标实测比同尺寸 PNG(64x64)大 60% 左右,但渲染速度快 2-3 倍。如果图标需要在大尺寸下使用(如平板),SVG 的体积优势才真正体现。Icon Font适合: 大量单色图标、需要整体缓存、图标风格统一的场景。不适合: 需要多色图标、对可访问性有要求、图标需要精确像素定位的场景。移动端注意事项: Icon Font 加载完成前会出现 FOUC(Flash of Unstyled Content),图标位置显示为空方块。可用 font-display: block 避免回退显示,但会导致文本渲染延迟。另外 Icon Font 的抗锯齿在移动端可能导致图标边缘模糊,特别是 1x 设备上。选型决策参考| 维度 | SVG | PNG | Icon Font ||------|-----|-----|-----------|| 多色支持 | 原生支持 | 支持 | 不支持 || 缩放质量 | 无损 | 有损 | 矢量但可能模糊 || 渲染速度 | 中(需光栅化) | 快 | 中 || 内存占用 | 看渲染尺寸 | 固定 | 看字体大小 || CSS 可控性 | 最强 | 无 | 颜色/大小 || 可访问性 | 好(语义标签) | 差 | 差 || 兼容性 | 现代浏览器好 | 最好 | 最好 |实际项目中,混合使用往往是最佳答案:主品牌图标用 SVG(保证质量和可控性),功能列表中的重复图标用 Icon Font(缓存和性能),照片级插图用 WebP/PNG。移动端 SVG 不是银弹,也不是禁区。关键在于理解它的渲染机制和内存模型,在正确的场景用正确的方案,在遇到性能问题时知道瓶颈出在哪一环。掌握了这些,SVG 在移动端的价值才能真正发挥出来。
服务端阅读 05月27日 14:37

SVG 在网页中有哪些使用方式,各自的优缺点是什么

为什么 SVG 的使用方式这么重要SVG 是前端开发中唯一一种"同一份资源,七八种嵌入姿势"的图片格式。选错方式,轻则图标颜色改不动、动画跑不起来,重则首屏渲染卡顿、多页面重复传输几十 KB 冗余标记。理解每种方式的边界条件,才能在具体项目中做出合理取舍。Inline SVG:直接写在 HTML 里把 <svg> 标签直接嵌入 HTML 文档,是最"裸"的用法:<svg width="24" height="24" viewBox="0 0 24 24"> <path d="M12 2L2 22h20L12 2z" fill="currentColor" /></svg>优点零额外 HTTP 请求,SVG 随 HTML 一起到达浏览器,渲染速度最快(实测约 12ms)CSS 可以直接选中 SVG 内部元素,改颜色、加 hover、写动画随心所欲JavaScript 可以读写 SVG 的 DOM,绑定事件、动态修改属性都没障碍currentColor 可以让图标继承父元素文本颜色,做主题切换非常方便缺点SVG 标记混在 HTML 里,文档体积膨胀,同一个图标在多个页面出现时会被重复传输无法被浏览器单独缓存——HTML 变了,SVG 也跟着重新下载大量内联 SVG 会拖慢 HTML 解析,阻塞首屏渲染适用场景:需要 CSS/JS 交互的图标、数量较少的关键路径图标、需要 currentColor 继承的主题图标。img 标签引用外部 SVG 文件最接近传统图片用法的姿势:<img src="icon.svg" alt="搜索" width="24" height="24">优点语法简单,和用 PNG/JPG 没有区别,学习成本为零浏览器可以缓存 SVG 文件,多页面复用时只下载一次支持 loading="lazy" 懒加载,配合 <picture> 还可以做响应式切换同一资源可以在 <img>、CSS 背景、srcset 中复用缺点CSS 无法穿透 img 边界操作 SVG 内部元素,改颜色只能换文件JavaScript 无法访问 SVG 的 DOM,交互能力为零SVG 内部的脚本和外部 CSS 不会执行SVG 内的 <style> 必须用内联样式,引用外部样式表无效适用场景:不需要交互的装饰性图标、多页面重复使用的静态图形、CMS 管理的图片资源。CSS 背景图方式把 SVG 当作装饰性背景使用:.icon-search { width: 24px; height: 24px; background: url("icon.svg") no-repeat center / contain;}也可以用 Data URI 直接嵌入:.icon-search { background: url("data:image/svg+xml,%3Csvg ...%3E%3C/svg%3E") no-repeat center / contain;}优点语义清晰,装饰性图形不该出现在 HTML 里,CSS 背景是正确位置外部文件方式支持浏览器缓存适合 background-size、background-position 等精细控制配合媒体查询可以做暗色模式切换(换背景图即可)缺点和 img 一样无法操作 SVG 内部,CSS 限定在 SVG 自身内联样式不支持 JS 交互不支持交互式 SVG 动画(SMIL 动画可以自动播放,但无法用 JS 控制)Data URI 编码会让 CSS 文件变大,Base64 编码还会额外膨胀约 33%适用场景:装饰性背景纹理、按钮/卡片的装饰图标、配合伪元素实现的小图形。object 和 embed 标签这两种方式属于"老派"做法,但在特定场景下仍有价值:<!-- object --><object data="chart.svg" type="image/svg+xml" width="400" height="300"> <p>您的浏览器不支持 SVG</p></object><!-- embed --><embed src="chart.svg" type="image/svg+xml" width="400" height="300">优点支持 JavaScript 访问 SVG 内部 DOM(通过 contentDocument 或 getSVGDocument())支持嵌入 SVG 时保留完整的交互和动画能力<object> 标签支持回退内容,SVG 不可用时显示替代文本外部文件可以被浏览器缓存缺点渲染开销比 img 大(object 约 78ms,embed 约 82ms)<embed> 是非标准标签,W3C 规范已不推荐使用跨域 SVG 可能因同源策略限制导致 JS 无法访问内部 DOM样式隔离:外部页面的 CSS 不会自动应用到 <object> 内的 SVG适用场景:需要 JS 交互但又不方便内联的复杂 SVG(如数据可视化图表),旧系统的兼容方案。SVG Sprite:symbol + use 复用图标Sprite 是管理大量图标的工程化方案,核心思路是把所有图标合并到一个 SVG 文件:<!-- sprite.svg --><svg xmlns="http://www.w3.org/2000/svg" style="display:none"> <symbol id="icon-search" viewBox="0 0 24 24"> <path d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/> </symbol> <symbol id="icon-home" viewBox="0 0 24 24"> <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/> </symbol></svg>使用时通过 <use> 引用:<!-- 内联 sprite + use --><svg class="icon"><use href="#icon-search"/></svg><!-- 外部 sprite + use --><svg class="icon"><use href="sprite.svg#icon-search"/></svg>优点所有图标合并为一个文件,只需一次 HTTP 请求内联 sprite 方式下 currentColor 可以正常继承,CSS 可以控制图标颜色<symbol> 自带 viewBox,每个图标可以有独立的视口构建工具(svgstore、svg-sprite)可以自动合并,开发时仍然独立维护每个图标缺点外部引用方式(href="sprite.svg#icon")在部分浏览器中 currentColor 继承失效,因为 Shadow DOM 隔离了样式外部引用在旧版 IE 和早期 Safari 中不支持,需要 svg4everybody 等 polyfill内联 sprite 会增加 HTML 体积,整个图标库无论用不用都会加载调试时需要通过 #id 定位具体 symbol,不如独立文件直观适用场景:图标数量在 20-100 个之间的项目,图标需要统一管理和换肤,配合构建工具自动化产出。Data URI:把 SVG 编码进 URL把 SVG 内容编码成 Data URI,可以嵌入 HTML 属性或 CSS 中:<!-- HTML img 标签 --><img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2L2 22h20z'/%3E%3C/svg%3E" alt="三角"><!-- CSS 背景使用 UTF-8 编码 -->.icon { background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10'/></svg>");}优点零额外 HTTP 请求,SVG 和 HTML/CSS 是一个整体适合只有一两个小图标的场景,省去文件管理开销UTF-8 编码比 Base64 更节省体积(Base64 会膨胀约 33%)缺点内联在 HTML 或 CSS 中无法被浏览器单独缓存Base64 编码体积膨胀且不可读,调试困难IE 不支持 Data URI 形式的 SVGSVG 内容越长,Data URI 越臃肿,超过 2-3 KB 就不值得了适用场景:极少量小图标、CSS 中需要内联简单图形、不想额外维护 SVG 文件的快速原型。iframe 方式用 iframe 加载独立 SVG 文件:<iframe src="chart.svg" width="400" height="300" title="数据图表"></iframe>优点创建完全独立的文档上下文,SVG 内部的样式和脚本与父页面完全隔离JS 可以通过 contentDocument 访问 SVG DOM(同源条件下)缺点性能开销最大(渲染约 240ms),因为每个 iframe 都会创建新的浏览上下文跨域 iframe 无法通过 JS 操作 SVG DOM布局上 iframe 尺寸不容易自适应 SVG 内容无障碍访问差,屏幕阅读器对 iframe 内 SVG 的支持不理想适用场景:需要完全样式隔离的第三方 SVG 嵌入、极少数需要沙箱化渲染的场景。大多数情况下不推荐。七种方式横向对比| 方式 | 可交互 | CSS 控制 | JS 控制 | 可缓存 | SEO 友好 | 渲染速度 | 典型体积影响 ||------|--------|----------|---------|--------|----------|----------|-------------|| Inline SVG | 完全 | 完全 | 完全 | 不可 | 好 | 最快(12ms) | HTML 膨胀 || img 标签 | 不可 | 仅内联样式 | 不可 | 可以 | 中 | 快(48ms) | 无额外影响 || CSS 背景 | 不可 | 仅内联样式 | 不可 | 可以 | 差 | 中(52ms) | CSS 膨胀 || object | 完全 | 隔离 | 受限 | 可以 | 差 | 慢(78ms) | 无额外影响 || embed | 完全 | 隔离 | 受限 | 可以 | 差 | 慢(82ms) | 无额外影响 || SVG Sprite | 完全(内联) | 完全(内联) | 完全(内联) | 外部可 | 中 | 快 | 取决于方式 || Data URI | 不可 | 仅内联样式 | 不可 | 不可 | 差 | 快 | HTML/CSS 膨胀 || iframe | 完全 | 隔离 | 受限 | 可以 | 差 | 最慢(240ms) | 无额外影响 |实际项目怎么选选择的核心逻辑只有两条:是否需要操作 SVG 内部,以及图标是否在多页面复用。不需要交互,静态展示为主——用 img 标签。缓存友好、语法简单、懒加载开箱即用,80% 的场景其实就够用了。需要改颜色、加动画、绑事件——用 Inline SVG 或内联 Sprite。currentColor 继承、CSS 动画、JS 事件这三样东西只有内联方式才能完整获得。图标多且全站复用——用外部 SVG Sprite 配合构建工具。开发时每个图标独立文件,构建时合并为 sprite.svg,通过 <use> 引用。注意外部引用的 currentColor 兼容性,必要时用 svg4everybody 做 polyfill。装饰性图形——用 CSS 背景。不该出现在 HTML 语义里的纯装饰元素,CSS 背景是正确的归属。混合策略往往是最务实的选择:首屏关键图标内联确保即时渲染,其余图标走 img 或外部 Sprite 利用缓存,装饰图形放 CSS 背景。不需要追求"统一一种方式",因为 SVG 本身就是为了解决不同场景而存在多种嵌入方式的。
服务端阅读 05月27日 14:37

SVG 的 defs 和 use 怎么配合实现图形复用?

当你手写 SVG 时,有没有遇到过这样的情况:同一个图标在页面里复制粘贴了七八次,改一个颜色就要全局替换?SVG 的 <defs> 和 <use> 就是用来解决这个问题的——把图形定义一次,到处引用。defs:定义但不渲染<defs> 是一个纯容器元素,它内部的所有子元素都不会直接显示在画布上。它的作用只有一个:给后续的引用提供"模板"。<svg width="0" height="0" style="position:absolute"> <defs> <circle id="dot" cx="10" cy="10" r="8" /> </defs></svg>这段代码在页面上什么都看不到。<circle> 被包在 <defs> 里,浏览器知道它的身份是"定义",跳过渲染。几乎所有 SVG 元素都能放进 <defs>——<g>、<path>、<linearGradient>、<clipPath>、<filter> 等等。但有一个例外是 <symbol>,它本身就不渲染,所以通常直接放在 <svg> 根元素下而非 <defs> 里。use:引用并实例化<use> 通过 href(或旧版的 xlink:href)指向一个已定义元素的 id,在文档中创建该元素的一个实例:<svg viewBox="0 0 200 80"> <defs> <rect id="btn" width="60" height="30" rx="6" /> </defs> <use href="#btn" x="10" y="25" fill="#4F46E5" /> <use href="#btn" x="80" y="25" fill="#10B981" /> <use href="#btn" x="150" y="25" fill="#F59E0B" /></svg>三个 <use> 各自独立定位、独立着色,但共享同一个 <rect> 的几何定义。改 <rect> 的 rx,三个按钮同时变。<use> 的 x 和 y 属性等价于在引用内容上施加一个 translate(x, y) 变换,不是重新定位原点——这个细节在组合 transform 时容易踩坑。symbol 和 defs 各管什么<symbol> 和 <defs> 都能定义不渲染的复用元素,但定位不同:<defs> 是通用容器,里面放什么都可以——渐变、路径、裁剪区、滤镜。它不提供自己的坐标系。<symbol> 专门面向"图标"这类场景,自带 viewBox 和 preserveAspectRatio,定义了自己的视口和缩放策略。<symbol id="icon-search" viewBox="0 0 24 24"> <circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2"/> <line x1="16" y1="16" x2="21" y2="21" stroke="currentColor" stroke-width="2"/></symbol><!-- 引用时可以自由设置尺寸 --><use href="#icon-search" width="24" height="24" /><use href="#icon-search" width="48" height="48" />如果没有 viewBox 的需求,<defs> + <g> 就够了;如果图标需要自适应缩放,<symbol> 更合适。实际项目中 <symbol> 用得更多,因为图标系统几乎都需要 viewBox。fill 继承与 currentColor这是 <use> 最实用的样式机制。理解它需要知道一个前提:SVG 的 fill 属性默认值是 black,不是 inherit。所以如果你在 <defs> 里给元素写了 fill="#333",外部怎么改 CSS 都不会生效。两种策略可以让 <use> 实例的颜色可控:策略一:不写 fill,让它级联<defs> <path id="arrow" d="M5 12h14M12 5l7 7-7 7" /></defs><use href="#arrow" fill="none" stroke="currentColor" stroke-width="2" /><path> 没有内联 fill,所以会从 <use> 上继承。stroke 同理。策略二:用 currentColor 做双色调图标<symbol id="icon-folder" viewBox="0 0 24 24"> <!-- 外框:跟随 color --> <path d="M2 6a2 2 0 012-2h5l2 2h9a2 2 0 012 2v10a2 2 0 01-2 2H4a2 2 0 01-2-2z" fill="currentColor"/> <!-- 内部:跟随 fill,默认 transparent --> <path d="M4 10h16v8H4z" fill="inherit"/></symbol>通过 CSS 同时设置 color 和 fill,可以实现双色图标。很多设计系统的图标库就是这么做的。use 的 Shadow DOM 问题<use> 引用元素时,浏览器会在内部创建一个 Shadow DOM,把引用的内容克隆进去。这带来了几个实际问题:CSS 选择器无法穿透。 你不能用 .icon path { fill: red } 这样的选择器去修改 <use> 内部的 <path>。Shadow DOM 是封闭的。JavaScript 无法直接操作内部节点。 querySelector 不会进入 Shadow DOM。要修改内部元素,只能通过修改原始定义或利用 CSS 继承从外部传入。ID 冲突。 如果同一个 <defs> 被多次引用,内部克隆的元素会带着相同的 id,可能导致页面上 id 不唯一。这是 <use> 在复杂场景下的一个隐性风险。可访问性。 屏幕阅读器对 Shadow DOM 内的内容支持不一致。对于功能性图标,建议在 <use> 外层的 <svg> 上添加 aria-label 或 aria-hidden="true":<svg aria-hidden="true" class="icon"> <use href="#icon-close" /></svg>跨文件引用<use> 的 href 可以指向外部 SVG 文件中的元素:<use href="sprites.svg#icon-home" />这种方式的优点是浏览器可以缓存 sprites.svg,所有页面共享同一个精灵文件。但也有明显限制:浏览器兼容性:IE 完全不支持,部分旧版 Edge 也有问题。现代浏览器基本都支持了。样式隔离更严格:外部文件的内部元素与当前页面的 CSS 完全隔离,连 currentColor 继承都不一定生效(取决于浏览器实现)。CORS 限制:跨域引用需要正确的 CORS 头。无法用 CSS 变量穿透:跨文件时 CSS 自定义属性不会传递进去。实际项目中更常见的做法是用构建工具(如 webpack 的 svg-sprite-loader、Vite 插件)在编译时把所有图标内联到页面顶部的 <svg> 精灵中,既保留了缓存优势,又避免了跨文件的限制。性能影响<use> 的性能模型需要分两面看:正面——DOM 节点更少。一个包含 50 个图标的页面,用 <use> 引用比复制 50 份完整 SVG 代码要轻得多。配合 GZIP 压缩,重复的 <use href="#icon-xx"> 标签压缩率极高。反面——Shadow DOM 的克隆有开销。浏览器需要为每个 <use> 创建内部 DOM 树。当引用的是复杂图形(比如几百个节点的地图区块),大量 <use> 实例会导致内存占用上升和渲染变慢。这种情况下,用 CSS background-image 或 <img> 标签引用可能更高效。一个实用的判断标准:如果引用的元素内部节点少于 20 个,<use> 几乎总是更好的选择;如果超过 100 个节点且实例数超过几十个,就要考虑替代方案。实际应用:图标系统SVG 图标系统是 <defs> / <symbol> + <use> 最典型的应用场景。一个常见的架构:<!-- 放在页面 body 顶部,display:none 隐藏 --><svg xmlns="http://www.w3.org/2000/svg" style="display:none"> <symbol id="icon-home" viewBox="0 0 24 24"> <path d="M3 12l9-9 9 9M5 10v10a1 1 0 001 1h3v-6h6v6h3a1 1 0 001-1V10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </symbol> <symbol id="icon-user" viewBox="0 0 24 24"> <path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2M12 11a4 4 0 100-8 4 4 0 000 8z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/> </symbol></svg><!-- 使用 --><button> <svg class="icon" width="20" height="20"> <use href="#icon-home" /> </svg> 首页</button>CSS 只需要一行就能控制图标颜色:.icon { color: inherit; } /* 自动跟随按钮文字颜色 */构建工具通常会把 src/icons/ 目录下的独立 SVG 文件自动合并成上面的精灵文件,开发时每个图标仍是单独的文件,构建时自动拼合。实际应用:背景图案<defs> 的另一个经典用法是定义 <pattern>,配合 <use> 或直接填充实现重复图案:<svg width="400" height="200"> <defs> <pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse"> <path d="M 20 0 L 0 0 0 20" fill="none" stroke="#e5e7eb" stroke-width="0.5"/> </pattern> </defs> <rect width="400" height="200" fill="url(#grid)" /></svg>CSS 中也可以直接引用:.background { background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20'><defs><pattern id='g' width='20' height='20' patternUnits='userSpaceOnUse'><path d='M20 0L0 0 0 20' fill='none' stroke='%23e5e7eb' stroke-width='0.5'/></pattern></defs><rect width='20' height='20' fill='url(%23g)'/></svg>");}<defs> 定义、<use> 引用,这组机制的本质是"声明一次,使用多次"。它把 SVG 从标记语言提升到了组件化思维的层面——定义和实例分离,样式通过继承和 currentColor 从外部控制,构建工具负责编译时拼合。掌握了 fill 继承、Shadow DOM 限制、<symbol> 的 viewBox 优势这几个关键点,就能在图标系统和图案复用中用好这套机制,而不是在代码里反复复制粘贴同一个图标的路径数据。
服务端阅读 05月27日 14:37

SVG 怎样实现渐变和滤镜效果?

为什么 SVG 需要渐变和滤镜纯色填充和简单描边只能解决最基本的视觉需求。当设计要求柔和过渡的光影、逼真的投影、或是非写实的色彩处理时,SVG 的渐变和滤镜才是真正的答案——它们让矢量图形脱离"扁平图标"的刻板印象,具备接近位图编辑软件的表现力,同时保留分辨率无关的优势。线性渐变 linearGradient线性渐变沿一条直线方向过渡颜色。它定义在 <defs> 内部,通过 x1/y1/x2/y2 控制渐变线的起止坐标。<defs> <linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" stop-color="#ff6b6b" /> <stop offset="50%" stop-color="#feca57" /> <stop offset="100%" stop-color="#48dbfb" /> </linearGradient></defs><rect width="300" height="100" fill="url(#grad1)" />这段代码实现了一个从左到右的红-黄-蓝水平渐变。x1="0%" x2="100%" 让渐变方向水平,把 y2 也设为 100% 就能得到对角线渐变。径向渐变 radialGradient径向渐变从一个中心点向外辐射,适合做球体高光、聚光灯等效果。<defs> <radialGradient id="grad2" cx="50%" cy="50%" r="50%" fx="40%" fy="40%"> <stop offset="0%" stop-color="#ffffff" /> <stop offset="100%" stop-color="#2d3436" /> </radialGradient></defs><circle cx="150" cy="100" r="80" fill="url(#grad2)" />cx/cy/r 定义渐变圆的圆心和半径,fx/fy 是焦点位置——偏移焦点可以模拟定向光源照射球体的效果。当 fx/fy 与 cx/cy 不重合时,高光区会偏向焦点一侧。渐变的关键控制属性stop-color、stop-opacity 和 offset每个 <stop> 节点通过 offset 指定在渐变线上的位置(0%–100%),stop-color 设定颜色,stop-opacity 控制该点的透明度。两个相邻 stop 之间的颜色会自动插值。想让渐变在某段区间保持纯色,只需把两个 stop 设为相同的 stop-color 但不同的 offset:<stop offset="30%" stop-color="#e74c3c" /><stop offset="60%" stop-color="#e74c3c" />这样从 30% 到 60% 都是纯红色,两侧才产生过渡。gradientUnitsgradientUnits 决定坐标是相对于元素本身还是整个视口:objectBoundingBox(默认):坐标 0–1 映射到元素的边界框userSpaceOnUse:使用 SVG 画布的绝对坐标当多个元素共享同一个渐变但尺寸不同时,userSpaceOnUse 能保证一致的渐变范围;objectBoundingBox 则自动适配每个元素。gradientTransform 和 spreadMethodgradientTransform 允许对渐变坐标施加矩阵变换(旋转、缩放等),等同于 CSS 的 transform。spreadMethod 控制渐变范围外的填充方式:pad(默认,延伸最后一色)、repeat(重复)、reflect(镜像翻转重复)。filter 滤镜的工作原理SVG 滤镜基于"图元管道"(filter primitive pipeline)模型:每个滤镜原语接收输入图像,处理后输出结果,下一个原语再接续处理。整条管线定义在 <filter> 元素中,放在 <defs> 里。<defs> <filter id="myFilter" x="-20%" y="-20%" width="140%" height="140%"> <!-- 滤镜原语依次排列 --> </filter></defs><rect filter="url(#myFilter)" ... />x/y/width/height 定义滤镜的计算区域,默认是 -10%/120%,如果模糊或偏移超出原元素边界,需要手动扩大这个区域。两个内置输入输出标识符贯穿整个管道:SourceGraphic:原始未过滤图形SourceAlpha:原始图形的 Alpha 通道(无颜色)result:当前原语的输出命名,供后续原语通过 in 引用高斯模糊 feGaussianBlur最常用的滤镜原语之一,stdDeviation 控制模糊半径,值越大越模糊:<filter id="blur1"> <feGaussianBlur in="SourceGraphic" stdDeviation="5" /></filter>可以分别指定水平和垂直方向的模糊:stdDeviation="8 2" 表示水平模糊 8px、垂直 2px。高斯模糊是构建投影、发光等效果的基础——先把图形模糊,再和原图叠加。投影 feDropShadowfeDropShadow 是一个复合原语,内部等价于 feOffset + feGaussianBlur + feFlood + feComposite 的组合:<filter id="shadow1"> <feDropShadow dx="4" dy="4" stdDeviation="3" flood-color="#000000" flood-opacity="0.4" /></filter>dx/dy 控制偏移,flood-color/flood-opacity 控制阴影颜色和透明度。需要内阴影或更复杂的投影时,就得手动拆分上述原语组合,灵活控制每一步。颜色矩阵 feColorMatrixfeColorMatrix 是 SVG 滤镜中最强大的色彩处理工具,支持四种模式:matrix 模式用 5×4 矩阵对每个像素的 RGBA 做线性变换:| R' | | r1 r2 r3 r4 r5 | | R || G' | = | g1 g2 g3 g4 g5 | × | G || B' | | b1 b2 b3 b4 b5 | | B || A' | | a1 a2 a3 a4 a5 | | A | | 1 |单位矩阵(无效果):values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"灰度转换只需让 R/G/B 三个通道取加权平均:<feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0" />saturate 模式type="saturate" values="0" 完全去色,values="2" 饱和度翻倍。值域 0–1 降低饱和度,>1 增强饱和度。hueRotate 模式type="hueRotate" values="90" 将色相旋转 90 度,值域 0–360。luminanceToAlpha 模式将亮度映射为透明度,常用于生成基于亮度的蒙版。注意:feColorMatrix 默认在 linearRGB 色彩空间计算,做反色等操作时结果可能偏离预期,在 <filter> 上设置 color-interpolation-filters="sRGB" 可切换。混合 feBlendfeBlend 将两个输入按指定模式混合,支持 normal/multiply/screen/darken/lighten/overlay/color-dodge/color-burn/hard-light/soft-light/difference/exclusion/hue/saturation/color/luminosity 共 16 种模式。<filter id="blend1"> <feGaussianBlur in="SourceAlpha" stdDeviation="6" result="blur" /> <feOffset in="blur" dx="3" dy="3" result="offsetBlur" /> <feFlood flood-color="#e74c3c" flood-opacity="0.6" result="color" /> <feComposite in="color" in2="offsetBlur" operator="in" result="shadow" /> <feBlend in="SourceGraphic" in2="shadow" mode="normal" /></filter>这个例子先模糊 Alpha 通道、偏移、着色,最后用 feBlend 把彩色投影和原图合成。in2 指定第二个输入——每一步都可以精确控制数据来源。滤镜组合实战把多个原语串起来才能实现复杂效果。一个完整的发光+投影滤镜长这样:<filter id="glowShadow" x="-30%" y="-30%" width="160%" height="160%"> <!-- 投影:偏移+模糊 --> <feOffset in="SourceAlpha" dx="4" dy="6" result="offset" /> <feGaussianBlur in="offset" stdDeviation="5" result="shadowBlur" /> <feFlood flood-color="#000000" flood-opacity="0.35" result="shadowColor" /> <feComposite in="shadowColor" in2="shadowBlur" operator="in" result="shadow" /> <!-- 发光:模糊+着色 --> <feGaussianBlur in="SourceAlpha" stdDeviation="8" result="glowBlur" /> <feFlood flood-color="#6c5ce7" flood-opacity="0.5" result="glowColor" /> <feComposite in="glowColor" in2="glowBlur" operator="in" result="glow" /> <!-- 分层合成:投影 → 发光 → 原图 --> <feMerge> <feMergeNode in="shadow" /> <feMergeNode in="glow" /> <feMergeNode in="SourceGraphic" /> </feMerge></filter>feMerge 是另一种合成方式,按顺序将多个输入叠加到同一画布上,先写的在底层。这里投影在最下,发光居中,原图最上。CSS filter 与 SVG filter 怎么选CSS filter 属性提供了 blur/brightness/contrast/drop-shadow/grayscale/hue-rotate/invert/opacity/saturate/sepia 等快捷函数,本质上就是 SVG 滤镜的常用子集,浏览器做了硬件加速优化。选择 CSS filter 的场景:只需要单种简单效果(如 filter: blur(4px))追求渲染性能,CSS filter 解析和执行更快不需要跨元素复用同一滤镜定义选择 SVG filter 的场景:需要多步管道组合(模糊+偏移+着色+混合)需要颜色矩阵等 CSS filter 无法表达的效果多个元素复用同一定义需要兼容旧版浏览器(SVG filter 起步更早)简单效果用 CSS,复杂效果用 SVG——这是最务实的分工。性能注意事项模糊是性能杀手:stdDeviation 超过 20 的模糊在移动端会显著卡顿,能用小值就别用大值缩小滤镜区域:精确设置 <filter> 的 x/y/width/height,避免对不可见区域做无用计算减少滤镜层级:每多一个原语就多一轮像素处理,能用 3 步完成的效果别拆成 7 步避免在动画中使用复杂滤镜:每帧都要重新计算像素,优先用 CSS transform/opacity 做动画硬件加速差异:CSS filter 在主流浏览器中走 GPU 加速路径,SVG filter 的加速程度取决于浏览器实现,Chrome 和 Firefox 的表现好于 Safari测试移动端:SVG filter 在低端移动设备上的性能差距会被放大,务必真机验证渐变让 SVG 拥有色彩过渡的能力,滤镜让 SVG 拥有像素级处理的能力。两者组合起来,矢量图形不再只是线条和填色——模糊、投影、色彩变换、多步合成,这些曾经需要位图编辑器才能完成的效果,现在直接写在 SVG 标记里就能实现。掌握 stop 节点控制渐变节奏,理解 filter primitive pipeline 的输入输出串联,比记住任何单个属性都重要。
服务端阅读 05月27日 14:36

SVG 怎样实现点击、拖拽、动画和无障碍交互?

从一个静态图标说起你在一个管理后台里放了一个 SVG 图标,产品说"点它能不能切换状态?"——于是你开始搜索 SVG 到底怎么绑定事件。接着设计说"hover 时能不能有个动画过渡?"——你发现 SVG 的动画方案不止一种。再后来需求升级到"能不能拖拽元素""能不能缩放平移画布"——你意识到 SVG 交互远比想象中复杂。这篇文章把 SVG 交互开发的核心技术一次性梳理清楚,从事件处理到动画方案,从拖拽缩放到无障碍支持,最后看 D3.js 如何把这些能力封装成数据驱动的交互模式。SVG 事件处理:click、hover、mousemoveSVG 元素是合法的 DOM 节点,所以你可以像操作 HTML 一样给它绑定事件。内联 SVG 直接挂在标签上就行:<svg width="200" height="200"> <rect id="box" x="10" y="10" width="80" height="80" fill="#4A90D9" /></svg><script> const rect = document.getElementById('box'); rect.addEventListener('click', () => { rect.setAttribute('fill', '#E74C3C'); }); rect.addEventListener('mouseenter', () => { rect.setAttribute('opacity', '0.8'); }); rect.addEventListener('mouseleave', () => { rect.setAttribute('opacity', '1'); }); rect.addEventListener('mousemove', (e) => { // 获取鼠标在 SVG 坐标系中的位置 const svg = rect.closest('svg'); const point = svg.createSVGPoint(); point.x = e.clientX; point.y = e.clientY; const svgPoint = point.matrixTransform(svg.getScreenCTM().inverse()); console.log(`x: ${svgPoint.x}, y: ${svgPoint.y}`); });</script>几个要点:mouseenter / mouseleave 不冒泡,mouseover / mouseout 会冒泡。如果 SVG 内有子元素(比如 <g> 里嵌了多个形状),用不冒泡版本避免反复触发。mousemove 中拿 e.clientX/Y 是屏幕坐标,要通过 getScreenCTM().inverse() 转换为 SVG 坐标,否则 SVG 做过缩放或位移后坐标会偏移。也可以用 SVG 属性写法 onclick="handler(evt)",但这种方式和 HTML 内联事件一样,不利于维护,推荐 addEventListener。如果 SVG 通过 <img> 标签引入,JavaScript 无法访问内部元素。需要交互的 SVG 必须内联或使用 <object> / <embed> 标签,再通过 contentDocument 访问内部 DOM。CSS 动画:transition 与 keyframesSVG 元素支持 CSS transition 和 animation,这是最轻量的动画方案。transition 处理状态变化.circle-btn { fill: #4A90D9; transition: fill 0.3s ease, r 0.3s ease, transform 0.3s ease; transform-origin: center; cursor: pointer;}.circle-btn:hover { fill: #E74C3C; transform: scale(1.15);}注意 SVG 的 transform-origin 默认是 SVG 画布的 (0, 0),不是元素自身中心。需要显式设置 transform-origin: center 或用具体的坐标值。Safari 对 SVG 的 transform-origin 处理曾有问题,可以用 transform-box: fill-box 让浏览器以元素的填充框为参考:.circle-btn { transform-box: fill-box; transform-origin: center;}keyframes 做持续动画描边动画是 SVG 最经典的 CSS 动画效果:.draw-path { stroke-dasharray: 1000; stroke-dashoffset: 1000; animation: draw 2s ease forwards;}@keyframes draw { to { stroke-dashoffset: 0; }}stroke-dasharray 定义虚线长度,stroke-dashoffset 控制偏移。把 offset 从总长度拉到 0,就是一条逐渐画出的线。总长度可以用 getTotalLength() 在 JavaScript 里获取后精确设置。CSS 动画的优势是不依赖 JavaScript,浏览器可以硬件加速。缺点是无法操作 SVG 特有的属性(比如 <path> 的 d 属性),也不能基于数据驱动动画。SMIL 动画:animate 与 animateTransformSMIL 是 SVG 原生的动画方案,直接写在 SVG 标签内部,不需要 CSS 也不需要 JavaScript。animate 属性变化<rect x="10" y="10" width="40" height="40" fill="#4A90D9"> <animate attributeName="width" from="40" to="120" dur="1s" begin="click" fill="freeze" /></rect>begin="click" 表示点击触发,fill="freeze" 让动画停在终态。begin 支持丰富的时间语法,比如 click + 0.5s(点击后 0.5 秒开始)、rect1.click(另一个元素点击时开始)。animateTransform 做变换<rect x="50" y="50" width="40" height="40" fill="#E74C3C"> <animateTransform attributeName="transform" type="rotate" from="0 70 70" to="360 70 70" dur="2s" repeatCount="indefinite" /></rect>type 支持 translate、scale、rotate、skewX、skewY。from 和 to 的值格式与对应的 transform 函数一致。SMIL 的优点是声明式、无需额外代码,且能精确控制动画时序关系。Chrome 曾在 2015 年宣布弃用 SMIL,后来撤回了决定,目前主流浏览器均支持。但如果你的项目需要 IE 兼容,SMIL 不可用。JavaScript 操作 SVG:DOM API 详解JavaScript 通过 DOM API 可以精确控制 SVG 的每个细节。创建和修改元素const svgNS = 'http://www.w3.org/2000/svg';const svg = document.querySelector('svg');// 创建元素必须用 createElementNS,不是 createElementconst circle = document.createElementNS(svgNS, 'circle');circle.setAttribute('cx', '100');circle.setAttribute('cy', '100');circle.setAttribute('r', '30');circle.setAttribute('fill', '#27AE60');svg.appendChild(circle);// 修改属性circle.setAttribute('r', '50');// 读取属性const radius = circle.getAttribute('r'); // "50"// 删除元素circle.remove();关键点:SVG 元素在 XML 命名空间下,创建时必须用 createElementNS,传入 'http://www.w3.org/2000/svg'。用 createElement('circle') 创建的元素浏览器不会识别为 SVG 元素,只会当作未知 HTML 标签。操作 transform// 获取当前变换矩阵const ctm = circle.getCTM(); // 相对于最近 SVG 容器const screenCtm = circle.getScreenCTM(); // 相对于屏幕// 通过 SVGTransform 接口设置变换const transform = svg.createSVGTransform();transform.setTranslate(50, 30);circle.transform.baseVal.appendItem(transform);getCTM() 返回的是元素相对于 SVG 视口的变换矩阵,包含了所有祖先元素的变换叠加。这在坐标转换时非常有用。用 requestAnimationFrame 做帧动画let angle = 0;const el = document.getElementById('rotating-rect');function animate() { angle += 1; el.setAttribute('transform', `rotate(${angle} 100 100)`); if (angle < 360) { requestAnimationFrame(animate); }}animate();requestAnimationFrame 在每帧重绘前调用回调,比 setInterval 更流畅且节能(标签页不可见时自动暂停)。拖拽实现:从 mousedown 到 dropSVG 拖拽比 HTML 拖拽多了坐标转换这一步。let dragging = null;let offset = { x: 0, y: 0 };function getClientToSVG(svg) { return (clientX, clientY) => { const pt = svg.createSVGPoint(); pt.x = clientX; pt.y = clientY; return pt.matrixTransform(svg.getScreenCTM().inverse()); };}const svg = document.querySelector('svg');const toSVG = getClientToSVG(svg);svg.addEventListener('mousedown', (e) => { if (e.target.classList.contains('draggable')) { dragging = e.target; const svgPt = toSVG(e.clientX, e.clientY); // 记录点击位置与元素位置的偏移 const cx = +dragging.getAttribute('cx') || 0; const cy = +dragging.getAttribute('cy') || 0; offset.x = svgPt.x - cx; offset.y = svgPt.y - cy; }});svg.addEventListener('mousemove', (e) => { if (!dragging) return; const svgPt = toSVG(e.clientX, e.clientY); dragging.setAttribute('cx', svgPt.x - offset.x); dragging.setAttribute('cy', svgPt.y - offset.y);});svg.addEventListener('mouseup', () => { dragging = null;});核心逻辑:把屏幕坐标转换为 SVG 坐标,减去初始偏移,设置为元素新位置。对于 <rect> 等用 x/y 定位的元素,修改 x/y 属性即可;对于 <g> 容器,修改 transform 的 translate 值。触摸设备需要额外监听 touchstart、touchmove、touchend,从 e.touches[0].clientX/Y 取坐标。缩放平移:SVG viewport 变换SVG 的缩放和平移有两种实现路径:修改 viewBox 或修改 transform。方案一:修改 viewBoxconst svg = document.querySelector('svg');let vb = { x: 0, y: 0, w: 800, h: 600 };const scale = 1.1;svg.addEventListener('wheel', (e) => { e.preventDefault(); const factor = e.deltaY > 0 ? scale : 1 / scale; // 以鼠标位置为中心缩放 const pt = svg.createSVGPoint(); pt.x = e.clientX; pt.y = e.clientY; const svgPt = pt.matrixTransform(svg.getScreenCTM().inverse()); vb.x = svgPt.x - (svgPt.x - vb.x) / factor; vb.y = svgPt.y - (svgPt.y - vb.y) / factor; vb.w /= factor; vb.h /= factor; svg.setAttribute('viewBox', `${vb.x} ${vb.y} ${vb.w} ${vb.h}`);});修改 viewBox 相当于改变"相机"的位置和焦距,画布上的元素本身不变。适合做整个画布的缩放平移,类似地图交互。方案二:transform 变换对单个元素或 <g> 容器使用 transform 做缩放平移,不影响其他元素:const group = document.getElementById('layer');group.setAttribute('transform', `translate(${tx}, ${ty}) scale(${s})`);两种方案的选择:画布级操作用 viewBox,元素级操作用 transform。实际项目中经常组合使用——viewBox 控制全局视口,transform 控制图层独立变换。无障碍交互:ARIA 与键盘支持SVG 交互不是视觉用户的专属,屏幕阅读器和键盘用户也需要能操作。让 SVG 可聚焦<svg width="200" height="200" role="img" aria-labelledby="chart-title chart-desc"> <title id="chart-title">季度销售额柱状图</title> <desc id="chart-desc">展示了2024年四个季度的销售额变化趋势</desc> <g tabindex="0" role="button" aria-label="第一季度:销售额120万" class="bar-interactive"> <rect x="20" y="80" width="30" height="120" fill="#4A90D9" /> </g> <g tabindex="0" role="button" aria-label="第二季度:销售额150万" class="bar-interactive"> <rect x="70" y="50" width="30" height="150" fill="#27AE60" /> </g></svg>关键点:用 tabindex="0" 让元素进入 Tab 导航序列。不要用已废弃的 focusable 属性。每个可聚焦元素必须有可访问名称,通过 aria-label 或 <title> 提供。非交互 SVG 用 role="img",交互部分用对应的 role(如 button、slider)。键盘事件处理document.querySelectorAll('.bar-interactive').forEach(bar => { bar.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); // 触发和 click 相同的逻辑 bar.dispatchEvent(new Event('click')); } }); bar.addEventListener('focus', () => { bar.querySelector('rect').setAttribute('stroke', '#333'); bar.querySelector('rect').setAttribute('stroke-width', '2'); }); bar.addEventListener('blur', () => { bar.querySelector('rect').removeAttribute('stroke'); });});focus 和 blur 事件提供可见的焦点指示器,这是 WCAG 2.1 的硬性要求。不要只用颜色区分焦点状态,高对比度模式下颜色差异可能消失。常见陷阱<img src="chart.svg"> 里的 SVG 对屏幕阅读器不可见,需要用 alt 文本或 aria-label 补充。如果 SVG 作为装饰图使用,加 aria-hidden="true" 让屏幕阅读器跳过。避免单独用颜色传达信息,补充形状或文字标记。D3.js 交互:数据驱动的 SVG 操作D3.js 把上面这些底层能力封装成了数据驱动的 API,是做 SVG 数据可视化的首选工具。事件绑定d3.selectAll('.data-point') .on('click', function(event, d) { d3.select(this) .attr('fill', '#E74C3C') .transition() .duration(300) .attr('r', d.value * 2); }) .on('mouseenter', function(event, d) { d3.select('#tooltip') .style('visibility', 'visible') .text(`${d.label}: ${d.value}`); }) .on('mouseleave', function() { d3.select('#tooltip').style('visibility', 'hidden'); });D3 的 on 方法会自动把绑定的数据 d 传给回调函数,不需要手动从 DOM 上取数据。this 指向当前 DOM 元素,event 是原生事件对象。数据驱动更新function updateChart(data) { const bars = d3.select('#chart') .selectAll('rect') .data(data, d => d.id); // 进入:新数据创建元素 bars.enter() .append('rect') .attr('x', (d, i) => i * 35) .attr('y', d => 300 - d.value) .attr('width', 30) .attr('height', d => d.value) .attr('fill', '#4A90D9') .attr('opacity', 0) .transition() .duration(500) .attr('opacity', 1); // 更新:已有元素过渡到新状态 bars.transition() .duration(500) .attr('y', d => 300 - d.value) .attr('height', d => d.value); // 退出:多余元素移除 bars.exit() .transition() .duration(300) .attr('opacity', 0) .remove();}这就是 D3 的 Enter-Update-Exit 模式。数据变化时,新增的元素做进入动画,更新的元素做过渡动画,删除的元素做淡出动画。整个过程不需要手动管理 DOM 状态。拖拽与缩放D3 提供了 d3.drag() 和 d3.zoom() 模块,封装了坐标转换和事件处理:// 拖拽d3.selectAll('.draggable') .call(d3.drag() .on('drag', function(event, d) { d.x = event.x; d.y = event.y; d3.select(this).attr('cx', d.x).attr('cy', d.y); }) );// 缩放平移const zoom = d3.zoom() .scaleExtent([0.5, 5]) .on('zoom', (event) => { d3.select('#canvas').attr('transform', event.transform); });d3.select('svg').call(zoom);d3.drag() 自动处理屏幕坐标到 SVG 坐标的转换,d3.zoom() 封装了滚轮缩放和拖拽平移,并维护 event.transform 对象(包含 x、y、k 三个分量)。比手动实现省去大量边界处理代码。方案选型速查| 场景 | 推荐方案 | 理由 ||------|---------|------|| 简单 hover 状态变化 | CSS transition | 零 JS、硬件加速 || 持续循环动画(描边、旋转) | CSS keyframes | 声明式、性能好 || 点击触发的属性动画 | SMIL animate | 内嵌 SVG、无需外部代码 || 基于数据的图表交互 | D3.js | 数据驱动、Enter-Update-Exit || 自定义拖拽 | 原生 JS + 坐标转换 | 完全控制、无依赖 || 画布级缩放平移 | viewBox 操作 | 改变视口而非元素 || 需要兼容 IE 的动画 | JS + requestAnimationFrame | SMIL 和部分 CSS 动画 IE 不支持 |选择的核心原则:能用 CSS 就不用 SMIL,能用 SMIL 就不用 JS,数据可视化直接上 D3.js。每多引入一层复杂度,就要多承担一层维护成本。不过这个原则有个前提——如果你需要根据数据动态更新,CSS 和 SMIL 的声明式语法反而会成为障碍,这时候 JS 方案更合适。写在最后SVG 交互的本质并不神秘:它是 DOM 元素,所以有 DOM 事件;它有视觉属性,所以能做 CSS 动画;它有 XML 命名空间,所以要用 createElementNS;它有独立的坐标系,所以要做坐标转换。理解了这几条线,剩下的就是根据场景选工具。CSS 处理简单状态过渡,SMIL 做声明式属性动画,JavaScript 处理复杂逻辑,D3.js 做数据驱动可视化——它们各有擅长的边界,拼在一起就是完整的 SVG 交互开发能力。
服务端阅读 05月27日 14:35

如何在项目中搭建一套可维护的 SVG 图标系统?

前端项目里的图标管理,往往是从"随便放几个 PNG"开始的。等到图标多了,尺寸不统一、颜色改不动、重复加载——问题一个接一个冒出来。SVG 图标系统解决的就是这件事:用一套工程化的方式,让图标的存储、引用、样式控制和更新维护都有章可循。SVG Sprite 的核心原理SVG Sprite 的思路和 CSS Sprite 类似——把多个图标合并到一个文件里,减少 HTTP 请求。不同的是,CSS Sprite 靠背景定位裁切,SVG Sprite 依赖 <symbol> 和 <use> 的引用机制,天然支持缩放和样式继承。一个典型的 SVG Sprite 文件长这样:<svg xmlns="http://www.w3.org/2000/svg" style="display:none"> <symbol id="icon-home" viewBox="0 0 24 24"> <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/> </symbol> <symbol id="icon-search" viewBox="0 0 24 24"> <path d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5z"/> </symbol></svg>使用时通过 <use> 引用:<svg><use href="#icon-home"/></svg>整个 Sprite 只需加载一次,之后引用任意图标都是零网络开销。symbol 与 use 的复用机制<symbol> 本身不渲染,它只是一个定义。关键的复用能力来自 <use>:同文档引用:<use href="#icon-home"/>,Sprite 直接内联在页面 HTML 中。外部文件引用:<use href="/sprites/icon.svg#icon-home"/>,Sprite 作为独立文件被缓存。外部引用的优势是浏览器可以缓存 Sprite 文件,后续页面无需重新下载。但要注意,IE11 不支持跨文件引用,如果有兼容需求,可以用 polyfill(如 svg4everybody)或回退到内联方案。一个实际的问题是:Sprite 内联后,<use> 引用的图标无法通过 CSS 直接修改 <symbol> 内部的 fill 或 stroke,因为 Shadow DOM 的隔离。解决办法是:源 SVG 中不要写死 fill 颜色,而是用 currentColor,这样外部 color 属性就能穿透进去。图标组件化:React 与 Vue 的实现组件化的目的是把 <svg><use/></svg> 这个模板封装起来,提供统一的 API。React 实现import spriteUrl from '/sprites/icon.svg';function Icon({ name, size = 24, className }) { return ( <svg className={className} width={size} height={size} aria-hidden="true" > <use href={`${spriteUrl}#${name}`} /> </svg> );}// 使用<Icon name="home" size={20} />Vue 实现<template> <svg :class="className" :width="size" :height="size" aria-hidden="true"> <use :href="`${spriteUrl}#${name}`" /> </svg></template><script setup>import spriteUrl from '/sprites/icon.svg';defineProps({ name: { type: String, required: true }, size: { type: Number, default: 24 }, className: { type: String, default: '' },});</script>组件化之后,图标的使用变得声明式——不需要记住 <use> 的写法,只需要关心 name 和 size。颜色通过 currentColor + CSS color 控制,或者直接给组件传 style/className。构建工具集成手动维护 Sprite 文件是不现实的,图标一多就容易漏。构建工具的介入让整个过程自动化。svg-sprite:通用的 Sprite 生成器svg-sprite 是一个 Node.js 工具,把一个目录下的 SVG 文件合并成 Sprite:npx svg-sprite --symbol --symbol-dest sprites --symbol-sprite icon.svg src/icons/*.svg它也提供 Gulp 和 Webpack 插件:// gulpconst svgSprite = require('gulp-svg-sprite');gulp.src('src/icons/*.svg') .pipe(svgSprite({ mode: { symbol: true } })) .pipe(gulp.dest('dist/sprites'));SVGR:SVG 转 React 组件如果你不想要 Sprite 方案,而是让每个 SVG 变成独立的 React 组件,SVGR 是更合适的选择。它会将 SVG 源码转换为 JSX,同时执行 SVGO 优化:npx @svgr/cli --icon --replace-attr-values "#000=currentColor" src/icons/home.svg输出:const SvgHome = (props) => ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}> <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" /> </svg>);Webpack 配置:// webpack.config.jsmodule.exports = { module: { rules: [ { test: /\.svg$/, issuer: /\.[jt]sx?$/, use: ['@svgr/webpack'], }, ], },};Vite 配置:// vite.config.jsimport svgr from 'vite-plugin-svgr';export default { plugins: [svgr()],};Vue 项目可以用 vite-svg-loader,效果类似:import svgLoader from 'vite-svg-loader';export default { plugins: [svgLoader()],};按需加载与 Tree-ShakingSprite 方案的局限在于:不管页面用了几个图标,整个 Sprite 都会被加载。对于图标量超过 200 的大型项目,这可能导致几十 KB 的浪费。两种解法:方案一:拆分 Sprite。按业务模块拆成多个 Sprite 文件(如 common.svg、dashboard.svg、editor.svg),页面只加载当前需要的 Sprite。方案二:SVGR 组件化 + 按需导入。每个图标是独立组件,只有被 import 的图标才会打包:import HomeIcon from './icons/Home';import SearchIcon from './icons/Search';function App() { return ( <div> <HomeIcon /> <SearchIcon /> </div> );}这种方式天然支持 tree-shaking。但要注意避免 barrel file(icons/index.ts)把所有图标都引入——如果写了 export { default as Home } from './Home' 之类的统一导出,打包工具可能无法正确 tree-shake。解决方法是配置 "sideEffects": false,或者干脆不写 barrel file,直接从文件路径导入。Vue 项目可以用 defineAsyncComponent 做动态加载:const icon = defineAsyncComponent(() => import(`../icons/${props.name}.svg?component`));图标尺寸与颜色控制尺寸最简单的做法是组件上暴露 size 属性,同时设置 width 和 height。更灵活的方式是用 em 单位,让图标尺寸跟随字体大小:.icon { width: 1em; height: 1em;}这样 font-size: 16px 时图标 16px,font-size: 24px 时图标 24px,和文字对齐非常方便。颜色控制颜色的前提是 SVG 源文件中使用 currentColor 而非硬编码色值。SVGO 可以自动做这个替换:// svgo.config.jsmodule.exports = { plugins: [ { name: 'preset-default' }, { name: 'replaceAttrValues', params: { values: { '#000': 'currentColor', '#333': 'currentColor' } }, }, ],};之后 CSS 控制颜色即可:.icon { color: #333; }.icon-danger { color: #e53e3e; }.icon-muted { color: #a0aec0; }如果图标有两种颜色(比如外框 + 填充),可以用 CSS 变量区分:.icon { --icon-primary: currentColor; --icon-secondary: #a0aec0;}SVG 源码中对应 fill="var(--icon-primary)" 和 fill="var(--icon-secondary)"。Figma 导出 SVG 的工作流设计到开发的图标流转,Figma 是大多数团队的起点。一个高效的导出流程长这样:统一画板尺寸:所有图标放在相同尺寸的 Frame 里(通常 24×24),保证 padding 一致。导出时选 Frame 而非内部路径,否则 padding 会丢。描边转轮廓:导出前执行 Outline Stroke(Cmd+Shift+O),把 stroke 转成 fill。否则导出的 SVG 会保留 stroke 属性,后续用 currentColor 控制颜色会出问题。批量导出:选中多个 Frame → Export 面板 → 格式选 SVG → 导出。图标多的时候用插件(如 Freya DS Icon Exporter)一键批量导出。命名规范:Frame 名称就是导出文件名,用 icon- 前缀 + kebab-case(如 icon-arrow-left),和代码中的引用名保持一致。SVGO 二次优化:Figma 导出的 SVG 可能包含多余属性(id、data-name、冗余 <g>),用 SVGO 清理一遍:npx svgo -f src/icons --config=svgo.config.jsCI 集成:在 CI 流程中加入 Figma API 拉取 + SVGO + Sprite 生成的步骤,设计师在 Figma 里改图标后,开发者无感更新。性能优化要点SVGO 压缩。这是成本最低、收益最明确的优化。默认 preset 就能去掉注释、元数据、编辑器私有属性,通常能减少 30%-60% 体积。关键配置是保留 viewBox(关闭 removeViewBox)、移除固定尺寸(开启 removeDimensions),让图标通过 CSS 控制大小。HTTP 缓存。Sprite 文件配置长期缓存(Cache-Control: max-age=31536000),文件名加 content hash。更新图标时 hash 变化,缓存自动失效。预加载。如果 Sprite 是外部文件,可以在 <head> 中加 <link rel="preload" href="/sprites/icon.svg" as="fetch" crossorigin>,让浏览器提前下载。内联 vs 外链的取舍。内联 Sprite 首屏零延迟,但会让 HTML 体积变大,且无法跨页缓存。外链 Sprite 可以缓存,但首次加载有一次额外请求。实践建议:核心图标(< 20 个)内联,其余走外链。避免重复定义。同一个图标不要在 Sprite 中出现两次。构建时对 id 去重,否则 <use> 引用会指向错误的目标。懒加载非关键图标。首屏不可见的图标,可以用 loading="lazy" 或在 Intersection Observer 触发后再插入 <use> 引用。搭建 SVG 图标系统,核心决策只有两个:用 Sprite 还是组件化,内联还是外链。小项目 Sprite 内联就够了,中大型项目组件化 + 按需导入更合适。不管选哪条路,currentColor + viewBox + SVGO 这三件事做好,后续维护就能省掉大量重复劳动。Figma 到代码的自动化流程补上最后一环,让设计变更不再是手动搬运的体力活。
服务端阅读 05月27日 14:35

如何在响应式设计中正确使用 SVG?

页面在手机上变形、图标在平板上模糊、Logo 在宽屏上被拉伸——这些问题多半和 SVG 的响应式处理有关。SVG 本身是矢量格式,理论上怎么缩放都不会失真,但如果 viewBox、preserveAspectRatio 和 CSS 尺寸没有配合好,结果反而比位图更糟糕。下面逐个拆解这些关键点。viewBox:SVG 响应式的基石viewBox 定义了 SVG 内部的坐标系统和可视区域,格式是 viewBox="min-x min-y width height"。它不决定 SVG 的实际渲染尺寸,而是告诉浏览器"这批图形画在一个多大的虚拟画布上"。<svg viewBox="0 0 200 100" xmlns="http://www.w3.org/2000/svg"> <rect width="200" height="100" fill="#3B82F6" rx="8" /></svg>这个 SVG 的虚拟画布是 200×100,宽高比 2:1。当容器宽度变化时,只要设置了合适的 CSS,图形就会按这个比例等比缩放。关键操作:要实现响应式,必须同时做两件事——设置 viewBox,然后移除 SVG 标签上的固定 width/height 属性,改由 CSS 控制尺寸。.responsive-svg { width: 100%; height: auto;}如果只移除 width/height 而不设 viewBox,SVG 会按默认 300×150 渲染,图形变形不可避免。preserveAspectRatio:控制缩放时的对齐与裁切当 SVG 容器的宽高比和 viewBox 的宽高比不一致时,preserveAspectRatio 决定了图形怎么适配。默认值是 xMidYMid meet,意思是居中显示、等比缩小到完全可见、留白均匀分布。这适合大多数场景,但有些设计需要不同行为:xMinYMin slice:从左上角对齐,等比放大填满容器,超出部分裁切。适合全屏背景图。none:不保持比例,拉伸填满。只在需要铺满且接受变形时使用。xMidYMid meet:居中完整显示,两侧或上下留白。适合 Logo 和图标。<!-- 全屏英雄区背景,裁切不留白 --><svg viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid slice" style="width:100%;height:100vh;"> ...</svg><!-- Logo 始终完整居中 --><svg viewBox="0 0 120 40" preserveAspectRatio="xMidYMid meet" style="width:100%;max-width:200px;height:auto;"> ...</svg>一个常见错误:在 preserveAspectRatio 设置了 meet 的情况下,用 CSS 强制设定与 viewBox 比例不同的固定宽高,结果出现大面积空白。正确做法是只约束一个维度(通常是宽度),让另一个维度自动计算。SVG 宽度自适应的几种写法内联 SVG 有三种方式让宽度自适应容器:1. 百分比宽度 + auto 高度.svg-container { width: 100%; height: auto;}最简单直接,前提是有 viewBox。2. max-width 限制最大宽度.svg-container { width: 100%; max-width: 600px; height: auto;}在大屏上不会无限撑开,适合内容区的图表和插画。3. 容器查询(Container Queries).card { container-type: inline-size;}@container (min-width: 400px) { .card svg { width: 50%; }}@container (max-width: 399px) { .card svg { width: 100%; }}容器查询让 SVG 根据父容器而非视口调整尺寸,在组件化开发中比媒体查询更精准。用媒体查询控制 SVG 内部样式SVG 内部可以写 <style> 标签,里面的媒体查询在不同条件下生效。但要注意一个容易踩的坑:当 SVG 通过 <img> 引入时,媒体查询的视口是 <img> 元素的 CSS 尺寸,不是页面视口。只有内联 SVG 的媒体查询才跟随页面视口。<svg viewBox="0 0 200 100" xmlns="http://www.w3.org/2000/svg"> <style> .label { font-size: 14px; fill: #333; } @media (max-width: 400px) { .label { font-size: 10px; fill: #666; } } </style> <text class="label" x="100" y="55" text-anchor="middle">数据标签</text></svg>这种技术在响应式图标上特别有用:大屏显示图标+文字,小屏只显示图标,通过媒体查询切换 display 即可。响应式图标策略图标系统是 SVG 响应式的高频场景,有三种主流方案:内联 SVG + CSS 控制直接把 SVG 写进 HTML,用 CSS 控制尺寸和颜色。优点是样式灵活、可交互、可做动画;缺点是 HTML 体积增大,大量图标时不适合。<button class="icon-btn"> <svg class="icon" viewBox="0 0 24 24" width="20" height="20"> <path d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.27 5.82 22 7 14.14l-5-4.87 6.91-1.01z"/> </svg> <span class="label">收藏</span></button>CSS background-image + mask-image用 SVG 做 mask,背景色即图标色,换色只需改 CSS 变量。.icon-star { width: 20px; height: 20px; background-color: var(--icon-color, #333); -webkit-mask-image: url("data:image/svg+xml,..."); mask-image: url("data:image/svg+xml,..."); -webkit-mask-size: contain; mask-size: contain;}SVG Sprite + use 引用把所有图标整合到一个 SVG 文件中,用 <use href="#icon-name"> 引用。适合图标数量多的项目。<!-- 隐藏的 sprite --><svg style="display:none"> <symbol id="icon-menu" viewBox="0 0 24 24"> <path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/> </symbol></svg><!-- 使用 --><svg class="icon" width="24" height="24"> <use href="#icon-menu"/></svg>SVG 作为背景图片的响应式适配SVG 做背景图片时,需要同时处理 SVG 文件内部的 viewBox 和 CSS 的 background-size:.hero { background-image: url('hero-pattern.svg'); background-size: cover; background-position: center; min-height: 400px;}background-size: cover 配合 SVG 的 preserveAspectRatio="xMidYMid slice",能保证背景始终填满容器且不变形。如果用 contain,则可能出现留白。对于平铺纹理,用 background-repeat: repeat 配合 background-size 控制单元大小:.pattern-bg { background-image: url('pattern.svg'); background-repeat: repeat; background-size: 60px 60px;}在小屏上可以缩小 background-size 让纹理更密集,大屏上放大让纹理更稀疏,通过媒体查询切换即可。srcset 与 picture 元素配合 SVGSVG 本身是矢量的,不需要多分辨率版本,但 <picture> 元素在两个场景下仍然有用:格式回退<picture> <source type="image/svg+xml" srcset="logo.svg"> <img src="logo.png" srcset="logo-2x.png 2x" alt="Logo"></picture>不支持 SVG 的浏览器(极少数旧浏览器)自动回退到 PNG。艺术指导(Art Direction)用 SVG 的 fragment 标识符在不同断点切换 viewBox,实现不同裁切:<picture> <source media="(min-width: 768px)" srcset="chart.svg#svgView(viewBox(0,0,800,400))"> <img src="chart.svg#svgView(viewBox(200,0,400,400))" alt="数据图表"></picture>大屏显示完整图表,小屏聚焦核心区域,不需要准备多个文件。svgView() 片段可以直接在 URL 中覆盖 viewBox 值。常见布局问题与排查SVG 高度塌陷移除固定 height 后,某些浏览器无法从 viewBox 计算出正确高度。解决方案是给外层容器设 aspect-ratio:.svg-wrapper { aspect-ratio: 2 / 1; /* 匹配 viewBox 的宽高比 */ width: 100%;}.svg-wrapper svg { width: 100%; height: 100%;}Flex/Grid 布局中 SVG 被拉伸Flex 容器的 align-items: stretch 会让 SVG 高度撑满容器。加 align-self: start 或 align-items: flex-start 可以恢复等比缩放。内联 SVG 与 <img> 的媒体查询不一致内联 SVG 的媒体查询参考视口宽度,<img> 引入的 SVG 媒体查询参考元素自身宽度。如果需要在 <img> 中根据视口变化,改用 <picture> 的 media 属性在 HTML 层切换。iOS Safari 下 viewBox 缩放异常给 SVG 显式设置 overflow: visible,并确保没有 width/height 属性和 CSS 尺寸冲突。SVG 的响应式并不复杂,核心就是三件事:viewBox 定坐标系、preserveAspectRatio 定适配规则、CSS 定实际尺寸。三者配合好,矢量图形在任何屏幕上都能正确显示。遇到变形先查 viewBox 有没有设,遇到留白先查 preserveAspectRatio 的值,遇到尺寸失控先查 CSS 和 HTML 属性是否冲突——按这个顺序排查,绝大多数问题都能定位。
服务端阅读 05月27日 14:32

如何用 SVG 从零创建交互式数据图表?

在网页上画一张图表,很多人第一反应是找 Chart.js 或 ECharts 这样的库,写几行配置就出图。但当你需要定制一个标尺刻度倾斜 30 度、柱子圆角渐变、悬停时弹出带箭头提示框的图表时,配置项就不够用了——你得回到 SVG 本身。SVG 是所有这些图表库的底层绘图语言,理解它意味着你能在任何场景下精确控制每一个像素。SVG 图表基础:用原生标签画三种经典图表SVG(Scalable Vector Graphics)是一种基于 XML 的矢量图形格式,浏览器可以直接渲染。它的核心优势在于:每个图形元素都是 DOM 节点,可以像操作 HTML 一样用 CSS 和 JavaScript 控制。柱状图柱状图是最直观的图表类型。核心思路:为每条数据生成一个 <rect> 元素,x 坐标按索引递增,y 坐标和高度由数据值决定。<svg width="500" height="300" viewBox="0 0 500 300"> <!-- Y 轴 --> <line x1="40" y1="10" x2="40" y2="260" stroke="#333" /> <!-- X 轴 --> <line x1="40" y1="260" x2="490" y2="260" stroke="#333" /> <!-- 柱子 --> <rect x="60" y="160" width="50" height="100" fill="#4F46E5" rx="4" /> <rect x="130" y="110" width="50" height="150" fill="#4F46E5" rx="4" /> <rect x="200" y="60" width="50" height="200" fill="#4F46E5" rx="4" /> <rect x="270" y="135" width="50" height="125" fill="#4F46E5" rx="4" /> <rect x="340" y="85" width="50" height="175" fill="#4F46E5" rx="4" /></svg>关键点:y 的值等于画布高度减去柱子高度,因为 SVG 坐标系原点在左上角。rx="4" 给柱子加上圆角,这是 SVG 原生支持的属性,不需要额外 CSS。折线图折线图的核心是 <polyline> 或 <path>,把数据点连成线。<svg width="500" height="300" viewBox="0 0 500 300"> <polyline points="60,200 130,160 200,80 270,140 340,90 410,110" fill="none" stroke="#4F46E5" stroke-width="2.5" stroke-linejoin="round" /> <!-- 数据点 --> <circle cx="60" cy="200" r="4" fill="#4F46E5" /> <circle cx="130" cy="160" r="4" fill="#4F46E5" /> <circle cx="200" cy="80" r="4" fill="#4F46E5" /> <circle cx="270" cy="140" r="4" fill="#4F46E5" /> <circle cx="340" cy="90" r="4" fill="#4F46E5" /> <circle cx="410" cy="110" r="4" fill="#4F46E5" /></svg>如果需要平滑曲线,把 <polyline> 替换为 <path>,使用三次贝塞尔曲线命令(C 或 S)即可。D3.js 的 d3.line().curve(d3.curveCatmullRom) 可以自动生成平滑路径。饼图饼图用 <path> 的弧线命令(A)绘制扇形,但手工计算弧线参数很繁琐。更实用的方式是用 stroke-dasharray 和 stroke-dashoffset 技巧在 <circle> 上实现:<svg width="200" height="200" viewBox="0 0 200 200"> <circle cx="100" cy="100" r="80" fill="none" stroke="#4F46E5" stroke-width="40" stroke-dasharray="251.3 502.65" stroke-dashoffset="0" /> <circle cx="100" cy="100" r="80" fill="none" stroke="#7C3AED" stroke-width="40" stroke-dasharray="150.8 502.65" stroke-dashoffset="-251.3" /> <circle cx="100" cy="100" r="80" fill="none" stroke="#A78BFA" stroke-width="40" stroke-dasharray="100.55 502.65" stroke-dashoffset="-402.1" /></svg>原理:圆的周长 = 2 * π * r = 502.65,stroke-dasharray 的第一个值是可见弧长(对应数据占比),第二个值是总周长。stroke-dashoffset 控制起始偏移量,让多个扇形依次排列。D3.js 与 SVG:数据驱动的图表开发手写 SVG 标签适合理解原理,但真实项目中数据是动态的。D3.js 解决的核心问题是:把数据和 SVG 元素绑定起来,数据变了图自动变。数据绑定与 enter-update-exit 模式const data = [120, 180, 90, 150, 200];const svg = d3.select('#chart') .append('svg') .attr('width', 500) .attr('height', 300);const bars = svg.selectAll('rect') .data(data);// enter: 新数据对应的元素bars.enter() .append('rect') .attr('x', (d, i) => 50 + i * 80) .attr('y', d => 260 - d) .attr('width', 50) .attr('height', d => d) .attr('fill', '#4F46E5') .attr('rx', 4);// update: 已存在元素的数据变化时bars.attr('height', d => d);// exit: 多余的元素移除bars.exit().remove();这个模式是 D3 的核心。enter 处理新增数据,update 处理数据变化,exit 处理数据减少。理解了它,你就理解了 D3 的一切数据绑定逻辑。比例尺:数据到像素的映射手写 SVG 时你得自己算坐标,D3 的比例尺把这个过程标准化了:const xScale = d3.scaleBand() .domain(data.map((d, i) => i)) .range([50, 480]) .padding(0.2);const yScale = d3.scaleLinear() .domain([0, d3.max(data)]) .range([260, 20]);bars.enter() .append('rect') .attr('x', (d, i) => xScale(i)) .attr('width', xScale.bandwidth()) .attr('y', d => yScale(d)) .attr('height', d => 260 - yScale(d));scaleBand 用于离散数据(柱状图的分类轴),scaleLinear 用于连续数据(数值轴)。range 的方向决定了坐标映射:y 轴从下到上递增,所以 range 是 [260, 20](从画布底部到顶部)。坐标轴一键生成svg.append('g') .attr('transform', 'translate(0, 260)') .call(d3.axisBottom(xScale));svg.append('g') .attr('transform', 'translate(50, 0)') .call(d3.axisLeft(yScale));d3.axisBottom 和 d3.axisLeft 会自动生成刻度线、刻度文字和轴线,省去大量手写 SVG 的工作。响应式图表:让 SVG 适配任何屏幕SVG 天然支持缩放,关键在于正确设置 viewBox 和容器样式。<div class="chart-container"> <svg viewBox="0 0 500 300" preserveAspectRatio="xMidYMid meet"> <!-- 图表内容 --> </svg></div><style>.chart-container { width: 100%; max-width: 800px;}.chart-container svg { width: 100%; height: auto;}</style>核心逻辑:viewBox="0 0 500 300" 定义了内部坐标空间,width: 100% 让 SVG 撑满容器,height: auto 保持宽高比。图表内容用固定坐标绘制,浏览器自动缩放。如果需要在窗口变化时动态调整边距和字体大小,可以监听 resize 事件,重新计算比例尺的 range 并更新元素属性:function resize() { const containerWidth = document.querySelector('.chart-container').clientWidth; xScale.range([50, containerWidth - 20]); svg.selectAll('rect').attr('x', (d, i) => xScale(i)).attr('width', xScale.bandwidth()); svg.selectAll('.x-axis').call(d3.axisBottom(xScale));}window.addEventListener('resize', resize);动画效果:SMIL 与 CSS 两条路径SMIL 动画SMIL(Synchronized Multimedia Integration Language)是 SVG 原生的动画规范,直接写在 SVG 标签内:<rect x="60" y="260" width="50" height="0" fill="#4F46E5" rx="4"> <animate attributeName="height" from="0" to="100" dur="0.6s" fill="freeze" /> <animate attributeName="y" from="260" to="160" dur="0.6s" fill="freeze" /></rect>这段代码让柱子从底部向上"长出来"。SMIL 的优点是声明式、不需要 JavaScript,但 Chrome 曾在 2015 年宣布废弃 SMIL(后来收回),且 SMIL 无法响应数据变化,所以实际项目中使用较少。CSS 动画更主流的方式是用 CSS 控制 SVG 属性:.chart-bar { transform-origin: bottom; transform: scaleY(0); animation: growUp 0.6s ease-out forwards;}@keyframes growUp { to { transform: scaleY(1); }}<rect class="chart-bar" x="60" y="160" width="50" height="100" fill="#4F46E5" rx="4" />CSS 动画可以用 transition 做交互过渡,也能用 @keyframes 做入场动画,浏览器性能优化更好。但注意:CSS 动画只能控制 CSS 可设置的 SVG 属性(如 transform、opacity、fill),不能控制 x、y、width 等呈现属性(在 SVG 2 中部分属性已支持 CSS 化)。D3 过渡动画D3 提供了 transition() API,最适合数据驱动的动画场景:bars.enter() .append('rect') .attr('y', 260) .attr('height', 0) .transition() .duration(600) .ease(d3.easeCubicOut) .attr('y', d => yScale(d)) .attr('height', d => 260 - yScale(d));数据更新时的过渡效果:bars.transition() .duration(400) .attr('y', d => yScale(d)) .attr('height', d => 260 - yScale(d));D3 的过渡 API 在数据变化场景下比 CSS 动画更灵活,因为它可以精确控制每个属性的插值起止值。交互实现:悬停提示与缩放悬停提示SVG 元素可以直接绑定 DOM 事件。一个简洁的 tooltip 实现:const tooltip = d3.select('body') .append('div') .attr('class', 'tooltip') .style('position', 'absolute') .style('padding', '6px 12px') .style('background', 'rgba(0,0,0,0.8)') .style('color', '#fff') .style('border-radius', '4px') .style('font-size', '13px') .style('pointer-events', 'none') .style('opacity', 0);bars.on('mouseover', function(event, d) { d3.select(this).attr('fill', '#6366F1'); tooltip.style('opacity', 1) .html(`值: ${d}`);}).on('mousemove', function(event) { tooltip .style('left', event.pageX + 10 + 'px') .style('top', event.pageY - 20 + 'px');}).on('mouseout', function() { d3.select(this).attr('fill', '#4F46E5'); tooltip.style('opacity', 0);});注意 pointer-events: none 很重要——没有它,鼠标移到 tooltip 上会触发 mouseout,导致闪烁。缩放与平移D3 的 d3.zoom() 模块提供了完整的缩放平移方案:const zoom = d3.zoom() .scaleExtent([0.5, 5]) .on('zoom', (event) => { chartGroup.attr('transform', event.transform); });svg.call(zoom);chartGroup 是一个包含所有图表内容的 <g> 元素。event.transform 包含 x、y、k(缩放比例)三个值,通过 transform 属性一次性应用。scaleExtent 限制缩放范围在 0.5x 到 5x 之间。如果只想缩放 X 轴(比如时间轴上的数据浏览),需要手动在 zoom 回调中更新 xScale 和所有关联元素:const zoom = d3.zoom() .on('zoom', (event) => { const newXScale = event.transform.rescaleX(xScale); svg.selectAll('rect') .attr('x', (d, i) => newXScale(i)) .attr('width', newXScale.bandwidth()); svg.select('.x-axis').call(d3.axisBottom(newXScale)); });性能优化:从渲染层到代码层减少 DOM 节点数量SVG 的性能瓶颈在于 DOM 节点过多。浏览器需要为每个节点维护事件监听、样式计算和渲染信息。优化策略:合并相似元素:用 <path> 代替多个 <rect>,一条路径一个 DOM 节点虚拟化渲染:只绘制可视区域内的数据点,滚动时动态更新。D3 社区有 d3-visualization-virtual-scroller 等方案简化路径:使用 d3.line().curve() 生成的路径比手工贝塞尔曲线更精简使用 requestAnimationFrame 批量更新避免在循环中逐个更新 SVG 属性,把更新集中在一帧内:function updateChart(newData) { requestAnimationFrame(() => { svg.selectAll('rect') .data(newData) .attr('y', d => yScale(d)) .attr('height', d => 260 - yScale(d)); });}关闭不可见元素的事件监听大量数据点时,给每个点绑定事件监听开销很大。改用事件委托:svg.on('mousemove', function(event) { const target = event.target; if (target.classList.contains('data-point')) { const data = d3.select(target).datum(); showTooltip(event, data); }});只给 SVG 容器绑定一个事件,通过 event.target 判断实际触发的元素,从 N 个监听器降为 1 个。CSS will-change 提示对频繁动画的元素加上 will-change: transform,让浏览器提前创建合成层:.chart-bar { will-change: transform, opacity;}但不要滥用——每个 will-change 都会消耗显存,只用在确实需要动画的元素上。SVG vs Canvas:什么时候该换?这个选择取决于你的数据规模和交互需求。SVG 的优势场景:数据点少于 3000-5000 个需要每个元素独立的悬停、点击事件需要屏幕阅读器可访问(每个元素都可以加 ARIA 标签)需要无损缩放,图表可能在不同分辨率设备上展示需要 CSS 样式控制Canvas 的优势场景:数据点超过 5000 个(热力图、散点图、实时数据流)需要每秒 60 帧的持续重绘(粒子动画、实时股价图)不需要单独操作每个数据元素对无障碍访问没有强制要求混合方案:一些图表库(如 Highcharts、ECharts)提供了 SVG/Canvas 双模式切换。你也可以在同一个页面中混合使用——用 SVG 画坐标轴和标签(需要清晰缩放和交互),用 Canvas 画数据密集区域(需要高性能渲染)。一个简单的判断流程:先用 SVG,如果性能不够再考虑 Canvas。过早切换到 Canvas 会丢失 SVG 的交互便利性和可访问性,开发成本也会上升。总结SVG 图表开发的路线是清晰的:先用原生标签理解坐标系和基本图形,再用 D3.js 的数据绑定和比例尺处理动态数据,通过 viewBox 实现响应式,用 CSS 动画或 D3 transition 加上动效,最后用事件绑定实现交互。性能瓶颈出现时,先从减少 DOM 节点、事件委托、批量更新入手,数据量超出 SVG 承受范围再切换 Canvas。掌握这些层次,你就不再受限于图表库的配置项,而是能从底层精确控制图表的每个细节。