标签

SVG

可伸缩矢量图形(SVG)是一种基于XML的二维矢量图形格式,也可以在HTML中使用。

SVG
查看更多相关内容
前端5月28日 05:32
如何在 JavaScript 中操作 SVG?核心方法与常见坑用 JavaScript 操控 SVG,本质就是操作 DOM——只不过多了个命名空间的坑。SVG 元素挂在 DOM 树上,所以 `querySelector`、`addEventListener` 这些老朋友都能用,但创建元素时必须用 `createElementNS`,这是新手最容易栽的地方。本文覆盖 SVG 元素选择、属性修改、事件绑定、动画实现、坐标换算、拖拽交互这些核心操作,顺带聊几个实际开发中踩过的坑。 ### 命名空间:第一个坑 HTML 元素用 `document.createElement('div')` 就行,SVG 不行——你必须指定命名空间: ```javascript 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`,它有自己的命名空间: ```javascript const XLINK_NS = 'http://www.w3.org/1999/xlink'; useEl.setAttributeNS(XLINK_NS, 'xlink:href', '#icon'); ``` 新规范中 `href` 已经可以直接用 `setAttribute` 设置,但兼容旧浏览器时还是得走 `xlink`。 ### 选择元素:和 HTML 一样 SVG 元素的选择没有特殊之处,标准 DOM API 直接用: ```javascript 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`)和 **样式属性**。两者都可以改,但走不同的路: ```javascript // 方式一: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`: ```javascript circle.setAttribute('tabindex', '0'); circle.addEventListener('keydown', (e) => { if (e.key === 'Enter') { circle.setAttribute('fill', 'red'); } }); ``` 对于大量同类元素(比如数据可视化中的几十个柱子),逐个绑定事件很浪费内存,用事件委托更合理: ```javascript svg.addEventListener('click', (e) => { const bar = e.target.closest('rect.bar'); if (bar) { highlightBar(bar); } }); ``` ### 动画:CSS 过渡 vs requestAnimationFrame 简单动画用 CSS 过渡就够了,改个属性值浏览器自动补间: ```javascript circle.style.transition = 'all 0.3s ease'; circle.setAttribute('r', '80'); ``` 需要精确控制的动画(比如沿路径运动、物理模拟)则要用 `requestAnimationFrame`。一个容易犯的错是用 `setInterval`——它不跟浏览器刷新率同步,动画会卡顿。`requestAnimationFrame` 的回调在浏览器下一次重绘前执行,能保证流畅: ```javascript 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 内部坐标需要做矩阵变换: ```javascript 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`,这一步是必须的。 ### 拖拽实现 拖拽是把鼠标坐标换算和事件监听组合起来的典型场景: ```javascript 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% 的体积,线上必须走一遍。
服务端5月27日 15:48
SVG 和 Canvas 有什么区别?什么时候用哪个?SVG 和 Canvas 都能在网页上绘图,但底层原理完全不同: **SVG 是矢量图,基于 DOM。** 每个图形都是一个独立的 DOM 节点,可以用 CSS 设样式、用 JS 绑事件,浏览器负责渲染和重绘。放大缩小永远清晰,因为存的是数学描述而非像素点。 **Canvas 是位图,基于像素。** 你通过 JS 调用绘图 API 在画布上逐像素绘制,画完浏览器就不管了——它不记得你画了什么,只保存最终那张位图。要改东西就得清空重画。 这个根本差异决定了它们在性能、交互、可访问性上的所有不同。 ## 7 个维度的详细对比 ### 1. 渲染机制 SVG 绘制的每个元素都保留在 DOM 树中。你画了一个圆,它就是一个 `<circle>` 节点,属性改了浏览器自动重绘。 Canvas 只有一个 `<canvas>` 标签,内部全靠 JS 维护状态。你画了一万个圆,DOM 里还是只有一个元素。 ```html <!-- 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 上模拟了对象模型,本质上还是在做碰撞检测。 ```js // 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 下来: ```js 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. 可访问性与 SEO SVG 内容是 DOM 节点,屏幕阅读器可以读取,搜索引擎可以索引文字内容。可以添加 `<title>` 和 `<desc>` 标签增强无障碍支持。 Canvas 对屏幕阅读器不可见。要支持无障碍,需要额外写 ARIA 标签或在画布外提供替代文本。搜索引擎也无法抓取 Canvas 中的内容。 如果页面内容需要被搜索到,SVG 是更好的选择。 ### 6. 动画实现 SVG 动画可以用 CSS 动画、SMIL 或 JS 操纵 DOM 属性。简单动画实现起来很直观: ```css /* 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。这个回答结构清晰,覆盖面试官可能追问的所有方向。
服务端5月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 <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 <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 示例 ```javascript const circle = document.querySelector('circle'); let position = 50; function animate() { position += 1; circle.setAttribute('cx', position); if (position < 150) { requestAnimationFrame(animate); } } animate(); ``` ### GSAP 示例 ```javascript 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 的灵活性: ```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。关键是根据动画复杂度、交互需求和兼容性要求做出权衡。
服务端5月27日 15:43
SVG 性能优化有哪些常用方法?## 为什么需要优化 SVG SVG 是前端开发中常用的矢量图形格式,但未经优化的 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 默认就是 black - `stroke-width="1"` — 默认值即为 1 - `stroke-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` ```text <!-- 优化前:绝对坐标 + 高精度 --> <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,支持插件化配置,能自动完成上述所有代码层面的优化: ```bash # 单文件优化 npx svgo input.svg -o output.svg # 批量优化整个目录 npx svgo -f ./icons -o ./optimized # 指定精度为 2 位小数 npx svgo input.svg -o output.svg --precision 2 ``` SVGO 默认插件包括移除元数据、移除注释、合并路径、转换样式等,大多数场景直接使用默认配置即可获得 50%-70% 的体积缩减。 ### SVGOMG 在线工具 如果不想安装命令行工具,SVGOMG 是 SVGO 的 Web 界面版本,可以在浏览器中实时预览优化效果,逐项开关插件并查看体积变化,适合偶尔使用或快速验证。 ### 服务器压缩 SVG 是纯文本的 XML 格式,gzip 和 Brotli 压缩效果极好: - gzip 压缩通常可再减小 60%-70% - Brotli 比 gzip 再额外节省 10%-15% - 配置 Nginx 开启 Brotli 后,一个 12KB 的 SVG 传输时可能只有 2-3KB ```nginx # Nginx 开启 gzip 压缩 SVG gzip 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 请求: ```html <!-- 定义 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>` 定义一次,多次引用: ```html <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 控制显示尺寸,实现响应式适配: ```html <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 动画性能更好,且兼容性更可控 ```css /* 推荐: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 配置 ```bash npm install svgo svgo-loader --save-dev ``` ```js // webpack.config.js module.exports = { module: { rules: [ { test: /\.svg$/, use: ['@svgr/webpack', 'svgo-loader'] } ] } } ``` ### Vite 配置 ```bash npm install vite-plugin-svgr --save-dev ``` ```js // vite.config.js import 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,对首屏加载速度的影响不可忽视。
服务端5月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` 没有本质区别: ```css .icon-circle { fill: #3b82f6; stroke: #1e40af; stroke-width: 2; opacity: 0.9; } ``` ```html <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 的各种选择器都能用: ```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` 就能做出丝滑的交互效果: ```css .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; } ``` ```html <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 时变色、展开时位移,简单高效: ```css .sidebar-arrow { transition: transform 0.3s ease; transform-origin: center; } .sidebar-arrow.open { transform: rotate(90deg); } ``` JavaScript 切换 `.open` 类名即可,不需要操作 SVG 属性。 ### 关键帧动画(@keyframes) 需要持续或循环的效果用 `@keyframes`: ```css .spinner { animation: spin 1s linear infinite; transform-origin: center; } @keyframes spin { to { transform: rotate(360deg); } } ``` ```css .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,线就"画"出来了。 ```css .draw-path { stroke-dasharray: 300; stroke-dashoffset: 300; animation: draw 2s ease forwards; } @keyframes draw { to { stroke-dashoffset: 0; } } ``` ```html <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 样式变得可配置,一套图形换个主题色只需改几个变量: ```css :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); } ``` 在暗色模式下覆盖变量即可,不用写重复的选择器: ```css @media (prefers-color-scheme: dark) { :root { --icon-primary: #60a5fa; --icon-stroke: #93c5fd; --icon-hover: #f87171; } } ``` ## 7. 外部样式表与样式分离 小型项目可以在 SVG 的 `<style>` 标签里写 CSS,但项目规模大了以后,把 SVG 样式抽到外部 CSS 文件更合理——和 HTML 样式统一管理,方便复用和压缩: ```html <!-- HTML --> <link rel="stylesheet" href="svg-styles.css" /> <svg viewBox="0 0 24 24" class="icon"> <path class="icon-path" d="..." /> </svg> ``` ```css /* 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 媒体查询,可以实现真正的响应式图形: ```html <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> ``` ```css .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% 的需求。
服务端5月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 逐帧绘制像素,画完之后不保留图形对象。 ```html <!-- 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 做渐进增强): ```html <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 组件方案: ```html <!-- 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**(性能可控)。三者不是互斥关系,一个页面中同时使用三种方案是常见做法。
服务端5月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 <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 的三个核心作用: 1. **解耦绘图尺寸与渲染尺寸**:图标设计常用 `viewBox="0 0 24 24"`,因为 24 的网格便于对齐和计算,实际显示大小由外部 CSS 控制。 2. **实现响应式缩放**:设置 `width="100%"` 配合 viewBox,SVG 自动适配容器大小,内部坐标无需改动。 3. **控制可视区域**:调整 `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 <!-- 居中完整显示,保持比例(默认值) --> <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。 ```svg <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) 旋转。 ```svg <!-- 围绕元素中心旋转 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`。 ```svg <!-- 等比放大 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 轴方向倾斜指定角度。 ```svg <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` 属性中,按从右到左的顺序依次应用(矩阵乘法的右乘规则): ```svg <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 <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)。三个圆的坐标都相对于这个新原点。 嵌套可以多层叠加,外层变换会传递到内层: ```svg <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 坐标变换在动画场景有几个容易踩的坑: 1. **CSS 动画和 SVG 属性动画的坐标系不同**:用 CSS `@keyframes` 做 transform 动画时,变换原点遵循 CSS 规则;用 SVG 的 `<animateTransform>` 时遵循 SVG 规则。两者混用会导致意外行为。 2. **transform 不可叠加**:SVG 的 `<animateTransform>` 的 `additive` 属性默认为 `replace`,多个动画会互相覆盖。需要 `additive="sum"` 才能叠加。 3. **缩放动画的坐标偏移**:对带有 `x`、`y` 属性的元素做 scale 动画,元素会向原点方向移动(因为坐标值也被缩放了)。解决方案是用 `<g>` 包裹,对 `<g>` 做动画。 ## 实际开发中的核心要点 1. **始终设置 viewBox**:让 SVG 具备响应式能力,避免硬编码固定尺寸。图标用 `viewBox="0 0 24 24"` 或 `viewBox="0 0 16 16"`,通过 CSS 控制实际显示大小。 2. **用 viewBox 而非 width/height 控制缩放**:设置 `width="100%"` 或不设宽高,通过 CSS 控制尺寸,viewBox 负责逻辑坐标映射。 3. **注意 transform 顺序**:不同顺序产生不同结果,推荐 SRT 顺序(Scale -> Rotate -> Translate)。 4. **用 `<g>` 组织和变换元素组**:减少重复代码,提高可维护性,也便于动画控制。 5. **SVG 旋转需指定中心点**:CSS 中可以 `transform-origin: center`,SVG 中必须手动写 `rotate(angle, cx, cy)` 或用 translate 模拟。 6. **避免在 SVG 内部使用百分比坐标**:百分比在 SVG 中的计算规则复杂,优先使用 viewBox 内的绝对坐标值。 7. **CSS transform 和 SVG 属性 transform 不要混用**:在同一元素上同时设置两者,行为在不同浏览器中可能不一致。选一种方式贯彻到底。
服务端5月27日 15:35
SVG 的 clipPath 裁剪和 mask 蒙版有什么区别?怎么用?SVG 里有两套视觉裁切机制:`clipPath` 和 `mask`。前者做硬边缘裁剪,后者做透明度遮罩。前端面试经常考两者的区别,实际开发中圆形头像、文字镂空、渐变淡出也都靠它们实现。这篇文章从语法、属性、CSS 联动到实战案例,逐一拆解。 ## clipPath:硬边缘裁剪 `clipPath` 的逻辑很简单:定义一个封闭区域,区域内的内容保留,区域外的内容直接消失,不存在半透明过渡。 ### 基本语法 ```svg <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,表示相对比例。 ```svg <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**:奇偶规则,适合有镂空的复合形状 ```svg <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 <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 <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 <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 还是 alpha `mask-type` 决定了 mask 的计算方式: - **luminance**(默认):根据颜色的亮度值计算透明度。白色=不透明,黑色=透明,灰色=半透明 - **alpha**:直接使用颜色的 alpha 通道,忽略颜色本身 ```svg <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 的坐标参照什么 ```svg <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 <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 <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 元素。 ```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: ```css .avatar { clip-path: circle(50%); } .banner { clip-path: polygon(0 0, 100% 0, 100% 80%, 0 100%); } ``` 但自定义复杂路径仍然需要 SVG `clipPath`。两者配合使用是最灵活的方案。 CSS `mask` 属性同样可以引用 SVG mask,还可以用图片做遮罩: ```css .card { mask-image: linear-gradient(to bottom, white, transparent); -webkit-mask-image: linear-gradient(to bottom, white, transparent); } ``` ## 组合使用:圆形裁剪 + 渐变淡出 ```svg <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 <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`: ```css .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>` 引用外部形状有历史 bug - Safari 对 CSS `mask` 需要加 `-webkit-` 前缀 - `clipPathUnits="objectBoundingBox"` 在老版本 WebKit 中可能有精度问题 - CSS `clip-path` 作用于 HTML 元素时,Firefox 早期版本需要额外的 SVG 引用处理 生产环境中建议在关键路径上做特性检测或提供降级方案,Safari 的 `-webkit-` 前缀别忘了加。 ## 实际业务中怎么选 选型其实不复杂,记住两条原则: 1. 只要不需要透明度过渡,就用 `clipPath`。性能好,语义清晰,浏览器计算快。 2. 需要渐变、半透明、软边缘时,才上 `mask`。代价是渲染开销更大。 几个典型业务场景的选型参考: - 用户头像裁成圆形 → `clipPath`,硬边缘就够了 - 文字 banner 镂空渐变 → `clipPath` 做文字裁剪 + 渐变填充 - 卡片底部淡出效果 → `mask`,需要从实到虚的渐变 - 鼠标跟随聚光灯 → `mask` + 径向渐变 - 斜切/波浪形分割线 → `clipPath` 或 CSS `clip-path: polygon()`,不需要半透明 如果项目已经大量使用 CSS `clip-path` 和 `mask-image`,SVG 定义可以作为复杂路径的补充,两者并不冲突。
服务端5月27日 15:31
SVG 路径命令怎么用?M/C/Q/A 每个命令的语法和原理详解SVG 路径(`<path>`)是 SVG 中功能最强大的绘图元素,通过 `d` 属性里的一组命令来描述任意形状。无论是简单直线还是复杂曲线,都可以用路径命令精确表达。本文按命令类别逐一讲解每条命令的语法、坐标规则和实际绘制效果。 ## 路径命令的基本规则 路径命令由一个字母加上若干数字组成,写在 `<path>` 元素的 `d` 属性中: - **大写字母**表示绝对坐标,数值参照 SVG 画布原点。 - **小写字母**表示相对坐标,数值参照当前画笔位置(即上一条命令的终点)。 - 连续使用同一命令时,命令字母可省略,只写参数。 - 命令之间的空格和逗号可以省略,但保留空格有助于可读性。 可以把路径命令想象成一支虚拟画笔:`M` 把笔尖移到某个位置但不落笔,`L`、`C`、`A` 等命令则从当前位置画线到新位置,`Z` 把笔尖拉回起点闭合路径。 ## 移动命令 M / m `M` 是每条路径的起点,把画笔移动到指定坐标,不产生线条。 ```svg M 50 50 ``` 这表示将画笔移到绝对坐标 (50, 50)。小写 `m 10 0` 则表示从当前位置向右移动 10 个单位。路径必须以 `M` 或 `m` 开头,否则浏览器无法确定起始位置。 ## 直线命令 L / H / V ### L —— 画直线到指定点 `L x y` 从当前位置画一条直线到 (x, y),是最常用的画线命令。 ```svg 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 坐标不变。 ```svg M 10 10 H 90 V 90 H 10 Z ``` 这个路径画了一个矩形:从 (10,10) 水平画到 (90,10),垂直画到 (90,90),水平画到 (10,90),最后 `Z` 闭合回起点。 ## 三次贝塞尔曲线 C / S ### C —— 两个控制点的曲线 三次贝塞尔曲线是路径中最常用的曲线命令,语法为: ```svg C x1 y1, x2 y2, x y ``` 它需要两个控制点 (x1, y1) 和 (x2, y2) 以及一个终点 (x, y)。起点是上一条命令的终点。控制点不在曲线上,它们像磁铁一样把曲线拉向自己的方向: - 第一个控制点 (x1, y1) 决定曲线离开起点时的切线方向。 - 第二个控制点 (x2, y2) 决定曲线进入终点时的切线方向。 ```svg M 10 80 C 40 10, 65 10, 95 80 ``` 这条曲线从 (10, 80) 出发,被第一个控制点 (40, 10) 向左上方拉,又被第二个控制点 (65, 10) 从右上方拉向终点 (95, 80),形成一个 S 形弧线。 ### S —— 平滑三次贝塞尔曲线 当需要连续画多段曲线并保持衔接处平滑时,用 `S` 可以省略一个控制点: ```svg S x2 y2, x y ``` `S` 会自动将前一段曲线的第二个控制点关于当前起点做对称,作为本段曲线的第一个控制点。这样就保证了连接处的切线方向一致,曲线不会出现尖角。 ```svg 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 / T ### Q —— 一个控制点的曲线 二次贝塞尔曲线只需一个控制点,语法更简单: ```svg Q x1 y1, x y ``` 控制点 (x1, y1) 同时影响起点和终点的切线方向。曲线弯曲程度比三次贝塞尔曲线弱,适合画简单的弧线。 ```svg M 10 80 Q 95 10, 180 80 ``` ### T —— 平滑二次贝塞尔曲线 和 `S` 的思路一致,`T` 自动推算控制点: ```svg T x y ``` `T` 会取前一段 `Q` 或 `T` 的控制点关于当前起点的对称点作为新的控制点,保证平滑衔接。 ```svg M 10 80 Q 95 10, 180 80 T 350 80 ``` 如果 `T` 不是跟在 `Q` 或 `T` 后面使用,控制点会和终点重合,画出直线。 ## 椭圆弧命令 A 椭圆弧是路径命令中最复杂的一个,语法为: ```svg 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` 决定画弧的方向,两个标志组合起来唯一确定一条弧线。 ```svg M 80 80 A 45 45 0 0 0 125 125 ``` 这条弧线以 45×45 的圆(rx=ry 即为圆弧),不旋转,选择小弧、逆时针方向,从 (80,80) 画到 (125,125)。 如果把 `large-arc-flag` 改为 1,会画出同一起终点之间大于 180° 的那段弧: ```svg M 80 80 A 45 45 0 1 0 125 125 ``` ## 闭合路径 Z `Z` 或 `z` 从当前位置画一条直线回到路径起点,闭合整条路径。大小写效果相同。 ```svg 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 怎么选?** 先确定需要大弧还是小弧(看弧线是否超过半圆),再确定绘制方向。两个标志各有两种取值,共四种组合,只有一种符合你要的弧线。
服务端5月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="img" aria-label="搜索图标"> <path d="M15.5 14h-.79l-.28-.27..." /></svg>```**纯装饰性 SVG**(背景花纹、分隔线装饰)用 `role="presentation"` 加 `aria-hidden="true"`:```svg<svg role="presentation" aria-hidden="true"> <circle cx="50" cy="50" r="40" fill="blue" /></svg>```装饰性 SVG 千万别加描述,否则屏幕阅读器会读出一堆无意义的内容,反而干扰用户。这个在 WebAIM 的年度调查里是高频错误——很多页面上几十个装饰图标全被读出来,用户听得一头雾水。## 交互式 SVG 必须支持键盘如果 SVG 有点击、拖拽等交互,就必须让键盘用户也能操作。核心就两步:让它可聚焦,让它可触发。**可聚焦**用 `tabindex="0"`:```svg<svg tabindex="0" role="button" aria-label="点击切换颜色"> <circle id="my-circle" cx="100" cy="100" r="50" fill="blue" /></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<svg role="img" aria-labelledby="chart-title chart-desc"> <title id="chart-title">月度销售趋势</title> <desc id="chart-desc">折线图显示1月到6月销售持续增长</desc> <g role="list" aria-label="数据点"> <g role="listitem" aria-label="1月:50万"> <circle cx="50" cy="150" r="5" fill="blue" /> </g> <g role="listitem" aria-label="2月:80万"> <circle cx="100" cy="120" r="5" fill="blue" /> </g> </g></svg>```用 `role="list"` 和 `role="listitem"` 把数据点组织成列表,屏幕阅读器会逐个播报每个数据点的含义,比一段笼统的描述强得多。## 响应式文本也别忽略SVG 里的文字要保证放大后依然可读。用 `viewBox` 配合百分比宽度就行:```svg<svg viewBox="0 0 200 100" width="100%" height="auto"> <text x="100" y="50" font-size="16" text-anchor="middle" font-family="Arial, sans-serif"> 可读的文本 </text></svg>```关键是 `viewBox` 要设,`width` 用 `100%`,`height` 用 `auto`。这样用户放大页面时文字跟着缩放,不会出现溢出或截断。## 测试才是最终的检验标准代码写得再规范,不上屏幕阅读器跑一遍心里都没底。常见测试组合:- **macOS**: VoiceOver(按 Cmd+F5 开启)- **Windows**: NVDA(免费)或 JAWS- **移动端**: iOS VoiceOver / Android TalkBack重点关注这几个场景:装饰性 SVG 是否被正确跳过、信息性 SVG 的描述是否准确完整、交互式 SVG 能否用键盘正常操作。自动化工具如 Lighthouse 和 axe 能扫出大部分基础问题,但语义是否准确还得人工验证。面试里被问到 SVG 可访问性,按照"描述 → 角色 → 键盘 → 对比度 → 测试"这个思路答,基本就覆盖了核心考点。实际项目里记得把这些实践落实到组件库和代码规范中,别让可访问性变成上线前才补的债。
服务端5月27日 14:41
SVG 中的文字怎么排版?text、tspan、textPath 各自解决什么问题网页里的 SVG 图标大家都用过,但一提到 SVG 里放文字,很多人就犯难:换行怎么做?沿曲线排列怎么搞?中文字体加载怎么保证?这几个问题背后,对应的是 SVG 文本体系里三个核心元素——`<text>`、`<tspan>`、`<textPath>`,以及一整套定位和对齐属性。理解它们的分工,SVG 文字排版就不再靠猜。 ## text:SVG 文本的基础容器 `<text>` 是 SVG 里唯一原生的文本渲染元素。它和 HTML 里的文本最大区别是——**不会自动换行**。你写多少字符,它就渲染成一行,超出的部分直接溢出容器。 ```svg <svg width="400" height="60"> <text x="20" y="40" font-size="24" fill="#333">这段文字不会自动换行</text> </svg> ``` ### 定位属性:x / y 与 dx / dy `x` 和 `y` 是绝对坐标,指定文本起始点在 SVG 画布上的位置。注意 `y` 指的是文字基线(baseline)的纵坐标,不是文字顶部,所以新手经常会发现文字比预期位置偏下——这是基线定位导致的。 `dx` 和 `dy` 是相对偏移,从"当前文本位置"出发做增量。在 `<text>` 上单独使用时,效果和 `x`/`y` 类似,但在 `<tspan>` 里配合使用时,才是它真正的价值所在——后面会展开。 ### rotate:逐字符旋转 `rotate` 接受一组角度值,按顺序分配给每个字符: ```svg <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 实现多行文本 ```svg <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>` 可以单独设置颜色、字号、字重等,不影响兄弟节点: ```svg <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` 是基于前一个字符位置的偏移,适合做下标、上标或微调间距: ```svg <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 <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` 决定文本从路径的哪个位置开始排列,支持百分比和绝对长度: ```svg <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-baseline SVG 文本的对齐控制比 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-shift `alignment-baseline` 控制 `<tspan>` 相对父元素的基线对齐方式,`baseline-shift` 做上下偏移——用来做上标下标很方便。不过 `baseline-shift` 正在被 CSS `vertical-align` 替代,新项目建议直接用 CSS。 ## 字体引用:@font-face 与 foreignObject SVG 里的字体加载和 HTML 共享同一套机制,但有细微差别。 ### @font-face 在 SVG 中的使用 在 HTML 页面里内联的 SVG,直接使用页面的 `@font-face` 声明即可,字体加载没有额外问题。但如果 SVG 作为 `<img>` 标签引用,浏览器出于安全限制会**阻止加载外部字体**——这是最常见的坑。 解决方案:把字体文件转成 Base64 嵌入 SVG 内部的 `<style>` 中: ```svg <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` 等属性实现自动换行: ```svg <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 <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: ```bash pyftsubset NotoSansSC-Regular.ttf --text-file=chars.txt --output-file=NotoSansSC-subset.woff2 --flavor=woff2 ``` 导出 SVG 图片时,子集化几乎是必须的,否则要么字体加载失败,要么文件体积爆炸。 ### SVG 作为图片引用时的中文字体问题 作为 `<img>` 标签引用时,SVG 无法加载外部字体,中文字符会回退到浏览器默认字体。两种解法: 1. **Base64 嵌入子集字体**:把子集化后的字体 Base64 编码写进 SVG 的 `<style>`,兼容性最好 2. **文字转路径**:用设计工具或 `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>` 手动分行。
服务端5月27日 14:40
SVG 有哪些基本形状元素,它们各自的属性和用法是什么## 为什么需要了解 SVG 基本形状 用 CSS 画一个圆角矩形要写一堆 border-radius,用 Canvas 画一个多边形要手动管理路径状态。SVG 不同——它为常见图形提供了专门的元素,写法直观,浏览器直接渲染,还能无损缩放。理解这六个基本形状和 path 元素,是用好 SVG 的前提。 ## 六种基本形状 ### rect:矩形 `<rect>` 画矩形,是最常用的形状元素之一。 核心属性: - `x` / `y`:矩形左上角的坐标,默认 0 - `width` / `height`:宽和高 - `rx` / `ry`:圆角半径 ```svg <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`:圆心坐标,默认 0 - `r`:半径 ```svg <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 轴半径 ```svg <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`:终点坐标 ```svg <line x1="0" y1="0" x2="200" y2="150" stroke="#8E44AD" stroke-width="3"/> ``` 直线没有填充,只有描边。如果忘记写 `stroke`,线段不可见——这是初学者最常踩的坑。 ### polyline:折线 `<polyline>` 画一系列连续线段,不自动闭合。 核心属性: - `points`:点序列,格式为 "x1,y1 x2,y2 x3,y3 ..." ```svg <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`:点序列 ```svg <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 就能画多边形: ```svg <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 | 三次贝塞尔示例: ```svg <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`:终点坐标 ```svg <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`:描边宽度,默认 1 - `stroke-linecap`:线段端点样式,butt(默认)/ round / square - `stroke-linejoin`:折点连接样式,miter(默认)/ round / bevel - `stroke-dasharray`:虚线模式,如 "5,3" 表示 5px 实线 3px 间隔 - `opacity` / `fill-opacity` / `stroke-opacity`:整体或分项透明度 ## 形状组合与变换 ### g 元素分组 `<g>` 把多个形状打包成一组,可以统一设置样式和变换: ```svg <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)`:倾斜 ```svg <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 Z` - ellipse 转 path:同 circle,把 r 换成 rx/ry - line 转 path:`M x1,y1 L x2,y2` - polyline 转 path:`M` 到第一个点,然后 `L` 到后续每个点 - polygon 转 path:同 polyline,末尾加 `Z` 前端可以用 Jarek Foksa 的 `path-data` polyfill 或在线工具(如 SVG Shape to Path Converter)完成批量转换。 ## 怎么选择合适的形状元素 简单规则:能用基本形状就用基本形状,语义更清晰、代码更短。需要曲线或复杂图形时才用 path。圆角矩形用 rect 的 rx/ry 比用 path 手拼 A 命令简单得多。需要做路径动画或变形时,再考虑把基本形状转成 path。
服务端5月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+ 渲染矢量图。使用时有几个必须注意的点: 1. **布局中用 `app:srcCompat` 代替 `android:src`**。后者在 4.x 上会直接报错,因为系统不认识矢量资源类型。 2. **构建时自动生成 PNG 回退**。在 `build.gradle` 中配置 `vectorDrawables.useSupportLibrary = true` 可以禁用自动 PNG 生成,减小包体积,但前提是所有地方都用了 compat 方式加载。如果不全用 compat,就不要开这个选项。 3. **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 在移动端的价值才能真正发挥出来。
服务端5月27日 14:37
SVG 在网页中有哪些使用方式,各自的优缺点是什么## 为什么 SVG 的使用方式这么重要 SVG 是前端开发中唯一一种"同一份资源,七八种嵌入姿势"的图片格式。选错方式,轻则图标颜色改不动、动画跑不起来,重则首屏渲染卡顿、多页面重复传输几十 KB 冗余标记。理解每种方式的边界条件,才能在具体项目中做出合理取舍。 ## Inline SVG:直接写在 HTML 里 把 `<svg>` 标签直接嵌入 HTML 文档,是最"裸"的用法: ```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 文件 最接近传统图片用法的姿势: ```html <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 当作装饰性背景使用: ```css .icon-search { width: 24px; height: 24px; background: url("icon.svg") no-repeat center / contain; } ``` 也可以用 Data URI 直接嵌入: ```css .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 标签 这两种方式属于"老派"做法,但在特定场景下仍有价值: ```html <!-- 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 文件: ```html <!-- 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>` 引用: ```html <!-- 内联 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 <!-- 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 形式的 SVG - SVG 内容越长,Data URI 越臃肿,超过 2-3 KB 就不值得了 **适用场景**:极少量小图标、CSS 中需要内联简单图形、不想额外维护 SVG 文件的快速原型。 ## iframe 方式 用 iframe 加载独立 SVG 文件: ```html <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 本身就是为了解决不同场景而存在多种嵌入方式的。
服务端5月27日 14:37
SVG 的 defs 和 use 怎么配合实现图形复用?当你手写 SVG 时,有没有遇到过这样的情况:同一个图标在页面里复制粘贴了七八次,改一个颜色就要全局替换?SVG 的 `<defs>` 和 `<use>` 就是用来解决这个问题的——把图形定义一次,到处引用。 ## defs:定义但不渲染 `<defs>` 是一个纯容器元素,它内部的所有子元素都不会直接显示在画布上。它的作用只有一个:给后续的引用提供"模板"。 ```xml <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`,在文档中创建该元素的一个实例: ```xml <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`,定义了自己的视口和缩放策略。 ```xml <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,让它级联** ```xml <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 做双色调图标** ```xml <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"`: ```xml <svg aria-hidden="true" class="icon"> <use href="#icon-close" /> </svg> ``` ## 跨文件引用 `<use>` 的 `href` 可以指向外部 SVG 文件中的元素: ```xml <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>` 最典型的应用场景。一个常见的架构: ```xml <!-- 放在页面 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 只需要一行就能控制图标颜色: ```css .icon { color: inherit; } /* 自动跟随按钮文字颜色 */ ``` 构建工具通常会把 `src/icons/` 目录下的独立 SVG 文件自动合并成上面的精灵文件,开发时每个图标仍是单独的文件,构建时自动拼合。 ## 实际应用:背景图案 `<defs>` 的另一个经典用法是定义 `<pattern>`,配合 `<use>` 或直接填充实现重复图案: ```xml <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 中也可以直接引用: ```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` 优势这几个关键点,就能在图标系统和图案复用中用好这套机制,而不是在代码里反复复制粘贴同一个图标的路径数据。
服务端5月27日 14:37
SVG 怎样实现渐变和滤镜效果?## 为什么 SVG 需要渐变和滤镜 纯色填充和简单描边只能解决最基本的视觉需求。当设计要求柔和过渡的光影、逼真的投影、或是非写实的色彩处理时,SVG 的渐变和滤镜才是真正的答案——它们让矢量图形脱离"扁平图标"的刻板印象,具备接近位图编辑软件的表现力,同时保留分辨率无关的优势。 ## 线性渐变 linearGradient 线性渐变沿一条直线方向过渡颜色。它定义在 `<defs>` 内部,通过 `x1/y1/x2/y2` 控制渐变线的起止坐标。 ```xml <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 径向渐变从一个中心点向外辐射,适合做球体高光、聚光灯等效果。 ```xml <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`: ```xml <stop offset="30%" stop-color="#e74c3c" /> <stop offset="60%" stop-color="#e74c3c" /> ``` 这样从 30% 到 60% 都是纯红色,两侧才产生过渡。 ### gradientUnits `gradientUnits` 决定坐标是相对于元素本身还是整个视口: - `objectBoundingBox`(默认):坐标 0–1 映射到元素的边界框 - `userSpaceOnUse`:使用 SVG 画布的绝对坐标 当多个元素共享同一个渐变但尺寸不同时,`userSpaceOnUse` 能保证一致的渐变范围;`objectBoundingBox` 则自动适配每个元素。 ### gradientTransform 和 spreadMethod `gradientTransform` 允许对渐变坐标施加矩阵变换(旋转、缩放等),等同于 CSS 的 `transform`。`spreadMethod` 控制渐变范围外的填充方式:`pad`(默认,延伸最后一色)、`repeat`(重复)、`reflect`(镜像翻转重复)。 ## filter 滤镜的工作原理 SVG 滤镜基于"图元管道"(filter primitive pipeline)模型:每个滤镜原语接收输入图像,处理后输出结果,下一个原语再接续处理。整条管线定义在 `<filter>` 元素中,放在 `<defs>` 里。 ```xml <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` 控制模糊半径,值越大越模糊: ```xml <filter id="blur1"> <feGaussianBlur in="SourceGraphic" stdDeviation="5" /> </filter> ``` 可以分别指定水平和垂直方向的模糊:`stdDeviation="8 2"` 表示水平模糊 8px、垂直 2px。高斯模糊是构建投影、发光等效果的基础——先把图形模糊,再和原图叠加。 ## 投影 feDropShadow `feDropShadow` 是一个复合原语,内部等价于 feOffset + feGaussianBlur + feFlood + feComposite 的组合: ```xml <filter id="shadow1"> <feDropShadow dx="4" dy="4" stdDeviation="3" flood-color="#000000" flood-opacity="0.4" /> </filter> ``` `dx/dy` 控制偏移,`flood-color/flood-opacity` 控制阴影颜色和透明度。需要内阴影或更复杂的投影时,就得手动拆分上述原语组合,灵活控制每一步。 ## 颜色矩阵 feColorMatrix feColorMatrix 是 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 三个通道取加权平均: ```xml <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"` 可切换。 ## 混合 feBlend `feBlend` 将两个输入按指定模式混合,支持 `normal/multiply/screen/darken/lighten/overlay/color-dodge/color-burn/hard-light/soft-light/difference/exclusion/hue/saturation/color/luminosity` 共 16 种模式。 ```xml <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` 指定第二个输入——每一步都可以精确控制数据来源。 ## 滤镜组合实战 把多个原语串起来才能实现复杂效果。一个完整的发光+投影滤镜长这样: ```xml <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——这是最务实的分工。 ## 性能注意事项 1. **模糊是性能杀手**:`stdDeviation` 超过 20 的模糊在移动端会显著卡顿,能用小值就别用大值 2. **缩小滤镜区域**:精确设置 `<filter>` 的 `x/y/width/height`,避免对不可见区域做无用计算 3. **减少滤镜层级**:每多一个原语就多一轮像素处理,能用 3 步完成的效果别拆成 7 步 4. **避免在动画中使用复杂滤镜**:每帧都要重新计算像素,优先用 CSS transform/opacity 做动画 5. **硬件加速差异**:CSS filter 在主流浏览器中走 GPU 加速路径,SVG filter 的加速程度取决于浏览器实现,Chrome 和 Firefox 的表现好于 Safari 6. **测试移动端**:SVG filter 在低端移动设备上的性能差距会被放大,务必真机验证 --- 渐变让 SVG 拥有色彩过渡的能力,滤镜让 SVG 拥有像素级处理的能力。两者组合起来,矢量图形不再只是线条和填色——模糊、投影、色彩变换、多步合成,这些曾经需要位图编辑器才能完成的效果,现在直接写在 SVG 标记里就能实现。掌握 stop 节点控制渐变节奏,理解 filter primitive pipeline 的输入输出串联,比记住任何单个属性都重要。
服务端5月27日 14:36
SVG 怎样实现点击、拖拽、动画和无障碍交互?## 从一个静态图标说起 你在一个管理后台里放了一个 SVG 图标,产品说"点它能不能切换状态?"——于是你开始搜索 SVG 到底怎么绑定事件。接着设计说"hover 时能不能有个动画过渡?"——你发现 SVG 的动画方案不止一种。再后来需求升级到"能不能拖拽元素""能不能缩放平移画布"——你意识到 SVG 交互远比想象中复杂。这篇文章把 SVG 交互开发的核心技术一次性梳理清楚,从事件处理到动画方案,从拖拽缩放到无障碍支持,最后看 D3.js 如何把这些能力封装成数据驱动的交互模式。 ## SVG 事件处理:click、hover、mousemove SVG 元素是合法的 DOM 节点,所以你可以像操作 HTML 一样给它绑定事件。内联 SVG 直接挂在标签上就行: ```html <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 与 keyframes SVG 元素支持 CSS transition 和 animation,这是最轻量的动画方案。 ### transition 处理状态变化 ```css .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` 让浏览器以元素的填充框为参考: ```css .circle-btn { transform-box: fill-box; transform-origin: center; } ``` ### keyframes 做持续动画 描边动画是 SVG 最经典的 CSS 动画效果: ```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 与 animateTransform SMIL 是 SVG 原生的动画方案,直接写在 SVG 标签内部,不需要 CSS 也不需要 JavaScript。 ### animate 属性变化 ```xml <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 做变换 ```xml <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 的每个细节。 ### 创建和修改元素 ```javascript const svgNS = 'http://www.w3.org/2000/svg'; const svg = document.querySelector('svg'); // 创建元素必须用 createElementNS,不是 createElement const 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 ```javascript // 获取当前变换矩阵 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 做帧动画 ```javascript 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 到 drop SVG 拖拽比 HTML 拖拽多了坐标转换这一步。 ```javascript 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`。 ### 方案一:修改 viewBox ```javascript const 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` 做缩放平移,不影响其他元素: ```javascript const group = document.getElementById('layer'); group.setAttribute('transform', `translate(${tx}, ${ty}) scale(${s})`); ``` 两种方案的选择:画布级操作用 `viewBox`,元素级操作用 `transform`。实际项目中经常组合使用——`viewBox` 控制全局视口,`transform` 控制图层独立变换。 ## 无障碍交互:ARIA 与键盘支持 SVG 交互不是视觉用户的专属,屏幕阅读器和键盘用户也需要能操作。 ### 让 SVG 可聚焦 ```html <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`)。 ### 键盘事件处理 ```javascript 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 数据可视化的首选工具。 ### 事件绑定 ```javascript 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` 是原生事件对象。 ### 数据驱动更新 ```javascript 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()` 模块,封装了坐标转换和事件处理: ```javascript // 拖拽 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 交互开发能力。
服务端5月27日 14:35
如何在项目中搭建一套可维护的 SVG 图标系统?前端项目里的图标管理,往往是从"随便放几个 PNG"开始的。等到图标多了,尺寸不统一、颜色改不动、重复加载——问题一个接一个冒出来。SVG 图标系统解决的就是这件事:用一套工程化的方式,让图标的存储、引用、样式控制和更新维护都有章可循。 ## SVG Sprite 的核心原理 SVG Sprite 的思路和 CSS Sprite 类似——把多个图标合并到一个文件里,减少 HTTP 请求。不同的是,CSS Sprite 靠背景定位裁切,SVG Sprite 依赖 `<symbol>` 和 `<use>` 的引用机制,天然支持缩放和样式继承。 一个典型的 SVG Sprite 文件长这样: ```xml <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>` 引用: ```xml <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 实现 ```jsx 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 实现 ```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: ```bash npx svg-sprite --symbol --symbol-dest sprites --symbol-sprite icon.svg src/icons/*.svg ``` 它也提供 Gulp 和 Webpack 插件: ```js // gulp const 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 优化: ```bash npx @svgr/cli --icon --replace-attr-values "#000=currentColor" src/icons/home.svg ``` 输出: ```jsx 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 配置: ```js // webpack.config.js module.exports = { module: { rules: [ { test: /\.svg$/, issuer: /\.[jt]sx?$/, use: ['@svgr/webpack'], }, ], }, }; ``` Vite 配置: ```js // vite.config.js import svgr from 'vite-plugin-svgr'; export default { plugins: [svgr()], }; ``` Vue 项目可以用 `vite-svg-loader`,效果类似: ```js import svgLoader from 'vite-svg-loader'; export default { plugins: [svgLoader()], }; ``` ## 按需加载与 Tree-Shaking Sprite 方案的局限在于:不管页面用了几个图标,整个 Sprite 都会被加载。对于图标量超过 200 的大型项目,这可能导致几十 KB 的浪费。 两种解法: **方案一:拆分 Sprite**。按业务模块拆成多个 Sprite 文件(如 `common.svg`、`dashboard.svg`、`editor.svg`),页面只加载当前需要的 Sprite。 **方案二:SVGR 组件化 + 按需导入**。每个图标是独立组件,只有被 import 的图标才会打包: ```jsx 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` 做动态加载: ```js const icon = defineAsyncComponent(() => import(`../icons/${props.name}.svg?component`) ); ``` ## 图标尺寸与颜色控制 ### 尺寸 最简单的做法是组件上暴露 `size` 属性,同时设置 `width` 和 `height`。更灵活的方式是用 `em` 单位,让图标尺寸跟随字体大小: ```css .icon { width: 1em; height: 1em; } ``` 这样 `font-size: 16px` 时图标 16px,`font-size: 24px` 时图标 24px,和文字对齐非常方便。 ### 颜色 控制颜色的前提是 SVG 源文件中使用 `currentColor` 而非硬编码色值。SVGO 可以自动做这个替换: ```js // svgo.config.js module.exports = { plugins: [ { name: 'preset-default' }, { name: 'replaceAttrValues', params: { values: { '#000': 'currentColor', '#333': 'currentColor' } }, }, ], }; ``` 之后 CSS 控制颜色即可: ```css .icon { color: #333; } .icon-danger { color: #e53e3e; } .icon-muted { color: #a0aec0; } ``` 如果图标有两种颜色(比如外框 + 填充),可以用 CSS 变量区分: ```css .icon { --icon-primary: currentColor; --icon-secondary: #a0aec0; } ``` SVG 源码中对应 `fill="var(--icon-primary)"` 和 `fill="var(--icon-secondary)"`。 ## Figma 导出 SVG 的工作流 设计到开发的图标流转,Figma 是大多数团队的起点。一个高效的导出流程长这样: 1. **统一画板尺寸**:所有图标放在相同尺寸的 Frame 里(通常 24×24),保证 padding 一致。导出时选 Frame 而非内部路径,否则 padding 会丢。 2. **描边转轮廓**:导出前执行 Outline Stroke(`Cmd+Shift+O`),把 stroke 转成 fill。否则导出的 SVG 会保留 stroke 属性,后续用 `currentColor` 控制颜色会出问题。 3. **批量导出**:选中多个 Frame → Export 面板 → 格式选 SVG → 导出。图标多的时候用插件(如 Freya DS Icon Exporter)一键批量导出。 4. **命名规范**:Frame 名称就是导出文件名,用 `icon-` 前缀 + kebab-case(如 `icon-arrow-left`),和代码中的引用名保持一致。 5. **SVGO 二次优化**:Figma 导出的 SVG 可能包含多余属性(`id`、`data-name`、冗余 `<g>`),用 SVGO 清理一遍: ```bash npx svgo -f src/icons --config=svgo.config.js ``` 6. **CI 集成**:在 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 到代码的自动化流程补上最后一环,让设计变更不再是手动搬运的体力活。
服务端5月27日 14:35
如何在响应式设计中正确使用 SVG?页面在手机上变形、图标在平板上模糊、Logo 在宽屏上被拉伸——这些问题多半和 SVG 的响应式处理有关。SVG 本身是矢量格式,理论上怎么缩放都不会失真,但如果 viewBox、preserveAspectRatio 和 CSS 尺寸没有配合好,结果反而比位图更糟糕。下面逐个拆解这些关键点。 ## viewBox:SVG 响应式的基石 viewBox 定义了 SVG 内部的坐标系统和可视区域,格式是 `viewBox="min-x min-y width height"`。它不决定 SVG 的实际渲染尺寸,而是告诉浏览器"这批图形画在一个多大的虚拟画布上"。 ```html <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 控制尺寸。 ```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 和图标。 ```html <!-- 全屏英雄区背景,裁切不留白 --> <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 高度** ```css .svg-container { width: 100%; height: auto; } ``` 最简单直接,前提是有 viewBox。 **2. max-width 限制最大宽度** ```css .svg-container { width: 100%; max-width: 600px; height: auto; } ``` 在大屏上不会无限撑开,适合内容区的图表和插画。 **3. 容器查询(Container Queries)** ```css .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 的媒体查询才跟随页面视口。 ```html <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 体积增大,大量图标时不适合。 ```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 变量。 ```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">` 引用。适合图标数量多的项目。 ```html <!-- 隐藏的 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: ```css .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` 控制单元大小: ```css .pattern-bg { background-image: url('pattern.svg'); background-repeat: repeat; background-size: 60px 60px; } ``` 在小屏上可以缩小 background-size 让纹理更密集,大屏上放大让纹理更稀疏,通过媒体查询切换即可。 ## srcset 与 picture 元素配合 SVG SVG 本身是矢量的,不需要多分辨率版本,但 `<picture>` 元素在两个场景下仍然有用: **格式回退** ```html <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,实现不同裁切: ```html <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`: ```css .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 属性是否冲突——按这个顺序排查,绝大多数问题都能定位。
服务端5月27日 14:32
如何用 SVG 从零创建交互式数据图表?在网页上画一张图表,很多人第一反应是找 Chart.js 或 ECharts 这样的库,写几行配置就出图。但当你需要定制一个标尺刻度倾斜 30 度、柱子圆角渐变、悬停时弹出带箭头提示框的图表时,配置项就不够用了——你得回到 SVG 本身。SVG 是所有这些图表库的底层绘图语言,理解它意味着你能在任何场景下精确控制每一个像素。 ## SVG 图表基础:用原生标签画三种经典图表 SVG(Scalable Vector Graphics)是一种基于 XML 的矢量图形格式,浏览器可以直接渲染。它的核心优势在于:每个图形元素都是 DOM 节点,可以像操作 HTML 一样用 CSS 和 JavaScript 控制。 ### 柱状图 柱状图是最直观的图表类型。核心思路:为每条数据生成一个 `<rect>` 元素,x 坐标按索引递增,y 坐标和高度由数据值决定。 ```xml <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>`,把数据点连成线。 ```xml <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>` 上实现: ```xml <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 模式 ```javascript 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 的比例尺把这个过程标准化了: ```javascript 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]`(从画布底部到顶部)。 ### 坐标轴一键生成 ```javascript 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` 和容器样式。 ```html <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` 并更新元素属性: ```javascript 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 标签内: ```xml <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 属性: ```css .chart-bar { transform-origin: bottom; transform: scaleY(0); animation: growUp 0.6s ease-out forwards; } @keyframes growUp { to { transform: scaleY(1); } } ``` ```xml <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,最适合数据驱动的动画场景: ```javascript 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)); ``` 数据更新时的过渡效果: ```javascript bars.transition() .duration(400) .attr('y', d => yScale(d)) .attr('height', d => 260 - yScale(d)); ``` D3 的过渡 API 在数据变化场景下比 CSS 动画更灵活,因为它可以精确控制每个属性的插值起止值。 ## 交互实现:悬停提示与缩放 ### 悬停提示 SVG 元素可以直接绑定 DOM 事件。一个简洁的 tooltip 实现: ```javascript 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()` 模块提供了完整的缩放平移方案: ```javascript 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 和所有关联元素: ```javascript 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 属性,把更新集中在一帧内: ```javascript function updateChart(newData) { requestAnimationFrame(() => { svg.selectAll('rect') .data(newData) .attr('y', d => yScale(d)) .attr('height', d => 260 - yScale(d)); }); } ``` ### 关闭不可见元素的事件监听 大量数据点时,给每个点绑定事件监听开销很大。改用事件委托: ```javascript 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`,让浏览器提前创建合成层: ```css .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。掌握这些层次,你就不再受限于图表库的配置项,而是能从底层精确控制图表的每个细节。