在前五篇文章中,我们从一个圆开始,一路征服了矩形、椭圆、路径——从直线到曲线,从简单到复杂。你已经可以用 JavaScript 绘制出任意形状的 SVG 图形了。
但有个问题开始浮现:当图形数量增多时,代码变得难以维护。
想象一下,你要在画布上放置 50 个相同的小圆点——难道要写 50 遍 <circle> 标签吗?又或者,你有一组图形需要整体旋转 45 度——难道要一个一个地设置 transform 属性吗?
答案显然是否定的。SVG 提供了两个专门解决这类问题的“组织大师”:
<g>(分组) :将多个元素打包成一个整体,统一施加变换和样式。
<use>(复用) :克隆已有元素,一处定义、多处使用。
掌握了这两个工具,你的 SVG 代码将从“手工作坊”升级为“工业化生产”——更简洁、更高效、更易于维护。
一、<g>:把散兵游勇变成一支军队
<g> 是 SVG 中最基础的容器元素(Container Element)。它的作用非常朴素:把多个 SVG 元素装进一个“组”里。
g ,英文全称 group。
1.1 基本语法
<svg viewBox="0 0 400 300">
<g id="myGroup">
<circle cx="100" cy="150" r="40" fill="blue" />
<rect x="150" y="110" width="80" height="80" fill="red" />
<circle cx="270" cy="150" r="40" fill="green" />
</g>
</svg>这个 <g> 把三个图形——两个圆和一个矩形——打包成了一个逻辑单元。从视觉上看,它们仍然是三个独立的图形;但从操作的角度看,它们成了一个整体。
1.2 SVGGElement:<g> 的 DOM 接口
在 JavaScript 中,<g> 元素对应的是 SVGGElement 接口。SVGGElement 继承自 SVGGraphicsElement,因此它拥有 getBBox()、getCTM() 等图形元素的通用能力。
const group = document.getElementById('myGroup');
console.log(group instanceof SVGGElement); // true
console.log(group instanceof SVGGraphicsElement); // true
// 获取整个组的边界框(包含所有子元素)
const bbox = group.getBBox();
console.log(`组的总宽度: ${bbox.width},总高度: ${bbox.height}`);关键认知:SVGGElement 本身不渲染任何可见内容——它只是一个容器。所有可见的渲染都来自它的子元素。这就像一个文件夹——文件夹本身是空的,但里面装的文件构成了内容。
1.3 组的三大核心能力
统一变换
对 <g> 施加的 transform 变换,会作用于组内的所有子元素。
<svg viewBox="0 0 400 300">
<g transform="translate(50, 30) rotate(15)">
<!-- 组内所有元素都会一起平移和旋转 -->
<rect x="0" y="0" width="80" height="80" fill="blue" />
<circle cx="40" cy="40" r="20" fill="white" />
</g>
</svg>这在制作图表、游戏角色、复杂图标时极其有用——你只需要移动或旋转整个组,而不需要逐个调整子元素。
样式继承
<g> 上的大部分呈现属性(presentation attributes)会被其子元素继承。
<g fill="blue" stroke="black" stroke-width="2">
<rect x="10" y="10" width="80" height="80" /> <!-- 自动继承 fill="blue" -->
<circle cx="60" cy="60" r="30" /> <!-- 同样继承 -->
<path d="M 10 90 L 50 50 L 90 90" /> <!-- 同样继承 -->
</g>你可以把通用的样式(颜色、描边、透明度等)提取到 <g> 上,让子元素只保留各自特有的属性。子元素也可以覆盖继承的样式——在子元素上单独设置 fill 会覆盖父级的 fill。
批量操作
在 JavaScript 中,你可以通过操作 <g> 来批量控制所有子元素:
<g id="myGroup"> ... </g>const group = document.getElementById('myGroup');
// 批量隐藏所有子元素
group.setAttribute('visibility', 'hidden');
// 批量改变所有子元素的透明度
group.setAttribute('opacity', '0.5');
// 批量应用变换
group.setAttribute('transform', 'translate(100, 50) scale(1.2)');1.4 组的嵌套
<g> 元素可以无限嵌套。一个组里面可以包含另一个组,形成树状结构:
<g id="scene">
<g id="background">
<rect width="400" height="300" fill="skyblue" />
</g>
<g id="character" transform="translate(200, 150)">
<g id="body">
<ellipse rx="40" ry="60" fill="orange" />
</g>
<g id="head" transform="translate(0, -70)">
<circle r="30" fill="orange" />
<circle cx="-10" cy="-5" r="4" fill="black" />
<circle cx="10" cy="-5" r="4" fill="black" />
</g>
</g>
</g>这种层级结构让你可以精细控制——你可以整体移动整个场景,也可以单独移动角色,还可以只调整角色的头部。
二、<defs>:定义但不渲染的“素材库”
defs的英文全称是 definitions。
在介绍 <use> 之前,我们需要先认识它的好搭档——<defs>。
<defs> 是 SVG 中的定义元素。它里面存放的内容不会被直接渲染,而是作为“模板”或“素材”供其他地方引用。
<svg viewBox="-100 -100 400 300">
<defs>
<!-- 定义一个星星模板,但不会显示 -->
<polygon id="star" points="0,-20 6,-6 20,-6 9,4 13,20 0,12 -13,20 -9,4 -20,-6 -6,-6" fill="gold" />
<!-- 定义一个渐变,也不会显示 -->
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="red" />
<stop offset="100%" stop-color="blue" />
</linearGradient>
</defs>
<!-- 这里才是真正显示的内容 -->
<use href="#star" fill="url(#gradient)" />
</svg><defs> 里可以放任何 SVG 内容——基本图形、路径、组、渐变、滤镜、图案等。它们就像设计师的“素材库”,随时可以被调用。
最佳实践:把所有可复用的内容都放在 <defs> 中,让主文档保持干净和清晰。
三、<use>:一处定义,多处克隆
<use> 是 SVG 中最强大的复用工具。它通过 URL 引用另一个元素,并将其克隆到当前位置。
3.1 基本用法
<svg viewBox="0 0 400 200">
<defs>
<circle id="dot" cx="0" cy="0" r="20" fill="blue" />
</defs>
<!-- 在三个不同位置复用同一个圆 -->
<use href="#dot" x="50" y="100" />
<use href="#dot" x="150" y="100" />
<use href="#dot" x="250" y="100" />
</svg>注意看:我们只定义了一个圆,但在三个位置“使用”了它。<use> 上的 x 和 y 属性会偏移克隆出来的内容。
3.2 href vs xlink:href
在 SVG 2 中,推荐使用 href 属性来指定引用目标。旧的 xlink:href 已被弃用,但为了兼容老旧浏览器,有时两者会同时使用:
<!-- 推荐写法(SVG 2) -->
<use href="#star" x="100" y="100" />
<!-- 兼容写法 -->
<use href="#star" x="100" y="100" xlink:href="#star" />3.3 <use> 可以引用什么?
<use> 可以引用几乎任何 SVG 元素:
单个图形(<rect>、<circle>、<path> 等)。
组(<g>)。
<defs> 中定义的内容。
<symbol>(一种特殊的可复用容器)。
甚至可以是外部 SVG 文件中的元素。
<!-- 引用同一文档中的元素 -->
<use href="#myIcon" x="10" y="10" />
<!-- 引用外部 SVG 文件中的元素(同源策略限制) -->
<use href="icons.svg#heart" x="0" y="0" />安全提示:外部引用受同源策略限制。data: 网址被视为同源资源,但会带来安全风险,应谨慎使用。
3.4 SVGUseElement:<use> 的 DOM 接口
<use> 元素在 JavaScript 中对应的是 SVGUseElement 接口。它继承自 SVGGraphicsElement,因此也拥有 getBBox() 等几何方法。
const useEl = document.querySelector('use');
console.log(useEl instanceof SVGUseElement); // true
// 获取 use 引用的目标元素 ID
console.log(useEl.href.baseVal); // "#dot"
// 获取克隆内容的边界框
const bbox = useEl.getBBox();四、Shadow DOM:<use> 背后的“影子军团”
这是理解 <use> 最关键的环节——也是很多开发者困惑的根源。
4.1 什么是 Shadow DOM?
当浏览器遇到 <use href="#dot"> 时,它并不会把 #dot 的元素“移动”过来,而是在内存中创建一份完整的克隆,放在一个叫作 Shadow DOM 的“影子树”中。
这个 Shadow DOM 有以下特点:
它是 DOM 树的一部分,但不属于主文档树。
对主文档的脚本和样式是“不可见”的——你无法通过 document.querySelector 直接访问 Shadow DOM 内部的节点。
样式是隔离的——主文档的 CSS 规则默认不会穿透到 Shadow DOM 内部。
4.2 在 DevTools 中查看 Shadow DOM
现代浏览器的开发者工具支持查看 Shadow DOM。以 Chrome 为例:
按F12, 打开 DevTools并选择Elements 面板。
点击右上角的齿轮图标(设置),选择左侧的 Preferences 菜单。
在右侧的Elemnets面板下勾选 “user agent shadow DOM”。
之后,你就能在 <use> 元素下方看到一个 #shadow-root 节点,里面是克隆出来的内容。

4.3 样式隔离带来的挑战
Shadow DOM 的样式隔离机制,给 <use> 的样式控制带来了挑战——你不能直接从外部 CSS 中选中 Shadow DOM 内部的元素。
<defs>
<g id="icon">
<circle id="icon-circle" cx="20" cy="20" r="18" />
<path id="icon-path" d="M 10 20 L 30 20" stroke="blue" stroke-width="3" />
</g>
</defs>
<use href="#icon" class="my-icon" />/* 这样不会生效——无法穿透 Shadow DOM */
.my-icon #icon-circle {
fill: red;
}
/* 这样也不会生效 */
use #icon-circle {
fill: red;
}4.4 解决方案一:CSS 自定义属性(CSS Variables)
CSS 自定义属性(变量)可以穿透 Shadow DOM,因为自定义属性是按继承方式传递的,而不是按选择器匹配。
<defs>
<g id="icon">
<!-- 使用 var() 引用自定义属性 -->
<circle cx="20" cy="20" r="18" fill="var(--icon-color, blue)" />
<path d="M 10 20 L 30 20" stroke="var(--icon-stroke, white)" stroke-width="3" />
</g>
</defs>
<!-- 使用时通过 CSS 自定义属性来控制颜色 -->
<use href="#icon" style="--icon-color: red; --icon-stroke: yellow;" />
<use href="#icon" style="--icon-color: green; --icon-stroke: white;" />这是目前最推荐的方式——干净、标准、无需 hack。
4.5 解决方案二:在 <use> 上设置可继承属性
某些可继承的 CSS 属性(如 fill、stroke、font-family 等)可以从 <use> 元素继承到 Shadow DOM 内部—— 前提是模板中的元素没有显式设置这些属性。
<defs>
<!-- 注意:这里没有设置 fill,让它继承 -->
<circle id="dot" cx="0" cy="0" r="20" />
</defs>
<!-- 在 use 上设置 fill,会继承到克隆的圆上 -->
<use href="#dot" x="50" y="100" fill="red" />
<use href="#dot" x="150" y="100" fill="blue" />但这种方法只对继承属性有效,而且要求模板中不能有冲突的样式声明。
4.6 解决方案三:currentColor
currentColor 是一个特殊的 CSS 关键字,它表示当前元素的 color 属性值。利用它,可以让 SVG 图标跟随文本颜色:
<defs>
<g id="icon">
<circle cx="20" cy="20" r="18" fill="currentColor" />
</g>
</defs>
<use href="#icon" style="color: red;" /> <!-- 圆变成红色 -->
<use href="#icon" style="color: blue;" /> <!-- 圆变成蓝色 -->这是一种轻量级的“颜色自定义”方案,非常适合图标系统。
五、<symbol>:<use> 的“最佳拍档”
<symbol> 是另一种容器元素,专门用于定义可复用的图形模板。它与 <g> 的主要区别在于:
特性 | <g> | <symbol> |
|---|---|---|
是否可见 | 可见(除非在 <defs> 中) | 默认不可见 |
viewBox | 不支持 | 支持——可以定义自己的坐标系统 |
preserveAspectRatio | 不支持 | 支持 |
使用场景 | 普通分组 | 图标库、可缩放模板 |
<symbol> 的标准用法:
<defs>
<!-- symbol 可以有自己的 viewBox,独立于父级 SVG -->
<symbol id="heart" viewBox="0 0 100 100">
<path d="M 50 88 C 20 65 0 50 0 35 C 0 15 20 5 35 15 L 50 30 L 65 15 C 80 5 100 15 100 35 C 100 50 80 65 50 88 Z" fill="red" />
</symbol>
</defs>
<!-- 使用时可以随意缩放,symbol 的 viewBox 保证了比例 -->
<use href="#heart" x="0" y="0" width="50" height="50" />
<use href="#heart" x="60" y="0" width="80" height="80" />
<use href="#heart" x="150" y="0" width="120" height="120" />每个 <use> 实例可以有自己的 width 和 height,<symbol> 的 viewBox 会确保内容按比例缩放。这使得 <symbol> 成为构建 SVG 图标系统的首选方案。
六、综合实战:构建一个可复用的图标系统
<style>
/* 图标容器样式 */
.icon-grid {
display: flex;
gap: 30px;
padding: 30px;
background: #f5f5f5;
}
.icon-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.icon-item svg {
width: 60px;
height: 60px;
}
/* 不同主题的图标 */
.icon-primary {
--icon-fill: #4A90D9;
--icon-stroke: white;
}
.icon-success {
--icon-fill: #27AE60;
--icon-stroke: white;
}
.icon-warning {
--icon-fill: #F39C12;
--icon-stroke: white;
}
.icon-danger {
--icon-fill: #E74C3C;
--icon-stroke: white;
}
/* 深色主题 */
.icon-dark {
--icon-fill: #2C3E50;
--icon-stroke: #ECF0F1;
}
</style><!-- SVG 定义区(不渲染) -->
<svg style="display: none;">
<defs>
<!-- 星形图标 -->
<symbol id="icon-star" viewBox="0 0 100 100">
<polygon points="50,5 63,35 95,40 70,62 78,95 50,80 22,95 30,62 5,40 37,35"
fill="var(--icon-fill, gold)"
stroke="var(--icon-stroke, #333)"
stroke-width="3"
stroke-linejoin="round" />
</symbol>
<!-- 心形图标 -->
<symbol id="icon-heart" viewBox="0 0 100 100">
<path d="M 50 88 C 20 65 0 50 0 35 C 0 15 20 5 35 15 L 50 30 L 65 15 C 80 5 100 15 100 35 C 100 50 80 65 50 88 Z"
fill="var(--icon-fill, red)"
stroke="var(--icon-stroke, #333)"
stroke-width="3" />
</symbol>
<!-- 圆形图标 -->
<symbol id="icon-circle" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40"
fill="var(--icon-fill, blue)"
stroke="var(--icon-stroke, #333)"
stroke-width="3" />
</symbol>
</defs>
</svg><!-- 使用图标 -->
<div class="icon-grid">
<div class="icon-item icon-primary">
<svg><use href="#icon-star" /></svg>
<span>Primary</span>
</div>
<div class="icon-item icon-success">
<svg><use href="#icon-heart" /></svg>
<span>Success</span>
</div>
<div class="icon-item icon-warning">
<svg><use href="#icon-circle" /></svg>
<span>Warning</span>
</div>
<div class="icon-item icon-danger">
<svg><use href="#icon-star" /></svg>
<span>Danger</span>
</div>
<div class="icon-item icon-dark">
<svg><use href="#icon-heart" /></svg>
<span>Dark</span>
</div>
</div>6.2 JavaScript 动态控制
<script>
// 获取所有图标容器
const iconItems = document.querySelectorAll('.icon-item');
// 点击图标时随机改变颜色
iconItems.forEach(item => {
item.addEventListener('click', function() {
const svg = this.querySelector('svg use');
const currentFill = this.style.getPropertyValue('--icon-fill');
// 随机生成颜色
const randomColor = '#' + Math.floor(Math.random()*16777215).toString(16);
this.style.setProperty('--icon-fill', randomColor);
// 同时随机改变描边颜色
const randomStroke = '#' + Math.floor(Math.random()*16777215).toString(16);
this.style.setProperty('--icon-stroke', randomStroke);
});
});
// 动态创建新的图标实例
function addIcon(iconId, container) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '60');
svg.setAttribute('height', '60');
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttribute('href', `#${iconId}`);
svg.appendChild(use);
container.appendChild(svg);
}
// 示例:点击按钮添加新图标
addIcon('icon-star', document.querySelector('.icon-grid'));
</script>七、常见错误与避坑指南
错误一:试图直接修改 <use> 克隆出来的内容
// 这样不会生效——克隆出来的元素在 Shadow DOM 中
const useEl = document.querySelector('use');
const clonedCircle = useEl.querySelector('circle'); // null!解决方案:修改原始模板(在 <defs> 或 <symbol> 中),或者使用 CSS 自定义属性来控制样式。
错误二:忘记把 <g> 放在 <defs> 中
<!-- 这样会显示两个星星!一个在 defs 外面(可见),一个被 use 引用 -->
<g id="star">
<polygon points="..." />
</g>
<use href="#star" />正确做法:把模板放在 <defs> 中,或者使用 <symbol>。
错误三:<use> 引用不存在的 ID
<!-- 引用了一个不存在的元素,什么都不会显示 -->
<use href="#nonexistent" />解决方案:确保 href 引用的 ID 在文档中存在,且在被引用之前已定义。
错误四:混淆 <use> 上的属性和模板中的属性
当 <use> 元素和模板元素有同名属性时,模板中的属性优先级更高(除了少数可继承属性)。
<defs>
<circle id="dot" cx="0" cy="0" r="20" fill="blue" />
</defs>
<!-- 这里的 fill="red" 不会生效,因为模板中已经定义了 fill="blue" -->
<use href="#dot" x="50" y="100" fill="red" />解决方案:模板中不要设置需要被覆盖的属性,或者使用 CSS 自定义属性。
八、本章总结
元素 | 接口 | 用途 | 关键特性 |
|---|---|---|---|
<g> | SVGGElement | 分组容器 | 统一变换、样式继承、批量操作 |
<defs> | SVGDefsElement | 定义可复用内容 | 内容不直接渲染 |
<symbol> | SVGSymbolElement | 可复用图标模板 | 支持独立的 viewBox |
<use> | SVGUseElement | 克隆并插入内容 | Shadow DOM、样式隔离 |
三个核心认知:
<g> 是“组织工具”——它把分散的元素聚合成一个逻辑单元,方便统一变换、样式和操作。它本身不渲染任何东西,但它的子元素会。
<use> 是“复印机”——它通过克隆来复用内容,但克隆出来的副本存在于 Shadow DOM 中,与主文档隔离。理解 Shadow DOM 的样式隔离机制,是正确使用 <use> 的关键。
CSS 自定义属性是穿透 Shadow DOM 的最佳方式——传统的 CSS 选择器无法穿透 Shadow DOM,但 CSS 自定义属性(var())可以。这是现代 SVG 图标系统的主流方案。
下篇预告
在下一篇文章中,我们将进入一个全新的领域——SVG 数据类型。我们将深入探索 SVGLength、SVGNumber 等数据类型接口,彻底搞懂那些“长得像对象”的属性值,理解 baseVal 和 animVal 的区别,并学会如何精确地读写和计算 SVG 属性的数值。
互动话题:在你的项目中,有没有遇到过需要大量重复 SVG 代码的场景?尝试用 <use> 重构一下,看看代码量减少了多少?欢迎在评论区分享你的重构前后对比!