SVG有自己结构化文档的方式。通过某些SVG元素,我们可以在文档中定义、分组以及引用对象。这些元素使得元件重用变得简单,而且还保持了代码的简洁性和可读性。在这篇文章中我们将讲解这些元素,并指出它们之间的区别,以及每个元素各自的优势。
使用<g>
元素分组
<g>
中的g
代表group(分组)的意思。分组元素用于在逻辑上对相关的图形元素进行分组。从图形编辑器的角度,例如Adobe Illustrator,<g>
元素提供了类似于Group Object的功能。你也可以认为分组和图形编辑器中图层的概念是相似的,因为一个图层就是一组元素。
<g>
元素将其所有子内容分到一组。它通常有一个id
属性,用来给分组命名。你给<g>
元素应用的样式也都会被应用于它所有的子元素。所以它很容易添加样式、动画、交互,甚至整个组的对象的动画。
例如,下面是一个SVG绘制的鸟的图。这只鸟是用几个简单的形状组合完成的,包括圆和路径。
如果你想要在Illustrator中把整只鸟从一个位置移动到另一个位置,你需要将这些元素组合到一起,这样你才不需要在每次移动的时候都将它们一个个选中。
这和SVG中使用<g>
元素分组的原理是相同的。在这个示例中,我们和身体部分的元素放一个组,头部的元素放一个组,然后将这两组内容再放到一个组中,赋予id
为bird
。
<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>
元素有x
,y
,height
,width
属性,它通过使用xlink:href
属性引用其它内容。所以如果你已经定义了一个分组,并给它赋予了id
,当你想要在其它地方使用它时,你只需要在xlink:href
属性中给一个URI,然后指定x
和y
的位置,也就是该组图像显示的原点(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>
中的x
和y
属性指定了分组元素开始的位置,也就是元素左上角应该处的位置。移动元素意味着你从当前位置开始,将其移动到另一个位置。我指的是“应该定位到”,它会暗示元素根据use
中的坐标系统在整个画布上定位元素,对吧?
但是事实证明,x
和y
坐标系其实是使用变换属性平移元素的简写。更具体地说,上面的<use>
等同于:
<use xlink:href="#bird" transform="translate(100, 100)" />
这个事实意味着我们现在的新的重用元素的位置,其实是相对于我们使用的原始元素的位置来定位的。这并不是什么好的特性,有一些缺点。
<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>
中的元素时,你为每个示例指定的定位都是相对于用户坐标系统的原点,而不是相对于初始元素的位置(也就是初始元素是一个模板,甚至都不需要在画布上渲染出来)。
在这个示例中,我们有一棵树,这棵树由一个树干和一组树叶组成。树叶组成了一个组,id
为id="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>
元素包裹#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
其它元素一样。唯一的不同是x
和y
属性现在是相对于用户坐标系统定位的,而不是相对于使用的元素。
例如,如果我们想要创建三个树的副本,然后在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" />
如上图所示,每棵树的定位都是相对于坐标系统的原点,在这里指的是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>
元素可以有自己的viewBox
和preserveAspectRatio
属性。也就是它可以适应视窗,然后以你想要的任何方式渲染,而不是都按照默认的样式。
<symbol>
非常适用于定义可重复使用的元件(或符号)。它也可以作为<use>
元素实例化的一个模板。而且有viewBox
和preserveAspectRatio
属性,它可以在引用<use>
元素定义的矩形视窗中自适应缩放。注意symbol
元素每次被use
元素实例化时都可以重新定义新的视窗。
此项功能是非常棒的,因为它允许你定义独立于它们渲染的视窗的元素,因此,确保你引用的symbol
总是以某种方式显示在视窗中。
你需要先了解viewBox
工作的方式,以及preserveAspectratio
属性的值,这样才能最大地使用好这项功能。Chris Coyier写过一篇文章来解释为什么<symbol>
元素是绘制图标的一个最好选择,以及如何使用。
我也将会写一篇介绍viewport
、viewBox
和preserveAspectRatio
属性的文章,来解释这些属性的工作方式,以及如何在SVG中使用它们来控制和缩放图形。所以如果你有兴趣的话,敬请关注。
注意不能给给symbol
元素应用display
属性;因此,即使display
属性的值设置为none
之外的其它值,symbol
元素也不能直接渲染。但是即使symbol
元素的display
属性或任何它的父元素设置为none
,symbol
元素也可以被引用。
总结
所有这些元素都是SVG中的容器结构元件,都有助于我们更容易地重用元素,也可保持代码的简洁性和可读性。这篇文章中我们提到的每个元素都有它自己的使用场景。现在你已经了解了每个元素的特性以及相互之间的区别,你可以自由选择使用哪个,根据需求决定。但是,不要忘了保持SVG的可访问性。