构建自适应SVG:使用<symbol>、<use>和CSS媒体查询的完整指南

本文详细介绍了如何使用SVG的<symbol>和<use>元素结合CSS媒体查询创建自适应SVG图形,实现在不同屏幕尺寸下的优化显示效果,避免代码重复并保持动画功能。

构建自适应SVG:使用和CSS媒体查询

SVG确实可以缩放,但如何让它们更好地适应不同的屏幕尺寸?网页设计先驱Andy Clarke解释了如何使用<symbol><use>和CSS媒体查询构建他称之为"自适应SVG"的方法。

问题背景

最近我写了很多关于如何准备和优化SVG代码以用作静态图形或动画的文章。我喜欢使用SVG,但其中有一个问题一直困扰着我。

为了说明如何构建自适应SVG,我选择了1959年首次播出的《The Quick Draw McGraw Show》中的一集"Bow Wow Bandit"。

假设我设计了一个基于Bow Wow Bandit的SVG场景,具有16:9的宽高比,viewBox尺寸为1920×1080。这个SVG可以放大和缩小(从名字就能看出来),无论尺寸巨大还是微小,看起来都很清晰。

但在小屏幕上,16:9的宽高比可能不是最佳格式,图像会失去其影响力。有时,像3:4这样的纵向方向更适合屏幕尺寸。

但问题在于,仅使用viewBox很难为不同的屏幕尺寸重新定位内部元素。这是因为在SVG中,内部元素位置被锁定到原始viewBox的坐标系,因此无法轻松在桌面和移动设备之间更改它们的布局。这是一个问题,因为动画和交互性通常依赖于元素位置,当viewBox更改时这些功能会中断。

挑战与解决方案

我的挑战是为小屏幕提供1080×1440版本的Bow Wow Bandit,为更大的屏幕提供不同版本。我希望内部元素(如Quick Draw McGraw和他的狗Snuffles)的位置和大小能够改变以最适合这两种布局。

方案1:显示和隐藏SVG

最明显的选择是在标记中包含两个不同的SVG,一个用于小屏幕,另一个用于大屏幕,然后使用CSS和媒体查询显示或隐藏它们:

1
2
3
4
5
6
7
<svg id="svg-small" viewBox="0 0 1080 1440">
  <!-- ... -->
</svg>

<svg id="svg-large" viewBox="0 0 1920 1080">
  <!--... -->
</svg>
1
2
3
4
5
6
7
#svg-small { display: block; }
#svg-large { display: none; }

@media (min-width: 64rem) {
  #svg-small { display: none; }
  #svg-mobile { display: block; }
}

但使用这种方法,两个SVG版本都会被加载,当图形复杂时,意味着要下载大量不必要的代码。

方案2:使用JavaScript替换SVG

我考虑过使用JavaScript在指定断点处交换更大的SVG:

1
2
3
4
5
if (window.matchMedia('(min-width: 64rem)').matches) {
  svgContainer.innerHTML = desktopSVG; 
} else {
  svgContainer.innerHTML = mobileSVG;
}

抛开JavaScript现在对设计显示至关重要这一事实,两个SVG通常都会被加载,这会增加DOM复杂性和不必要的重量。此外,维护也成了问题,因为现在有两个版本的 artwork 需要维护,更新像Quick Draw尾巴形状这样小的东西所需的时间会翻倍。

最终解决方案:一个SVG符号库和多个使用

我的目标是:

  • 为小屏幕提供一个版本的Bow Wow Bandit
  • 为更大的屏幕提供不同版本
  • 只定义一次artwork(DRY原则)
  • 能够调整元素大小和重新定位

<symbol>元素允许您定义可重用的SVG元素,这些元素可以隐藏和重用,以提高可维护性并减少代码膨胀。它们就像SVG的组件:创建一次,在需要的地方使用:

1
2
3
4
5
6
7
8
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
  <symbol id="quick-draw-body" viewBox="0 0 620 700">
    <g class="quick-draw-body">[…]</g>
  </symbol>
  <!-- ... -->
</svg>

<use href="#quick-draw-body" />

<symbol>就像将字符存储在库中。我可以根据需要多次引用它,以保持代码一致和轻量。使用<use>元素,我可以在不同位置或大小多次插入相同的符号,甚至可以在不同的SVG中插入。

导出单独的ViewBox

我之前的文章介绍了如何将元素导出为图层以便更轻松地使用它们。在创建符号时,这个过程有点不同。

通常,我会使用相同的viewBox大小导出所有元素。但是当我创建一个符号时,我需要它有自己特定的viewBox。

我将每个元素导出为单独大小的SVG,这给了我将其内容转换为符号所需的尺寸。以Quick Draw McGraw帽子的SVG为例,它的viewBox大小为294×182:

1
2
3
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 294 182">
  <!-- ... -->
</svg>

我将SVG标签替换为<symbol>,并将其artwork添加到我的SVG库中:

1
2
3
4
5
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
  <symbol id="quick-draw-hat" viewBox="0 0 294 182">
    <g class="quick-draw-hat">[…]</g>
  </symbol>
</svg>

然后,我对artwork中的所有剩余元素重复这个过程。现在,如果我需要更新任何符号,更改将自动应用于使用它的每个实例。

在多个SVG中使用<symbol>

我希望我的元素出现在Bow Wow Bandit的两个版本中,一种排列用于小屏幕,另一种排列用于大屏幕。因此,我创建了两个SVG:

1
2
3
4
5
6
7
<svg class="svg-small" viewBox="0 0 1080 1440">
  <!-- ... -->
</svg>

<svg class="svg-large" viewBox="0 0 1920 1080">
  <!-- ... -->
</svg>

并在两个SVG中插入对我的符号的链接:

1
2
3
4
5
6
7
<svg class="svg-small" viewBox="0 0 1080 1440">
  <use href="#quick-draw-hat" />
</svg>

<svg class="svg-large" viewBox="0 0 1920 1080">
  <use href="#quick-draw-hat" />
</svg>

定位符号

一旦使用<use>将符号放入布局中,我的下一步就是定位它们,如果我想要为不同屏幕尺寸提供替代布局,这一点尤其重要。符号的行为类似于<g>组,因此我可以使用width、height和transform等属性来缩放和移动它们:

1
2
3
4
5
6
7
<svg class="svg-small" viewBox="0 0 1080 1440">
  <use href="#quick-draw-hat" width="294" height="182" transform="translate(-30,610)"/>
</svg>

<svg class="svg-large" viewBox="0 0 1920 1080">
  <use href="#quick-draw-hat" width="294" height="182" transform="translate(350,270)"/>
</svg>

我可以使用transform独立放置每个<use>元素。这很强大,因为我不是重新定位SVG内部的元素,而是移动<use>引用。我的内部布局保持整洁,文件大小保持较小,因为我没有重复artwork。浏览器只加载一次,这减少了带宽并加快了页面渲染速度。而且因为我总是引用相同的符号,所以无论屏幕尺寸如何,它们的外观都保持一致。

动画<use>元素

这里事情变得棘手。我想动画化角色的部分 - 比如Quick Draw的帽子倾斜和他的腿踢动。但是当我添加针对<symbol>内部元素的CSS动画时,什么也没有发生。

提示:您可以动画化<use>元素本身,但不能动画化<symbol>内部的元素。如果您希望单个部分移动,请将它们制作成自己的符号并动画化每个<use>

事实证明,您不能样式化或动画化<symbol>,因为<use>创建了不容易定位的shadow DOM克隆。因此,我必须巧妙处理。在我的库SVG中的每个<symbol>内部,我在要动画化的部分周围添加了一个<g>元素:

1
2
3
4
5
<symbol id="quick-draw-hat" viewBox="0 0 294 182">
  <g class="quick-draw-hat">
    <!-- ... -->
  </g>
</symbol>

并使用属性子字符串选择器动画化它,针对use元素的href属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use[href="#quick-draw-hat"] {
  animation-delay: 0.5s;
  animation-direction: alternate;
  animation-duration: 1s;
  animation-iteration-count: infinite;
  animation-name: hat-rock;
  animation-timing-function: ease-in-out;
  transform-origin: center bottom;
}

@keyframes hat-rock {
from { transform: rotate(-2deg); }
to   { transform: rotate(2deg); } }

用于显示控制的媒体查询

一旦我创建了两个可见的SVG - 一个用于小屏幕,一个用于大屏幕 - 最后一步是决定在哪个屏幕尺寸显示哪个版本。我使用CSS媒体查询来隐藏一个SVG并显示另一个。我默认显示小屏幕SVG:

1
2
.svg-small { display: block; }
.svg-large { display: none; }

然后我使用min-width媒体查询在64rem及以上切换到大型屏幕SVG:

1
2
3
4
@media (min-width: 64rem) {
  .svg-small { display: none; }
  .svg-large { display: block; }
}

这确保一次只有一个SVG可见,保持我的布局简单且DOM没有不必要的混乱。而且因为两个可见的SVG都引用相同的隐藏<symbol>库,所以无论两个布局中出现多少个<use>元素,浏览器只下载artwork一次。

总结

通过结合<symbol><use>、CSS媒体查询和特定的变换,我可以构建自适应SVG,在不重复内容、加载额外资源或依赖JavaScript的情况下重新定位其元素。我只需要在隐藏的符号库中定义每个图形一次。然后我可以在几个可见的SVG中根据需要重用这些图形。使用CSS进行布局切换,结果是快速和灵活的。

这提醒我们,网络上一些最强大的技术不需要大型框架或复杂的工具 - 只需要一点SVG知识和对基础知识的巧妙使用。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计