SVG中的结构化、分组和引用元素

发布于 彦子

SVG有自己结构化文档的方式。通过某些SVG元素,我们可以在文档中定义、分组以及引用对象。这些元素使得元件重用变得简单,而且还保持了代码的简洁性和可读性。在这篇文章中我们将讲解这些元素,并指出它们之间的区别,以及每个元素各自的优势。

使用<g>元素分组

<g>中的g代表group(分组)的意思。分组元素用于在逻辑上对相关的图形元素进行分组。从图形编辑器的角度,例如Adobe Illustrator,<g>元素提供了类似于Group Object的功能。你也可以认为分组和图形编辑器中图层的概念是相似的,因为一个图层就是一组元素。

<g>元素将其所有子内容分到一组。它通常有一个id属性,用来给分组命名。你给<g>元素应用的样式也都会被应用于它所有的子元素。所以它很容易添加样式、动画、交互,甚至整个组的对象的动画。

例如,下面是一个SVG绘制的鸟的图。这只鸟是用几个简单的形状组合完成的,包括圆和路径。

Group

如果你想要在Illustrator中把整只鸟从一个位置移动到另一个位置,你需要将这些元素组合到一起,这样你才不需要在每次移动的时候都将它们一个个选中。

group

这和SVG中使用<g>元素分组的原理是相同的。在这个示例中,我们和身体部分的元素放一个组,头部的元素放一个组,然后将这两组内容再放到一个组中,赋予idbird

<svg width="1144.12px" height="400px" viewBox="0 0 572.06 200">
    <style>
        svg{background-color:white;}
        #wing{fill:#81CCAA;}
        #body{fill:#B8E4C2;}
        #pupil{fill:#1F2600;}
        #beak{fill:#F69C0D;}
        .eye-ball{fill:#F6FDC4;}
    </style>
    <g id="bird">
        <g id="body">
            <path d="M48.42,78.11c0-17.45,14.14-31.58,31.59-31.58s31.59,14.14,31.59,31.58c0,17.44-14.14,31.59-31.59,31.59
            S48.42,95.56,48.42,78.11"/>
            <path d="M109.19,69.88c0,0-8.5-27.33-42.51-18.53c-34.02,8.81-20.65,91.11,45.25,84.73
            c40.39-3.65,48.59-24.6,48.59-24.6S124.68,106.02,109.19,69.88"/>
            <path id="wing" d="M105.78,75.09c4.56,0,8.84,1.13,12.62,3.11c0,0,0.01-0.01,0.01-0.01l36.23,12.38c0,0-13.78,30.81-41.96,38.09
            c-1.51,0.39-2.82,0.59-3.99,0.62c-0.96,0.1-1.92,0.16-2.9,0.16c-15.01,0-27.17-12.17-27.17-27.17
            C78.61,87.26,90.78,75.09,105.78,75.09"/>
        </g>
        <g id="head">
            <path id="beak" d="M50.43,68.52c0,0-8.81,2.58-10.93,4.86l9.12,9.87C48.61,83.24,48.76,74.28,50.43,68.52"/>
            <path class="eye-ball" d="M60.53,71.68c0-6.33,5.13-11.46,11.46-11.46c6.33,0,11.46,5.13,11.46,11.46c0,6.33-5.13,11.46-11.46,11.46
                C65.66,83.14,60.53,78.01,60.53,71.68"/>
            <path id="pupil" d="M64.45,71.68c0-4.16,3.38-7.53,7.54-7.53c4.16,0,7.53,3.37,7.53,7.53c0,4.16-3.37,7.53-7.53,7.53
                C67.82,79.22,64.45,75.84,64.45,71.68"/>
            <path class="eye-ball" d="M72.39,74.39c0-2.73,2.22-4.95,4.95-4.95c2.73,0,4.95,2.21,4.95,4.95c0,2.74-2.22,4.95-4.95,4.95
                C74.6,79.34,72.39,77.13,72.39,74.39"/>
        </g>
    </g>
</svg>

如果你想要改变#body分组的填充颜色,它里边的所有元素都会变成你指定的颜色,非常方便。

分组元素非常好用,不仅是因为其组织和结构的特性。当你想要给由几块内容组成的SVG图像添加交互或动画的时候,非常好用。你可以把这些内容项放到一个组中,然后给它们定义移动、缩放或旋转的动画,这样它们相互之间的空间关系就可以被保持,也就是位置不会被打乱。

如果你想要对整只鸟进行缩放,让它变成现在尺寸的两倍,只需要一行CSS即可完成。

#bird {
    transform: scale(2);
}

分组尤其使得交互变得非常方便。你可以将鼠标事件应用到整只鸟上,然后让它作为一整个组去回应事件,而不必给组中的每个元素分别去应用相同的交互或者变换。

<g>元素有一个更重要的特性:它可以有自己的<title><desc>标签,使其更容易被屏幕阅读器解读,而且代码整体的可读性也更好。例如:

<g id="bird">
    <title>Bird</title>
    <desc>An image of a cute little green bird with an orange beak.</desc>
    <!-- ... -->
</g>

使用<use>重用元素

很多时候,在一个图像中,有一些元素是重复使用的。在Illustrator中如果你想要重复某个元素,你需要复制该元素,然后把它粘贴到相应的位置。复制粘贴现有元素比重新创建一个相同的元素要方便得多。

<use>元素可以让你重用现有的元素,给你一个类似于图形编辑器中复制粘贴的功能。它可以用于重用单个元素,也可以重用一组用<g>定义的元素。

<use>元素有xyheightwidth属性,它通过使用xlink:href属性引用其它内容。所以如果你已经定义了一个分组,并给它赋予了id,当你想要在其它地方使用它时,你只需要在xlink:href属性中给一个URI,然后指定xy的位置,也就是该组图像显示的原点(0, 0)

例如,当我们想要在我们的SVG画布上创建另一只鸟时,代码如下:

<use x="100" y="100" xlink:href="#bird" />

你可以在xlink:href属性中引用任何SVG元素,即使该元素是存在于外部文件中的。引用的元素和组不需要一定存在于同一个文件中。这对于组织以及缓存文件来说是非常棒的(例如,你可以单独给要用于重用的元素建一个文件)。例如,如果我们示例中的鸟是在一个单独的叫做animals.svg的文件中创建的,我们可以像这样引用它:

<use x="100" y="100" xlink:href="path/to/animals.svg#bird" />

但是,在<use>中引用外部SVG在大多数版本的IE中是不行的(至少要IE11)。我建议你阅读Chris Coyier的这篇文章了解详细的情况,以及降级机制。

现在,你可能已经注意到,我说<use>中的xy属性指定了分组元素开始的位置,也就是元素左上角应该处的位置。移动元素意味着你从当前位置开始,将其移动到另一个位置。我指的是“应该定位到”,它会暗示元素根据use中的坐标系统在整个画布上定位元素,对吧?

但是事实证明,xy坐标系其实是使用变换属性平移元素的简写。更具体地说,上面的<use>等同于:

<use xlink:href="#bird" transform="translate(100, 100)" />

use

这个事实意味着我们现在的新的重用元素的位置,其实是相对于我们使用的原始元素的位置来定位的。这并不是什么好的特性,有一些缺点。

<use>元素的另一个缺点是:初始元素的“副本”会和初始元素保持相同的样式。你给#bird组元素应用的任何样式或变换,鸟的副本也会拥有相同的样式和变换。

你可以use一个元素,并给它应用独立的变换,例如,下面的这行代码将会重用我们的这只鸟,然后使用了一个缩放变换,将元件变成初始大小的一半。

<use x="100" y="100" xlink:href="#bird" transform="scale(0.5)" />

但是,你不能在副本中覆盖初始元素的样式(例如描边和填充)。这也就意味着如果你想要创建多只鸟或多个图标,你可能希望每个图标都是不同的颜色,这是不可能用<use>元素完成的(除非初始元素是在<defs>元素中定义的,并且没有应用这些样式。详情请阅读下一节)。

<use>元素可以让你重用一个已经在画布上渲染过的元素。但是如果你想要定义一个元素,但是并不想让它显示出来,等到想使用的时候再调用?这时候就需要<defs>元素了。

使用<defs>重用已存储元素

<defs>元素可以用来存储那些我们不想直接显示的内容。换句话说,<defs>元素就是用来定义元件,但是不直接渲染。这个隐藏的存储元件可以在后面被其它SVG元素应用及显示,这使得它非常适合用于绘制那些包含重用图像的图案。

所以,使用<defs>我们可以定义一个我们想要使用的元素。这个元素可以是任何内容,可以是我们前面看到的一只鸟,也可以是裁剪路径、蒙版或一个线性渐变。基本上,任何内容,只要是我们想要定义并保存,然后在后面再使用的,我们都可以在<defs>中定义,而且该元件可以保存为模板,或是作为一种工具,以便将来使用。模板仅在实例化的时候显示。

下面的示例定义了一个SVG渐变,然后把它作为一个简单的SVG矩形的填充颜色:

<svg>
    <defs>
        <linearGradient id="gradient">
            <stop offset="0%" style="stop-color: deepPink"></stop>
            <stop offset="100%" style="stop-color: #009966"></stop>
        </linearGradient>
    </defs>
 
    <rect stroke="#eee" stroke-width="5" fill="url(#gradient)"></rect>
</svg>

<defs>元素中定义线性渐变,就是确保该渐变不会被渲染,除非它在哪个需要的地方被引用了。

在上一节中我们提到了<use>元素的两个缺陷:

  • 新元素的位置相对于初始元素定位。
  • 初始元素的样式不能在新副本中被覆盖。

的确,还包括重用use元素会在画布上渲染这一点。

使用<defs>元素,所有这些缺陷都可以避免。不仅不会渲染初始元素,而且当你想要重用<defs>中的元素时,你为每个示例指定的定位都是相对于用户坐标系统的原点,而不是相对于初始元素的位置(也就是初始元素是一个模板,甚至都不需要在画布上渲染出来)。

在这个示例中,我们有一棵树,这棵树由一个树干和一组树叶组成。树叶组成了一个组,idid="leaves",这个组又和树干组合成了一个更大的叫做tree的组。

<svg width="500.79px" height="200px" viewBox="0 0 500.79 200">
    <style type="text/css">
        #leaves{fill:#8CC63F;}
        #bark{fill:#A27729;}
    </style>
    <g id="tree">
        <path id="bark" d="M91.33,165.51c0,0,4.18-27.65,1.73-35.82l-18.55-25.03l3.01-2.74l17.45,19.87l1.91-37.6h4.44l1.83,24.53
        l15.26-16.35l3.27,4.36l-16.07,19.34c0,0-2.72,0-1.09,19.34c1.63,19.34,3,29.7,3,29.7L91.33,165.51z"/>
        <g id="leaves">
            <path class="leaf" d="M96.97,79.07c0,0-14.92,4.34-23.52-14.05c0,0,19.4-7.98,24.37,11.9c0,0-9.68-3.57-13.07-6.73
                C84.75,70.2,91.82,77.99,96.97,79.07z"/>
            <path class="leaf" d="M74.07,100.91c0,0-15.94-1.51-17.2-22.39c0,0,21.62-0.27,18.83,20.66c0,0-7.92-7.1-9.97-11.41
                C65.73,87.77,69.55,97.92,74.07,100.91z"/>
            <!-- ... -->
        </g>
    </g>
</svg>

现在这棵树如下:

defs

如果我们用一个<defs>元素包裹#tree组,这棵树就不会在画布上渲染。

<svg width="500.79px" height="200px" viewBox="0 0 500.79 200">
    <style type="text/css">
        #leaves{fill:#8CC63F;}
        #bark{fill:#A27729;}
    </style>
    <defs>
        <g id="tree">
            <!-- ... -->
        </g>
    </defs>
</svg>

现在这棵树就相当于一个模板。我们可以通过<use>元素来使用它,就像我们use其它元素一样。唯一的不同是xy属性现在是相对于用户坐标系统定位的,而不是相对于使用的元素。

例如,如果我们想要创建三个树的副本,然后在SVG画布上显示它们。假设在这种情况下,用户坐标系统匹配视窗的宽度和高度,初始位置也和SVG视窗的左上角重合,我们会得到如下的代码和结果:

<use xlink:href="#tree" x="50" y="100" />
<use xlink:href="#tree" x="200" y="100" />
<use xlink:href="#tree" x="350" y="100" />

use

如上图所示,每棵树的定位都是相对于坐标系统的原点,在这里指的是SVG的左上角。所以每棵树的左上角都是定位在它自己在用户坐标系统中的位置(x, y),独立于其它树以及<defs>中定义的树模板。

当你使用<defs>来重用元素,你可以给它应用不同的样式,给每棵树填充不同的颜色,只要这些样式没有在初始的树模板中定义。如果<defs>中的树已经使用了这些样式,这些样式同样没办法被新实例的样式覆盖。所以<defs>非常适合用于创建实例很少的模板,然后给副本应用其需要的样式。如果没有<defs>,只用<use>是不可能完成的。

<defs>元素中的内容不是渲染树的一部分,就像defs是一个g元素,其display的值被设置为none。然而,defs的子内容总是在源代码树中写出,然后被其它元素引用;因此,defs元素或它的任何子内容的display属性的值都不能阻止这些元素被其它元素引用,即使设置为none

使用<symbol>对元素进行分组

<symbol>元素和<g>元素相似——它提供了一种对元素进行分组的方式。但是,它和分组元素有两个主要的不同:

  • <symbol>元素不会被渲染。在这种方式中实际上它类似于<defs>元素。只有在use时才显示。
  • <symbol>元素可以有自己的viewBoxpreserveAspectRatio属性。也就是它可以适应视窗,然后以你想要的任何方式渲染,而不是都按照默认的样式。

<symbol>非常适用于定义可重复使用的元件(或符号)。它也可以作为<use>元素实例化的一个模板。而且有viewBoxpreserveAspectRatio属性,它可以在引用<use>元素定义的矩形视窗中自适应缩放。注意symbol元素每次被use元素实例化时都可以重新定义新的视窗。

此项功能是非常棒的,因为它允许你定义独立于它们渲染的视窗的元素,因此,确保你引用的symbol总是以某种方式显示在视窗中。

你需要先了解viewBox工作的方式,以及preserveAspectratio属性的值,这样才能最大地使用好这项功能。Chris Coyier写过一篇文章来解释为什么<symbol>元素是绘制图标的一个最好选择,以及如何使用。

我也将会写一篇介绍viewportviewBoxpreserveAspectRatio属性的文章,来解释这些属性的工作方式,以及如何在SVG中使用它们来控制和缩放图形。所以如果你有兴趣的话,敬请关注。

更新:文章已写好:理解SVG坐标系统和转换(Part 1)——The viewport, viewBox, and preserveAspectRatio

w3cplus中文译文~

注意不能给给symbol元素应用display属性;因此,即使display属性的值设置为none之外的其它值,symbol元素也不能直接渲染。但是即使symbol元素的display属性或任何它的父元素设置为nonesymbol元素也可以被引用。

总结

所有这些元素都是SVG中的容器结构元件,都有助于我们更容易地重用元素,也可保持代码的简洁性和可读性。这篇文章中我们提到的每个元素都有它自己的使用场景。现在你已经了解了每个元素的特性以及相互之间的区别,你可以自由选择使用哪个,根据需求决定。但是,不要忘了保持SVG的可访问性

感谢您的阅读,希望这篇文章能帮到您!

本文根据@SaraSoueidan的《STRUCTURING, GROUPING, AND REFERENCING IN SVG — THE <G>, <USE>, <DEFS> AND <SYMBOL> ELEMENTS》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://sarasoueidan.com/blog/structuring-grouping-referencing-in-svg/

彦子

在校学生,本科计算机专业。逗比一枚,热爱前端热爱生活,喜欢CSS喜欢JavaScript喜欢SVG,爱玩PS玩AI玩啊逗比的软件。努力向上,厚积薄发。

如需转载,烦请注明出处:https://www.fedev.cn/svg/structuring-grouping-referencing-in-svg.htmlFootwear Friday Nike, Jordan & More