使用 React 创建 SVG 图标系统

Avatar of Sarah Drasner
Sarah Drasner

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

我最近参加了 Michael Jackson 和 Ryan Florence 的 ReactJS 训练营。我非常兴奋地参加了这次训练营,部分原因是我有很多关于 SVG 和 React 的问题。使用 React 和 SVG,尤其是对 SVG 进行操作,有很多功能目前还不太支持。对我来说,一个主要的差距是 <use> 元素,因为大多数 SVG 图标系统都是使用 <use> 构建的。

我问 Michael 他是否认为这些功能中的一些可能会得到更好的支持,但他向我展示了一种更好的使用方式,完全绕过了这种方法。我们将讨论这种技术,以便您可以开始在 React 中编写可扩展的 SVG 图标系统,以及一些我认为可以很好地工作的技巧。

注意:值得一提的是,use 支持 最近有所改进,但我注意到它充其量只是零星存在,而且存在其他路由和 XML 问题。我们将在这里向您展示另一种更简洁的方法。

什么是 <use>

对于那些不熟悉 SVG 图标系统通常如何构建的人来说,它的工作方式有点像这样。<use> 元素克隆了任何其他 SVG 形状元素的副本,该元素的 ID 在 xlink:href 属性中引用,并且仍然可以对其进行操作而无需重复所有路径数据。您可能想知道为什么人们不会直接使用 SVG 作为 <img> 标签。您可以这样做,但每个图标都会是一个单独的请求,而且您将无法访问更改 SVG 的部分,例如 fill 颜色。

使用 <use> 使我们能够将图标的路径数据和基本外观定义在一个地方,这样它们就可以更新一次并在所有地方更改,同时仍然让我们能够动态更新它们。

Joni Trythall 有一篇关于 use 和 SVG 图标的 很棒的文章,Chris Coyier 也在 CSS-Tricks 上写了另一篇 很棒的文章

如果您想看看标记是什么样的,这里有一个小例子

查看 CodePen 中 Sarah Drasner (@sdras) 的 bc5441283414ae5085f3c19e2fd3f7f2

为什么要使用 SVG 图标?

有些人可能想知道为什么我们要使用 SVG 图标系统,而不是一开始就使用图标字体。我们有 关于这个问题的比较。另外还有 大量的人 写文章 以及 发言 关于这个问题。

在我看来,以下是一些更令人信服的原因

  • 图标字体很难做到无障碍。SVG 可以添加标题和 ARIA 标签,这极大地提高了无障碍性,尤其是在图标单独存在且是信息导航的唯一来源的情况下。想想:盲人、阅读障碍者、老年人(您迟早也会成为老年人,所以,如果您不是那种关心这部分人群的开发者,就为了积累善业而做吧!但说真的,关心老年人。)
  • 图标字体在某些显示器上不够清晰。您可以通过在 CSS 中进行一些精密的字体平滑来避免这种情况,但我注意到有一点:很难覆盖字体平滑,而不会完全关闭字体平滑。总的来说,SVG 更清晰,因为它们是专门用来绘制的。
  • 图标字体经常出现故障。我认识的大多数开发人员都遇到过在框中缺少字形 X 的情况,图标字体有很多种方式会导致故障,而 SVG 则不会。无论是 CORS 问题还是 Opera mini,它都是一个让人头疼的问题。
  • 图标字体很难定位。它们是一种使用字体样式来定位的图像。没什么好说的。您无法对它们的部分进行动画,除非使用技巧进行堆叠。SVG 提供了一个可导航的 DOM,用于对图标的各个部分进行动画或对部分进行着色。并不是每个人都想这样做,但拥有这种选择总是很不错的。

如果您像我一样,需要更新一个巨大的代码库,而为了从图标字体迁移到 SVG,您必须更新数百个标记实例,我理解您的感受。在这种情况下,可能不值得花时间。但如果您要重写视图并使用 React 更新它们,那么值得重新考虑这个机会。

简而言之:您在 React 中不需要 <use>

在 Michael 耐心地听我解释了我们如何使用 <use> 以及让我展示一个示例图标系统后,他的解决方案很简单:它实际上没有必要。

考虑一下:我们定义图标以便之后重用它们(通常是在 <defs> 中作为 <symbol>),唯一的原因是为了避免重复,并且只需要在一个地方更新 SVG 路径即可。但 React 已经允许这样做。我们只需创建组件

// Icon
const IconUmbrella = React.createClass({
 render() {
   return (
     <svg className="umbrella" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" aria-labelledby="title">
	<title id="title">Umbrella Icon</title>
        <path d="M27 14h5c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2v0zM27 14c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2v0 14c0 1.112-0.895 2-2 2-1.112 0-2-0.896-2-2.001v-1.494c0-0.291 0.224-0.505 0.5-0.505 0.268 0 0.5 0.226 0.5 0.505v1.505c0 0.547 0.444 0.991 1 0.991 0.552 0 1-0.451 1-0.991v-14.009c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-5.415 6.671-9.825 15-9.995v-1.506c0-0.283 0.224-0.499 0.5-0.499 0.268 0 0.5 0.224 0.5 0.499v1.506c8.329 0.17 15 4.58 15 9.995h-5z"/>
      </svg>
   )
 }
});

// which makes this reusable component for other views
<IconUmbrella />

查看 CodePen 中 Sarah Drasner (@sdras) 的 SVG 图标在 React 中

我们可以反复使用它,但与旧的 <use> 方式不同,我们没有额外的 HTTP 请求。

您可能会从上面的示例中注意到两件与 SVG 相关的事情。首先,我没有这种输出

<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

或者甚至在 SVG 标签本身也不会有这个

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" …

这是因为我在将标记添加到任何地方之前,确保使用 SVGOMGSVGO 对我的 SVG 进行了优化。我强烈建议您也这样做,因为您可以将 SVG 的大小减小到相当大的程度。我通常看到大约 30% 的百分比,但可以高达 60% 或更高。

您可能还会注意到我添加了一个标题和 ARIA 标签。这将有助于屏幕阅读器为使用辅助技术的人朗读图标。

 <svg className="umbrella" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" aria-labelledby="title">
  <title id="title">Umbrella Icon</title>

由于此 ID 必须是唯一的,因此我们可以对图标的实例传递属性,它将像这样传播到标题和 aria 标签

// App
const App = React.createClass({
  render() {
    return (
      <div>
        <div className="switcher">
          <IconOffice iconTitle="animatedOffice" />
        </div>
        <IconOffice iconTitle="orangeBook" bookfill="orange" bookside="#39B39B" bookfront="#76CEBD"/>
        <IconOffice iconTitle="biggerOffice" width="200" height="200"/>
      </div>
    )
  }
});

// Icon
const IconOffice = React.createClass({
 ...
 render() {
   return (
     <svg className="office" xmlns="http://www.w3.org/2000/svg" width={this.props.width} height={this.props.height} viewBox="0 0 188.5 188.5" aria-labelledby={this.props.iconTitle}>
        <title id={this.props.iconTitle}>Office With a Lamp</title>
        ...
      </svg>
   )
 }
});
 
ReactDOM.render(<App/>, document.querySelector("#main"));

也许最好的部分

整个事情中真正酷的一点是:除了不需要额外的 HTTP 请求之外,我还可以在将来完全更新 SVG 的形状,而无需进行任何标记更改,因为组件是自包含的。更棒的是,我无需在每个页面上加载整个图标字体(或 SVG 精灵)。由于所有图标都被组件化了,我可以使用 webpack 之类的工具来“选择”我需要的任何图标,用于特定视图。考虑到字体本身的重量,尤其是重量很大的图标字体字形,这为性能优化带来了巨大的可能性。

除此之外,我们还可以通过 SVG 和属性以非常简单的方式动态修改图标的部分,使其呈现不同的颜色或进行动画。

动态修改

您可能注意到这里我们还没有动态调整它,而这正是我们使用 SVG 的原因之一,对吧?我们可以为图标声明一些默认属性,然后更改它们,如下所示

// App
const App = React.createClass({
  render() {
    return (
      <div>
        <IconOffice />
        <IconOffice width="200" height="200"/>
      </div>
    )
  }
});

// Icon
const IconOffice = React.createClass({
  getDefaultProps() {
    return {
      width: '100',
      height: '200'
    };
  },
 render() {
   return (
     <svg className="office" width={this.props.width} height={this.props.height} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 188.5 188.5" aria-labelledby="title">
        <title id="title">Office Icon</title>
        ...
      </svg>
   )
 }
});
 
ReactDOM.render(<App />, document.querySelector("#main"));

查看 CodePen 中 Sarah Drasner (@sdras) 的 SVG 图标在 React 中带有默认属性

让我们更进一步,根据实例更改一些外观。我们可以为此使用 props,并声明一些默认属性。

我喜欢 SVG,因为我们现在拥有一个可导航的 DOM,所以在下面,让我们使用 fill 动态更改多个形状的颜色。请记住,如果您习惯于处理图标字体,那么您不再使用 color 更改颜色,而是使用 fill。您可以查看下面的第二个示例,看看它如何工作,书已经改变了颜色。我还喜欢能够动态地为这些部分制作动画,在下面,我们将其包装在一个 div 中,以便使用 CSS 很容易地对其制作动画(您可能需要点击重新运行才能看到动画播放)

查看 CodePen 中 Sarah Drasner (@sdras) 的 SVG 图标在 React 中带有默认属性和动画

// App
const App = React.createClass({
  render() {
    return (
      <div>
        <div className="switcher">
          <IconOffice />
        </div>
        <IconOffice bookfill="orange" bookside="#39B39B" bookfront="#76CEBD" />
        <IconOffice width="200" height="200" />
      </div>
    )
  }
});

// Icon
const IconOffice = React.createClass({
  getDefaultProps() {
    return {
      width: '100',
      height: '200',
      bookfill: '#f77b55',
      bookside: '#353f49',
      bookfront: '#474f59'
    };
  },
 render() {
   return (
     <svg className="office" xmlns="http://www.w3.org/2000/svg" width={this.props.width} height={this.props.height} viewBox="0 0 188.5 188.5" aria-labelledby="title">
        <title id="title">Office Icon</title>
        <g className="cls-2">
          <circle id="background" className="cls-3" cx="94.2" cy="94.2" r="94.2"/>
          <path className="cls-4" d="M50.3 69.8h10.4v72.51H50.3z"/>
          <path fill={this.props.bookside} d="M50.3 77.5h10.4v57.18H50.3z"/>
          <path fill={this.props.bookfront} d="M60.7 77.5h38.9v57.19H60.7z"/>
          <path className="cls-7" d="M60.7 69.8h38.9v7.66H60.7z"/>
          <path className="cls-5" d="M60.7 134.7h38.9v7.66H60.7z"/>
          ...
      </svg>
   )
 }
});
 
ReactDOM.render(<App />, document.querySelector("#main"));
.switcher .office {
  #bulb { animation: switch 3s 4 ease both; }
  #background { animation: fillChange 3s 4 ease both; }
}

@keyframes switch {
  50% {
    opacity: 1;
  }
}

@keyframes fillChange {
  50% {
    fill: #FFDB79;
  }
}

我 Trulia 的一位很棒的同事,Mattia Toso,还推荐了一种非常不错、更简洁的声明所有这些属性的方式。我们可以通过为所有使用情况声明常量来减少 this.props 的重复,然后只需简单地应用该变量即可

render() {
   const { height, width, bookfill, bookside, bookfront } = this.props;
   return (
     <svg className="office" xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 188.5 188.5" aria-labelledby="title">
        <title id="title">Office Icon</title>
        <g className="cls-2">
          <circle id="background" className="cls-3" cx="94.2" cy="94.2" r="94.2"/>
          <path className="cls-4" d="M50.3 69.8h10.4v72.51H50.3z"/>
          <path fill={bookside} d="M50.3 77.5h10.4v57.18H50.3z"/>
          <path fill={bookfront} d="M60.7 77.5h38.9v57.19H60.7z"/>

我们还可以通过为我们使用的属性声明 propTypes 来使它更加出色。PropTypes 非常有用,因为它们就像我们正在重用属性的文档。

propTypes: {
  width: string,
  height: string,
  bookfill: string,
  bookside: string,
  bookfront: string
},

这样,如果我们使用不当,比如下面的例子,我们会得到一个控制台错误,不会阻止代码运行,但会提醒可能与我们协作的其他人(或我们自己)我们正在错误地使用道具。这里,我使用的是数字而不是字符串作为我的道具。

<IconOffice bookfill={200} bookside="#39B39B" bookfront="#76CEBD" />

然后我得到以下错误

查看 Pen 带有错误的 React 中的 SVG 图标使用扩展运算符 by Sarah Drasner (@sdras) on CodePen.

React 0.14+ 变得更加精简

在 React 的新版本中,我们可以减少一些这些冗余,并进一步简化我们的代码,但前提是它是一个非常“愚蠢”的组件,例如,它不使用生命周期方法。图标是一个很好的用例,因为我们主要只是渲染,所以让我们试一试。我们可以摆脱 React.createClass 并将我们的组件写成简单的函数。如果您使用 JavaScript 很长时间,但不太熟悉 React 本身,这会很不错 - 它读起来就像我们都习惯使用的函数。让我们进一步清理我们的道具,并像在网站上一样重复使用雨伞图标。

// App
function App() {
  return (
    <div>
      <Header />
      <IconUmbrella />
      <IconUmbrella umbrellafill="#333" />
      <IconUmbrella umbrellafill="#ccc" />
    </div>
  )
}
 
// Header
function Header() {
 return (
   <h3>Hello, world!</h3>
 )
}

// Icon
function IconUmbrella(props) {
  const umbrellafill = props.umbrellafill || 'orangered'
  
  return (
    <svg className="umbrella" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" aria-labelledby="title">
      <title id="title">Umbrella</title>
      <path fill={umbrellafill} d="M27 14h5c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2v0zM27 14c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2v0 14c0 1.112-0.895 2-2 2-1.112 0-2-0.896-2-2.001v-1.494c0-0.291 0.224-0.505 0.5-0.505 0.268 0 0.5 0.226 0.5 0.505v1.505c0 0.547 0.444 0.991 1 0.991 0.552 0 1-0.451 1-0.991v-14.009c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-5.415 6.671-9.825 15-9.995v-1.506c0-0.283 0.224-0.499 0.5-0.499 0.268 0 0.5 0.224 0.5 0.499v1.506c8.329 0.17 15 4.58 15 9.995h-5z"/>
    </svg>
  )
}
 
ReactDOM.render(<App />, document.querySelector("#main"));

查看 Pen React 中的 SVG 图标 by Sarah Drasner (@sdras) on CodePen.

SVG 图标系统在 React 中非常简单,易于扩展,减少 HTTP 请求,并且由于我们可以将来完全更新输出,而无需进行任何重复的标记更改,因此易于维护。我们可以通过选择我们需要的来提高性能。我们可以使用道具动态地更改颜色,甚至添加 CSS 动画。所有这些,我们还可以使其对屏幕阅读器可访问,这使得 React 和 SVG 图标系统成为在 Web 上添加图标的非常好的方法。