【非标题党】SVG 图标看我就够了

【非标题党】SVG 图标看我就够了

都 2020 了如果你还没有在项目中使用过 SVG,就好比你没有在项目使用过 REACT 或 VUE 一样。

在不考虑兼容性(IE8+)的情况下,SVG 应该是目前解决项目中图标问题的最佳方案,没有之一。

SVG 在变大变小的情况下不会出现失真(出现锯齿或者看到像素点),也可以像 GIF 一样可以动起来。你也不再会有只是因为同一个图标颜色不同,就需要切两张图的困扰。

只要你愿意,你甚至可以用 HTML,CSS,JS 其中任何一个语言,来实时修改你的 SVG 图标,即使是前后两个图标可能长得完全不一样。

我们项目在建站初期直接就选择了 SVG 作为我们图标的解决方案。虽然以上众多好处让我们体会到了它的好,然而在实际项目中还是遇到了一些坑。为了不做标题党,这里总结了我们团队近三年对于 SVG 使用的大大小小的问题。

  • 一、SVG 的几种使用方式
    • SVG as Img
    • SVG as Sprite (Iconfont)
    • SVG in HTML
    • SVG in CSS
    • SVG in JS
  • 二、技术方案实际体感
    • SVG as Img & Sprite
    • SVG In HTML & CSS
    • SVG In React
  • 三、SVG In React 交付最佳实践
  • 四、SVG In React 使用最佳实践
  • 五、SVG 的一些坑

一、SVG 的几种使用方式

1. SVG as Img

首先,SVG 可以像 JPG,PNG,GIF 一样,作为图片文件去使用。

<style>
[data-icon] {
  display: inline-block;
  width: 1em;
  height: 1em;
  background: no-repeat center/contain;
}
[data-icon][size="24"]{
  width: 24px;
  height: 24px;
}
[data-icon="hello"] {
  background-image: url("./assets/hello.svg");
}
</style>
<i aria-hidden="true" size="24" data-icon="hello"></i>
<img src="./assets/hello.svg" width="24" height="24" alt="hello" />

不管是将它作为背景图片,或者是作为<img/>src属性都可以。

还可以作为一些 embed object iframe … 等标签等src属性,但因为项目中几乎很少用到这里不多做介绍。

2. SVG as Sprite (Iconfont)

我们都知道为了减少图片资源请求数量,会将大多数的小图标合并成雪碧图,然后利用 CSS 控制background-position的位置来实现不同图标的显示。

对于 SVG 实现雪碧图,当然首推的是iconfont只需要将你的 SVG 文件上传上去,然后点击下载,就能得到合并好的三种不同使用姿势的雪碧图(类似知名的还有icomoon)。

方案UnicodeFont classSymbol
兼容性IE6+IE8+IE9+
特点渲染效果不及后两种上手容易,书写直观面向未来

基本上现在只需要基于浏览器兼容性,考虑后面两种方案就好。

3. SVG in HTML

SVG 是使用 XML 来描述二维图形和绘图程序的语言。

因为 SVG 本身是 XML 格式的,这个和 HTML 天然类似,所以可以直接将 SVG 内联到 HTML 中。

<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<rect width="50" height="50" fill="#ff0000"/>
</svg>

比如你想画长宽都是50px 的正方形,你只需要将上面的代码粘贴到 HTML 即可。想要改变颜色,或者尺寸只需要修改对应属性即可,是不是非常的直观和方便。

这也是 SVG 相对于其它图片格式,除开矢量特性之外的另一个不可替代的优势。 可是我们的图标往往不是只使用一次,也并不是每个图标的 HTML 代码量都这么少,如果想通过直接复制粘贴来达到复用的效果就比较的麻烦。

既然是对于 HTML 的复用,那么比较最简单的易行的方式就是使用HTML模版引擎,这里以 nunjunks 举例。

<!-- components/Icons/ISquare.html -->
{% macro ISquare(color='red', size='16') %}
<svg width="{{ size }}" height="{{ size }}" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
  <rect width="50" height="50" fill="{{ color }}"/>
</svg>
{% endmacro %}

这里我们在ISquare.html文件中,定义一个拥有颜色和大小两个入参的组件。

{% import "../components/Icons/ISquare.html" as ISquare %}
<ul>
  <li>{{ ISquare() }} 红色大小为 16 的正方形</li>
  <li>{{ ISquare('yellow', 24) }} 黄色大小为 24 的正方形</li>
</ul>

定义好组件之后,我们只需要在使用的地方import一下,调用即可。这个和你在 React 中定义了一个组件是一样的。

4.SVG in CSS

然而并不是所有的项目中都一定用了模版引擎,你可能只是一个非常简单静态活动页。

对于这样的项目我们可以换一个思路,将 SVG 内联到 CSS 当中。当然这个也只是一个老瓶装新酒的方案,因为很多构建工具都有能力将我们小于 8kb 的图片,转成 base64 的代码内联到我们的 CSS 中。

<style>
[data-icon] {
    display: inline-block;
    width: 1em;
    height: 1em;
    vertical-align: middle;
    background: no-repeat center/contain;
}
[data-icon][size="24"]{
   width: 24px;
   height: 24px;
}
[data-icon="square"] {
    background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGZpbGw9InJlZCIgZD0iTTAgMGgxMDB2MTAwSDB6Ii8+PC9zdmc+");
}
</style>
<ul>
    <li><i data-icon="square"></i> 红色大小为 1em 的正方形</li>
    <li><i data-icon="square" size="24"></i> 红色大小为 24px 的正方形</li>
</ul>

同理我们也可以将我们的 SVG 转成 base64 内联到 CSS 当中。虽然可以借助background-size: contain;这个属性通过控制容器的大小,来间接控制图标的大小,但也失去了对图标颜色的控制。并且这一堆乱码混在 CSS 看起来也是特别的奇怪的。

如果你有跟进张鑫旭老师的博客,你会发现有这样的一篇文章《学习了,CSS中内联SVG图片有比Base64更好的形式》。简单的解释就是 SVG 内联进 CSS 可以不使用 base64 而是直接将 XML 内联进 CSS。

<style>
[data-icon] {
    display: inline-block;
    width: 1em;
    height: 1em;
    vertical-align: middle;
    background: no-repeat center/contain;
}
[data-icon][size="24"]{
   width: 24px;
   height: 24px;
}
[data-icon="square"] {
  background-image: url("data:image/svg+xml,%3Csvg width='50' height='50' viewBox='0 0 50 50' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='red' d='M0 0h50v50H0z'/%3E%3C/svg%3E");
}
[data-icon="square"][color="blue"] {
  background-image: url("data:image/svg+xml,%3Csvg width='50' height='50' viewBox='0 0 50 50' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='blue' d='M0 0h50v50H0z'/%3E%3C/svg%3E");
}
</style>
<ul>
  <li><i data-icon="square"></i> 红色大小为 1em 的正方形</li>
  <li><i data-icon="square" size="24" color="blue"></i> 蓝色大小为 24 的正方形</li>
</ul>

可以看到我们利用 CSS 选择器,映射了两个颜色不一样的 SVG 图标,以间接实现了图标的换色,虽然这个方案并不完美(需要复制一份 XML ),但是这个可读性是比使用 base64 更好的,改一个颜色也是很方便的。

5. SVG In JS

可以看到不管是用 HTML 内联,还是 CSS 内联,都并不是那么的完美。于是我们网页三剑客就只剩下了 JS 。按照道理来说,能用 HTML 和 CSS 解决的问题我们都不会优先选择 JS ,然而随着目前 React 和 Vue 等 JS 框架的流行,JS 反而成为了我们的首选。

于是就有了 SVG 内联进 JS 的方案。这个逻辑其实和利用模版引擎将 SVG 内联进 HTML 原理一样。只是我们换了一个更高级的模版引擎,这里以 React 中的 JSX 为例。

import React from "react";
import "./styles.css";
// import ISquare from "./components/Icons/ISquare";
const ISquare = ({ size = "16", color = "red" }) => (
  <svg width={size} height={size} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
    <rect width="100" height="100" fill={color} />
  </svg>
);
export default function App() {
  return (
    <ul>
      <li><ISquare /> 蓝色大小为 24 的正方形</li>
      <li><ISquare size="24" color="yellow" />黄色大小为 24 的正方形</li>
    </ul>
  );
};

可以看到这里我们定义了一个拥有颜色和大小两个属性的图标组件<ISquare/>。 定义好组件之后,就可以直接使用就可以了,如果你想要这个图标被其它页面使用,只需要将这个组件独立出去即可。并且更可喜的是,你对整个图标里面的所有节点都是有操控能力的。你可以在里面定制任何你想要的逻辑。

二、技术方案实际体感

方案没有好坏,只看它与当下的场景是否贴合
/作为图片SVG 雪碧图(Iconfont)SVG In HTMLSVG In CSSSVG In JS
优点原始,简单在线编辑,多人协作,可换色方便,颜色定制复用方便,独立性强复用方便,自由度高
缺点请求数量,不可换色成本高,图标需要额外处理需要模版引擎需要转格式(颜色复用不便),管理不方便需要特定运行环境

上一节我们介绍了五种使用 SVG 的方式,这一节我们会介绍这些方案实际在项目中的使用体验。

1. SVG as Img & Sprite

首先把 SVG 作为普通图片格式来处理,上手成本是最低的。

但只发挥了 SVG 矢量的这一特性(放大缩小不会失真),同一个图标不同的颜色还是需要两份文件(可以考虑使用 CSS 滤镜来解决这个问题,但是上手成本就高了)。并且一个图标一个请求,当页面图标较多的时候是一个不小的压力(可能 HTTP2 会让请求数量不再是一个问题 )。

为了解决请求数量我们考虑到了图片精灵这个方案。于是我们选择了 Iconfont,它的在线编辑功能还是很好用的,也充当了一个图标管理员的角色,并且可以让设计师也能直接看到我们当前项目中用到的图标,有一种所见即所得的感觉。然而一个项目要接入 Iconfont 可能成本比想象中高。

首先,Iconfont 中要上传图标是严格标准的。处理图标格式这件事情应该谁做就成了一个问题,你说让前端同学去处理吧,又没有几个前端同学了解如何在设计工具中去处理这些规则。你说让设计同学弄吧,这又好像是他们原本不需要做的事情。关键我们不可能把全站用到的 SVG 打包成一个超大的文件,所以多少有一些代码拆分的逻辑,这个你还要设计同学了解,他们内心就更拒绝了。

再者当有图标更新的时候,我们又得重复走一套,设计工具处理,导出,上传,下载,替换本地文件的套路。还经常出现好不容易处理好了,上传到 Iconfont 中还是失败的问题,你就得回到设计工具再处理一次(说出来都是泪)。

按照以往的经验,对于类似工作,其实应该交给构建工具去做更合适。约定一个文件夹格式,然后在这个文件夹中的 SVG 都会自动打包成一个大的图片精灵。

理想很丰满,现实很骨感!

在构建工具中去集成类似图片的处理能力,同样是一堆乱七八糟的事情,特殊依赖装不上不说,还大大拖慢整体项目构建效率。然后已经两天过去了,你可能在项目中连 SVG 的影子都还没有看到,全在做基建的工作了。

2. SVG In HTML & CSS

迫于构建压力,可能采用内联 HTML 或者内联 CSS 更好一些。

当然在这两者当中,如果条件允许(需要依靠模版引擎来实现复用),首推的还是内联 HTML。优势就在于可以通过传参的方式,来定制我们的 SVG 图标。

当然这两者都还是需要有一个从设计工具中的 SVG 素材转换成特定 XML 代码格式的过程。如果你的项目比较小,这里再次推荐一下,张鑫旭老师做的SVG在线压缩合并工具

这里如果非要说一个 内联 CSS 比 内联 HTML 更好的地方在于对于渐变图标的支持。我们都知道在一个 HTML 页面里 ID 是唯一的。而我们的 SVG 图标实现 SVG 渐变的时候,也是通过 ID 来实现复用的。这就很容易出现两个不同的渐变样式的图标,使用了相同 的 渐变 ID 名(这个 ID 是设计工具动态生成的),导致后一个图标直接使用的是前一个图标的渐变样式。而这个逻辑在我们 CSS 内联中是不存在的。

3. SVG In JS

在 React 的世界里,万物皆组件,万物皆被构建。

前面提到的 SVG 人工转 XML 的工作,可以交给@svgr/webpack这个webpack 插件来做。还可以在项目中,引用svgo这个 npm 包,来实现对 SVG 的压缩和优化。

然后在项目中,你的实际使用体验是这样的。

看到这里你会发现,你虽然可以改变 SVG 尺寸但是你好像不知道要怎么去修改 SVG 图标的颜色。

<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M50 0H0V50H50V0Z" fill="#FF0000"/>
</svg>

首先,<ISquare />这个组件,直接暴露给我们的是 svg 这一层, path 相当于其内部组件。想要修改 path 的颜色我们只能通过 CSS 迂回的实现。

svg{
 stroke-width: 0;
 stroke: currentColor;
 fill: currentColor;
}
svg path[fill] {
 fill: currentColor;
 fill-opacity: 1;
}
svg path[stroke] {
 stroke: currentColor;
 stroke-opacity: 1;
}

这个样式的核心就在于currentColor, 通过这个属性我们就可以实现让 svg 内部 path 的 fill 和 stroke 继承 svg 的 color 。

import React from "react";
import "./style.css";
const Icon = ({ size = 16, Svg, ...otherProps}) => (
  <Svg width={size} height={size} {...otherProps}/>
);
export default Icon;

然后我们将这个逻辑封装进另一个名叫<Icon />组件中,还在这个组件当中将 width 和 height 用 size 替代以减少暴露的接口数量。

这样我们就得到了现在这样的写法(也可以把颜色这个属性内聚到<Icon/>组件内部,这里是为了降低代码演示的逻辑复杂度)。

照理来说,如果这个修改尺寸和颜色的逻辑在@svgr/webpack就能实现的话,这里额外封装的<Icon />是多余的。

三、SVG In React 交付最佳实践

前面几节里,我们始终都是在如何优化构建逻辑这个小小圈子里面打转。在实际使用中更影响体验的还有一个难题——SVG管理交付

比如之前提到的,Iconfont 要做图标精灵代码分割就很难。通常我们把全局都要用到的图标,打包成一个 SVG 雪碧图,然后其它的按照页面去分别打包。但是往往会出现的问题是,有一个页面它就是很特别,它就只用到了全局那个 SVG 雪碧图中的一个,你怎么办?或者 A 页面 和 B 页面共享了某几个图标,你又怎么办?并且我们这个逻辑,设计同学可能完全没有感知。

除开 SVG In CSS 之外,对于 Svg In Html 和 Svg In JS 来说,可以将每一个图标文件集中放到一个文件夹下(./components/Icons/*),然后按需加载引用,就没有了分组管理的问题。然而一切还是想得太简单。

比如某次迭代中我们需要一个为别人点赞的图标,A 同学导出了这个图标,并取名为了good.svg,B 同学也发现自己也需要这个图标,但是他在图标文件夹里没有找到like.svg,于是他导出了一个名为like.svg的图标。

随着项目推进,设计师又设计了这个图标的另外一个描边版本good-outline.svg。然后隔两天又新加入了一个设计师,又设计了一个渐变版本like-gradient.svg

后面老大看到整个网站说,为啥这个网站同一个点赞图标又这么多样式,给我统一成一个样式。然后设计师又出了一个新版good-new.svg让你全局替换。此时我想你是蒙圈的。因为你并不知道这些图标分布在哪个页面的哪个角落,并且他们名字还不一样,你也不敢删你也不敢问。

出现这个状况的原因,在于开发人员和设计同学在维护同一套设计资源。并且两端的管理方式还可能不太一致。作为开发者的我们自然会想,既然设计同学已经有一套管理方案了?那我们是否可以直接拿过来使用呢?

这个难题在我看到使用 Figma + GitHub Actions 完成 SVG 图标的完全自动化交付这篇文章之后,变成了现实(为原作者打 Call)。

简单介绍一下就是,设计师只需要在 Figma (设计工具) 中编辑和修改好图标之后,点击update按钮,然后前端npm install一下一切就都搞定了,之前繁琐的管理构建在使用阶段统统不需要关心(整体的时间延迟,大约2~3分钟)。

具体逻辑可以看原文详情的介绍,这边特别需要指出的是,这个 figma svg 转 npm 的过程,我们是可以在 github 仓库中全程接管的,也就是说可以基于实际项目去定制想要的任何逻辑。这里介绍一下基于我们团队的调性,模改出来的 React 版本:

  1. 精简了最后生成组件的样式;
  2. 和设计师约定以下划线开头的图标会被忽略不会发布到 npm 中;
  3. 相同文件名的图标,只会选择最后一个;
  4. 生成的 React component 的名称统一会以大写字母I开头,比如<ICamera />
  5. 生成的组件顺序会按照首字母排序(找起来更容易);
  6. 原作者是用的 github pages 来展示的图标,为了更高的自由度,我们这边替换成了用 codesandbox 来作为图标的展示。
想要相同模改版本的同学,可以点击这里 Fork webnovel-icons

可以看到目前我们整个网站,有 183 个图标,然后这个展示的界面,还可以修改图标大小,颜色,以及背景颜色,需相同展示 Demo 的同学可以直接点击链接Edit on CodeSandBoxFork 使用。

这一切最值得开心的就是,前端同学完全不需要维护这一整套图标,设计同学直接就可以帮我们搞定。我们要做的就是,当图标有更新的时候npm install一下。

四、SVG In React 使用最佳实践

经过这一翻操作之后,我们就将图标管理直接甩锅给了设计同学,接下来我们就只需要解决图标使用问题了。

可以看到这里我们兼容了@svgr/webpackwebnovel-icons两种图标引用方式。

在我们项目中实际是通过 CSS 作为我们的 Token 来管理颜色的。所以并未在 React Component 中暴露类似color属性来修改颜色。这里可以基于自己实际项目定制。

.icon {
  display: inline-block;
  vertical-align: middle;
}
.icon:not(._self_color) {
  stroke-width: 0;
  stroke: currentColor;
  fill: currentColor;
}
.icon:not(._self_color) path[fill] {
  fill: currentColor;
  fill-opacity: 1;
}
.icon:not(._self_color) path[stroke] {
  stroke: currentColor;
  stroke-opacity: 1;
}

同时为了兼容多色图标,我们额外暴露一个属性isSelfColor利用 CSS 选择器去决定是否去掉我们图标默认的颜色。我们项目中单色图标的使用场景是远远超过多色图标的,所以我们默认是单色图标。这样我们就算比较完整的解决了 SVG 使用过程中的几大问题。

  1. SVG 管理
  2. SVG 压缩,优化
  3. SVG 转 React Component
  4. SVG 定制大小,修改颜色
  5. 多色图标的支持
  6. 其它自由定制需求

这边还有一点需要特别提出的是,@svgr/webpackwebnovel-icons两种方式的虽然都可以做到 SVG 的压缩和优化,但是他们的时间点是不一样的。@svgr/webpack是在 webpack 去实时处理的,而webnovel-icons是在转成 React component 之前就处理好了。

五、SVG 的一些坑

1. SVG 变形

在使用某些圆形图标的时候,会发现这个图标在设计稿中是圆的,但是在浏览器里面就是不圆。这种情况,通常是因为我们的图标里面的 path 路径不是整数造成的。发生这种情况需要麻烦设计师将其改为整数。但是这个是有概率的,只能说是尝试一下,不行就得继续改(这个问题其实有点迷)。

这边猜测可能是压缩工具导致的,当SVG原生尺寸较小的时候,数值可能是 1.233458这样,压缩工具会取合适的小数位数,结果就会导致细节失真。如果 SVG 原始尺寸较大,例如iconfont.cn中图标都是 200 以上,就不容易出现这样的问题。

2. SVG 事件没有触发

在某些浏览器中,如果直接在 SVG 组件上绑定 onClick 事件,这个事件并不会触发。这边我们建议的逻辑是在<svg/>标签外再套一层<i/>标签或者是<button />标签。然后将这个事件绑定到外容器上。

3. SVG 的颜色只变了一半

这种问题往往是一个 SVG 图标中,同时出现了 fill,stroke 两种类型的 path,这种建议让设计师在设计工具中将图标先轮廓化(outline stroke), 然后再扁平化(Flatten)。这个可能不同设计工具叫法不一样,可以咨询一下设计师。

4. SVG 边缘被切掉了

这个问题在于,设计同学提供的 SVG 贴边了,建议在 SVG 图标和容器之间至少留一像素。

End

到这里我们已经介绍了,在大大小小项目中使用 SVG 图标的各种姿势。希望能够尽可能全的帮助到大家,所以体量有一些的大。如果有遗漏的或者是大家有更好的方案,也欢迎大家给我留言,交流。

当然 SVG 的优势还远不止如此,特别是在一些动画能力上是特别方便以及优秀的。这里也是非常推荐大家尝试的。

本文作者:ziven27
原创声明:本文为阅文前端团队 YFE 成员出品,请尊重原创,转载请联系公众号 ( id: yuewen_YFE ) 获取授权,并注明作者、出处和链接。
编辑于 2020-09-24 15:20