面试题手册

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

服务端阅读 05月27日 14:41

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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