在前五篇文章中,我们从一个圆开始,一路征服了矩形、椭圆、路径——从直线到曲线,从简单到复杂。你已经可以用 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 有以下特点:

  1. 它是 DOM 树的一部分,但不属于主文档树。

  2. 对主文档的脚本和样式是“不可见”的——你无法通过 document.querySelector 直接访问 Shadow DOM 内部的节点。

  3. 样式是隔离的——主文档的 CSS 规则默认不会穿透到 Shadow DOM 内部。

4.2 在 DevTools 中查看 Shadow DOM

现代浏览器的开发者工具支持查看 Shadow DOM。以 Chrome 为例:

  1. 按F12, 打开 DevTools并选择Elements 面板。

  2. 点击右上角的齿轮图标(设置),选择左侧的 Preferences 菜单。

  3. 在右侧的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、样式隔离

三个核心认知:
  1. <g> 是“组织工具”——它把分散的元素聚合成一个逻辑单元,方便统一变换、样式和操作。它本身不渲染任何东西,但它的子元素会。

  2. <use> 是“复印机”——它通过克隆来复用内容,但克隆出来的副本存在于 Shadow DOM 中,与主文档隔离。理解 Shadow DOM 的样式隔离机制,是正确使用 <use> 的关键。

  3. CSS 自定义属性是穿透 Shadow DOM 的最佳方式——传统的 CSS 选择器无法穿透 Shadow DOM,但 CSS 自定义属性(var())可以。这是现代 SVG 图标系统的主流方案。

下篇预告

在下一篇文章中,我们将进入一个全新的领域——SVG 数据类型。我们将深入探索 SVGLength、SVGNumber 等数据类型接口,彻底搞懂那些“长得像对象”的属性值,理解 baseVal 和 animVal 的区别,并学会如何精确地读写和计算 SVG 属性的数值。

互动话题:在你的项目中,有没有遇到过需要大量重复 SVG 代码的场景?尝试用 <use> 重构一下,看看代码量减少了多少?欢迎在评论区分享你的重构前后对比!