更多将内联 SVG 应用于生产环境的坑 – 第二部分

Avatar of Rob Levin
Rob Levin

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

以下是 Rob Levin 和 Chris Rumble 的客座文章。Rob 和 Chris 都在 Mavenlink 的产品设计团队工作。Rob 也是 SVG Immersion Podcast 的创建者和主持人,并在 2014 年撰写了最初的 5 个坑 文章。Chris 是一位来自旧金山的 UI 和动效设计师/开发人员。在这篇文章中,他们回顾了在 2 年多前将内联 SVG 集成到 Mavenlink 的旗舰应用程序后遇到的其他一些问题。文章插图由 Rob 完成,并且 – 本着我们主题的精神 – 100% 是矢量 SVG!

Explorations in Debugging

哇,自从我们发布 将 SVG 应用于生产环境的 5 个坑 文章已经两年多了。好吧,我们遇到了一些新的坑,是时候发布另一篇后续文章了!我们将这些坑标记为 6-10,向原始文章中的前 5 个坑致敬 :)

坑六:IE 拖放 SVG 消失

SVG Disappears After Drag and Drop in IE

如果您查看上面的动画 GIF,您会注意到我在左侧有一个任务图标的下拉列表,我尝试将行拖到可排序容器元素之外,然后,当我将行放回时,SVG 图标完全消失了。这个阴险的错误在我的测试中似乎没有发生在 Windows 7 IE11 上,但在 Windows 10 的 IE11 上发生了!虽然,在我们的示例中,问题是由于使用了 jQuery UI SortablenestedSortable 插件 的组合导致的(它需要能够将项目拖出容器以实现嵌套,任何类型的 DOM 元素分离和/或在 DOM 中移动它们等都可能导致这种消失行为。奇怪的是,在撰写本文时,我无法找到 Microsoft 的工单,但是,如果您有权访问 Windows 10/IE11 设置,您可以亲眼看到这种情况是如何发生的,请查看这个 简单的笔,它是从 fergaldoyle 分叉而来。该笔显示了相同的消失行为,但是,这次是由于简单地通过 JavaScript 的 appendChild 移动包含 SVG 图标的元素导致的。

解决此问题的办法是在回调被调用时,重置 <use> 元素的 href.baseVal 属性,这些 <use> 元素是从 event.target 容器元素派生的。例如,在使用 Sortable 的情况下,我们能够从 Sortable 的 stop 回调内部调用以下方法

function ie11SortableShim(uiItem) {
  function shimUse(i, useElement) {
    if (useElement.href && useElement.href.baseVal) {
      // this triggers fixing of href for IE
      useElement.href.baseVal = useElement.href.baseVal;
    }
  }

  if (isIE11()) {
    $(uiItem).find('use').each(shimUse);
  }
};

我省略了 isIE11 的实现,因为它可以通过多种方式完成(遗憾的是,最可靠的方法是嗅探 window.navigator.userAgent 字符串并匹配正则表达式)。但是,总体思路是,在容器元素中找到所有 <use> 元素,然后将其 href.baseVal 重新分配给 IE 以重新获取这些外部 xlink:href。现在,您可能有一整行复杂的嵌套子视图,可能需要采用更蛮力的方法。在我的例子中,我还需要做

$(uiItem).hide().show(0);

重新渲染行。您的里程可能会有所不同;)

如果您在 Sortable 之外遇到此问题,您可能只需要挂接到父/容器元素上的某个“之后”事件,然后执行相同的操作。

由于我对这个 IE11 特定的问题感到困惑,因此如果您自己也遇到过此问题,有任何替代解决方案和/或对 IE 根问题有更深入的了解,请务必发表评论。

坑七:用 Ajax 策略替换 SVG4Everybody 以提升 IE 的性能

Performance Issues

在原始文章中,我们建议使用 SVG4Everybody 作为不支持使用外部 SVG 定义文件并在通过 xlink:href 属性引用时进行填充的 IE 版本的垫片。但是,事实证明,这样做会导致性能问题,并且可能更加笨拙,因为它基于用户代理嗅探正则表达式。更“直接”的方法是使用 Ajax 加载 SVG 雪碧图。以下是我们执行此操作的代码片段,它与链接文章中的代码基本相同

  loadSprite = null;

  (function() {
    var loading = false;
    return loadSprite = function(path) {
      if (loading) {
        return;
      }
      return document.addEventListener('DOMContentLoaded', function(event) {
        var xhr;
        loading = true;
        xhr = new XMLHttpRequest();
        xhr.open('GET', path, true);
        xhr.responseType = 'document';
        xhr.onload = function(event) {
          var el, style;
          el = xhr.responseXML.documentElement;
          style = el.style;
          style.display = 'none';
          return document.body.insertBefore(el, document.body.childNodes[0]);
        };
        return xhr.send();
      });
    };
  })();

  module.exports = {
    loadSprite: loadSprite,
  };

这对我们来说有趣的部分是 – 在我们图标密集的页面上 – 我们将 IE11 中的加载时间从大约 15 秒缩短到大约 1-2 秒(对于第一次未缓存的页面访问)。

在使用 Ajax 方法时需要考虑的一件事是,在 HTTP 请求得到解决之前,您可能需要处理“没有 SVG 的闪现”。但是,如果您已经有一个加载缓慢的初始 SPA 风格应用程序,并且会显示一个加载器或进度指示器,那么这可能是一笔沉没成本。或者,您可能希望直接内联您的 SVG 定义/雪碧图并利用缓存来获得更好的感知性能。如果是这样,请衡量您增加了多少负载。

坑八:设计非缩放描边图标

在您想要拥有相同图标的不同尺寸的情况下,您可能希望锁定这些图标的描边大小…

为什么,问题是什么?

Strokes VS Fills

假设您有一个 height: 10px; width: 10px; 的图标,其中有一些 1px 的形状,并将其缩放至 15px。这些 1px 的形状现在将变为 1.5px,这最终会导致由于边框显示在子像素边界上而导致图标变模糊或不清晰。这种模糊度还取决于您缩放到的尺寸,因为这将影响您的图标是否位于子像素边界上。通常,最好控制图标的清晰度,而不是将其留给查看者的浏览器的处理。

另一个问题更多的是视觉权重问题。当您使用填充缩放标准图标时,它会按比例缩放……我可以听到您说“SVG 应该这样做”。是的,但是能够控制图标的描边可以帮助它们感觉更相关,并且被视为更像一个系列。我喜欢将其比作使用文本字体进行标题设置,而不是使用显示或标题字体,您可以这样做,但为什么不使用紧凑且清晰的 UI 呢?

准备图标

我主要使用 Illustrator 创建图标,但有很多工具都可以正常工作。这只是我在其中一个工具中的工作流程。我开始创建图标时,会专注于它需要传达的信息,而不是任何技术细节。在我对它满足了我的视觉需求感到满意后,我开始缩放和调整它以满足我们的技术需求。首先,将您的图标调整大小并对齐到像素网格(在 Mac 上,在 Illustrator 中使用 ⌘⌥Y 进行像素预览)到您将要使用的尺寸。我尽量将对角线保持在 45°,并调整任何曲线或奇形怪状的形状以防止它们变得怪异。对此没有固定的公式,只需尽可能将其调整到您喜欢的效果即可。如果它在所需的尺寸下无法正常工作,有时我会放弃整个想法并从头开始。如果它是最佳的视觉解决方案,但没有人能识别出来……它就没有什么价值。

导出 AI

我通常只使用 Illustrator 中的“导出为 SVG”选项,我发现它为我提供了一个标准且最小的起点。我使用“演示属性”设置并保存它。它看起来像这样

<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
  <title>icon-task-stroke</title>
  <polyline points="5.5 1.5 0.5 1.5 0.5 4.5 0.5 17.5 17.5 17.5 17.5 1.5 12.5 1.5" fill="none" stroke="#b6b6b6" stroke-miterlimit="10"/>
  <rect x="5.5" y="0.5" width="7" height="4" fill="none" stroke="#b6b6b6" stroke-miterlimit="10"/>
  <line x1="3" y1="4.5" x2="0.5" y2="4.5" fill="none" stroke="#b6b6b6" stroke-miterlimit="10"/>
  <line x1="17.5" y1="4.5" x2="15" y2="4.5" fill="none" stroke="#b6b6b6" stroke-miterlimit="10"/>
  <polyline points="6 10 8 12 12 8" fill="none" stroke="#ffa800" stroke-miterlimit="10" stroke-width="1"/>
</svg>

我知道您在那里看到了一些 0.5 像素!关于这一点,似乎有几种不同的观点。我更喜欢将描边与像素网格对齐,因为这将是最终显示的效果。坐标放置在 0.5 像素上,以便您的 1px 描边在路径的两侧各为 0.5。它看起来像这样(在 Illustrator 中)

Strokes on the Pixel Grid

坑九:实现非缩放描边

清理

Galactic Vacuum

我们的 Grunt 任务(Rob 在上一篇文章中提到过)几乎清理了所有内容。不幸的是,对于 non-scaling-stroke,您需要对 SVG 进行一些手动清理,但我保证这很容易!只需在您希望限制笔划缩放的路径上添加一个类。然后,在您的 CSS 中添加一个类并应用属性 vector-effect: non-scaling-stroke;,它应该看起来像这样

.non-scaling-stroke {
  vector-effect: non-scaling-stroke;
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
  <title>icon-task-stroke</title>
  <polyline class="non-scaling-stroke" points="5.5 1.5 0.5 1.5 0.5 4.5 0.5 17.5 17.5 17.5 17.5 1.5 12.5 1.5" stroke="#b6b6b6" stroke-miterlimit="10"/>
  <rect class="non-scaling-stroke" x="5.5" y="0.5" width="7" height="4" stroke="#b6b6b6" stroke-miterlimit="10"/>
  <line class="non-scaling-stroke" x1="3" y1="4.5" x2="0.5" y2="4.5" stroke="#b6b6b6" stroke-miterlimit="10"/>
  <line class="non-scaling-stroke" x1="17.5" y1="4.5" x2="15" y2="4.5" stroke="#b6b6b6" stroke-miterlimit="10"/>
  <polyline class="non-scaling-stroke" stroke="currentcolor" points="6 10 8 12 12 8" stroke="#ffa800" stroke-miterlimit="10" stroke-width="1"/>
</svg>

这可以防止笔划(如果指定)在 SVG 缩放时发生变化(换句话说,即使 SVG 的整体尺寸发生变化,笔划也将保持 1px)。我们还在 CSS 脚本中的一个类中添加 fill: none;,在那里我们也控制笔划颜色,因为它们默认情况下将填充 #000000。就是这样!现在,您拥有了漂亮的像素精确笔划,可以保持笔划宽度!

在所有操作完成后(并且您已根据第一篇文章通过 grunt-svgstore 进行预处理),您的 SVG 在 defs 文件中将如下所示

<svg>
  <symbol viewBox="0 0 18 18" id="icon-task-stroke">
    <title>icon-task-stroke</title>
    <path class="non-scaling-stroke" stroke-miterlimit="10" d="M5.5 1.5h-5v16h17v-16h-5"/>
    <path class="non-scaling-stroke" stroke-miterlimit="10" d="M5.5.5h7v4h-7zM3 4.5H.5M17.5 4.5H15"/>
    <path class="non-scaling-stroke" stroke="currentColor" stroke-miterlimit="10" d="M6 10l2 2 4-4"/>
  </symbol>
</svg>

CodePen 示例

左侧的图标集按比例缩放,右侧我们使用的是 vector-effect: non-scaling-stroke;。如果您注意到调整大小的 SVG 图标的笔划开始变得失控,上述技术将使您能够锁定这些笔划。

查看 Chris Rumble(@Rumbleish)在 CodePen 上的 Pen SVG 图标:非缩放笔划

陷阱十:可访问性

Accessible planet illustration

在构建 SVG 图标系统时,很容易忽略可访问性。这很可惜,因为 SVG 本身就是可访问的,尤其是在与图标字体相比时,图标字体众所周知并不总是能与屏幕阅读器很好地配合使用。至少,我们需要添加一些代码来防止屏幕阅读器宣布嵌入在 SVG 图标中的任何文本。虽然我们很想只添加一个带有替代文本的 <title> 标签并“就此作罢”,但 Simply Accessible 的人们发现,Firefox 和 NVDA 实际上不会宣布 <title> 文本。

他们的建议是将 aria-hidden="true" 属性应用于 <svg> 本身,然后添加一个具有 .visuallyhidden 类的相邻 span 元素。该视觉隐藏元素的 CSS 将在视觉上隐藏,但其文本将可供屏幕阅读器宣布。我感到很遗憾,因为它感觉不太语义化,但在 <title> 标签(以及 rolearia-labelledby 等朋友的组合)在浏览器和屏幕阅读器实现中都得到支持之前,这可能是一个合理的折衷方案。在我看来,SVG 上的 aria-hidden 可能是最大的优势,因为我们不希望无意中触发屏幕阅读器,例如页面上的 50 个图标!

以下是借鉴自 Simply Accessible 的 pen 并略作修改的通用模式

<a href="/somewhere/foo.html">
    <svg class="icon icon-close" viewBox="0 0 32 32" aria-hidden="true">
        <use xlink:href="#icon-close"></use>
    </svg>
    <span class="visuallyhidden">Close</span>
</a>

如前所述,这里有两个有趣的地方

  1. 应用 aria-hidden 属性以防止屏幕阅读器宣布嵌入在 SVG 中的任何文本。
  2. 讨厌但有用的 visuallyhidden span,它将由屏幕阅读器宣布。

老实说,如果您宁愿只使用 <title> 标签等方法来编写此代码,我不会与您争论,因为它确实感觉很笨拙。但是,正如我向您展示的我们使用的代码一样,您可以将此解决方案视为版本 1 实现,然后在支持更好的情况下轻松地进行切换……

假设您有一些用于生成 use xlink:href 片段的集中式模板助手或实用程序系统,那么实现上述操作非常容易。我们在 Coffeescript 中执行此操作,但由于 JavaScript 更通用,因此以下是解析到的代码

  templateHelpers = {
    svgIcon: function(iconName, iconClasses, iconAltText) {
      var altTextElement = iconAltText ? "" + iconAltText + "" : '';
      var titleElement = iconTitle ? "<title>" + iconTitle + "</title>" : '';
      iconClasses = iconClasses ? " " + iconClasses : '';
      return this.safe.call(this, "<svg aria-hidden='true' class='icon-new " + iconClasses + "'><use xlink:href='#" + iconName + "'>" + titleElement + "</use></svg>" + altTextElement);
    },
    ...

为什么我们将 <title> 标签作为 <use> 的子元素而不是 <svg> 的子元素?根据 Amelia Bellamy-Royds(受邀的 SVG 和 ARIA 规范专家 @w3c。O'Reilly Media 出版社的 SVG 书籍作者)的说法,您将在更多浏览器中获得工具提示。

以下是 .visuallyhidden 的 CSS。如果您想知道我们为什么以这种特殊方式而不是使用 display: none; 或其他熟悉的方法进行操作,请参阅 Chris Coyier 的文章,其中对这一点进行了深入解释

.visuallyhidden {
    border: 0;
    clip: rect(0 0 0 0);
    height: 1px;
    width: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    position: absolute;
}

此代码并非旨在以“复制粘贴”的方式使用,因为您的系统可能会有细微的差异。但是,它展示了通用方法,并且重要的部分是

  • iconAltText,它允许调用者提供替代文本(如果合适,例如,图标并非纯粹是装饰性的)。
  • aria-hidden="true",现在始终放置在 SVG 元素上。
  • .visuallyhidden 类将视觉上隐藏元素,同时仍使该元素中的文本可供屏幕阅读器使用

如您所见,稍后可以轻松地重构此代码以使用通常推荐的 <title> 方法,并且如果我们选择这样做,至少维护成本不会太高。相关的重构更改可能类似于

var aria = iconAltText ? 'role="img" aria-label="' + iconAltText + '"' : 'aria-hidden="true"';
return this.safe.call(this, "<svg " + aria + " class='icon-new " + iconClasses + "'><use xlink:href='#" + iconName + "'>" + titleElement + "</use></svg>");

因此,在此版本中(感谢 Amelia 提供 aria 部分!),如果调用者传入替代文本,我们不会隐藏 SVG,也不会使用视觉隐藏 span 技术,同时将 rolearia-label 属性添加到 SVG。这感觉更干净,但陪审团尚未确定屏幕阅读器是否会像使用视觉隐藏 span 技术一样支持此方法。也许“专家”(Amelia 和 Simply Accessible 的人员)将在评论中发表意见 :)

额外陷阱:使 viewBox 宽度和高度为整数,否则缩放会变得怪异

如果您有一个导出后具有如下 viewBox 的 SVG 图标:viewBox="0 0 100 86.81",如果您使用 transform: scale,可能会遇到问题。例如,如果您通常将宽度和高度设置为相等(例如 16px x 16px),您可能期望 SVG 应该只是将其自身居中在其包含框中,尤其是在使用 preserveAspectRatio 的默认值时。但是,如果您尝试对其进行任何缩放,您将开始注意到裁剪。

在下方的 Adobe Illustrator 屏幕截图中,您可以看到“吸附到网格”和“吸附到像素”都已选中

Align and Snap to Pixel Grid

以下 Pen 显示前三个图标被裁剪。此特定图标(它被定义为 <symbol>,然后使用我们已经讨论过的 xlink:href 策略进行引用)具有非整数高度为 86.81 的 viewBox,因此我们看到侧面出现了裁剪。接下来的 3 个示例(图标 4-6)具有整数宽度和高度(viewBox 的第三个参数是宽度,第四个参数是高度),并且不会裁剪。

查看 Rob Levin(@roblevin)在 CodePen 上的 Pen SVG 图标:缩放裁剪测试 2

结论

上述挑战只是我们在 Mavenlink 遇到的一些挑战,我们的应用程序中已经拥有一个全面的 SVG 图标系统超过两年了。鉴于我们碎片化的世界中存在各种浏览器、屏幕阅读器和操作系统,其中一些挑战的神秘本质是意料之中的。但是,也许这些额外的陷阱将帮助您和您的团队更好地强化您的 SVG 图标实现!