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 优势这几个关键点,就能在图标系统和图案复用中用好这套机制,而不是在代码里反复复制粘贴同一个图标的路径数据。