SVG、SVG Symbol、字体图标调研与工程优化

Icon 是前端非常重要的基础组件,几乎每一个组件库都提供了对应的图标库,也有很多专门提供图标库的平台,比如 iconfont,iconpark。

这篇文章将会介绍 Icon 在 web 端的主流实现方案、对比、以及对于图标库使用的优化。

主流图标方案

前端侧对图标的应用方式,分为了如下三种:

  1. SVG 图标
  2. 字体图标
  3. SVG Symbol 图标

SVG 图标

SVG 是解决图标问题的好手,其具有一下特点:

  1. 无损缩放,因为 SVG 是矢量图。
  2. 修改方便,SVG 基于 XML 格式,能快速修改
  3. CSS 控制样式,SVG 可以使用 CSS 控制样式

因此 SVG 是最主流的图标方案。

接着我们看一下前端一般使用 svg 图标的方式。

图标组件

一种做法是将 SVG 图标封装成一个前端组件,并提供一个 NPM 包。使用者通过安装 NPM 包的方式使用 SVG 图标。

这样做的好处是集中化管理图标,便于图标的复用。

比如使用 IconPark 中的图标,在安装了对应的 npm 包之后,你可以在平台上直接复制对应的组件代码。

image.png

我们简单看一下源代码,可以看到的 react 组件中包裹了 svg

image.png

IconPark 不仅仅是对于 svg 的一层简单封装,IconPark 还实现了一套对于图标风格的精细化控制。更多细节可以查看 IconPark 的源代码。

并不是所有的图标组件都实现的如此复杂,比如 element-plus 提供的图标库,就是对 svg 做了一层简单的封装。

SVG Loader

另一种方式是借助打包工具的能力,在处理 SVG 文件的时候,通过 Loader 转换成对应的前端组件,以实现在工程中使用 SVG 图标。

比如使用 SVGR,通过 SVGR 可以将 SVG 转换成一个 React 组件。

这种做法更加适合于“项目属性”的图标,因为不具备通用性,所以不需要放在图标库,借助打包工具,能轻松实现在项目中快速使用 SVG。

字体图标

什么是字体图标?首先我们需要了解一下字体的基本概念。

字体图标原理

世界上几乎所有的字符都分配了一个唯一的数字标识,这个数字标识被称为码点(code point),而我们可以用 Unicode 来表示这个唯一的数字标识。

而字体是用于显示字符的可视化表示的集合。每个字符在字体中都有一个对应的图形形状,字体定义了这些形状的外观和样式。字体文件包含了所有字符的图形描述和相关的元数据。

例如可以用 U+0041 表示拉丁字母 A。

image.png

所以,我们可以制作出一个名为 Iconfont 的字体,该字体中存在某一个码点,这个码点对应的字体的形状和 Icon 的形状一样。

比如 U+E66E 表示一个书签形状的字,并且使用 SVG 来表现这个字形。

image.png

这就是 Icon font 的实现原理。接下来简单介绍一下前端如何使用 Icon font。

字体本身也是一种矢量图,所以 SVG 也可以去生成字体。而我们常见的字体图标都是通过 svg 生成一套对应的字体。

在制作字体时,常用的矢量图形格式主要有以下几种:

  1. SVG (Scalable Vector Graphics):SVG 是一种基于 XML 的矢量图像格式,它支持动画和交互性。SVG 文件在网页设计中非常流行,因为它们可以无损缩放并且文件大小相对较小。
  2. EPS (Encapsulated PostScript):EPS 是一种基于 PostScript 描述语言的矢量图像格式,它被广泛用于高分辨率图像的打印。
  3. AI (Adobe Illustrator):AI 是 Adobe Illustrator 的原生文件格式,它是一种非常强大的矢量图像格式,可以包含复杂的图形和效果。
  4. PDF (Portable Document Format):虽然 PDF 通常用于分发文档,但它也支持矢量图形,因此可以用于字体设计。
  5. Glyphs:这是一种专门用于字体设计的文件格式,由 Glyphs 字体编辑器使用。
  6. UFO (Unified Font Object):UFO 是一种开放的字体文件格式,它是为字体设计和生产工作流程而设计的。

使用字体图标

前端主要借助 @font-face 实现,我们以阿里的 IconFont 提供的 demo 为例:

创建了一个名为 helloIcon 的字体,并指定了字体资源的位置,这里用的是 IconFont 提供的 CDN。

@font-face {
  font-family: 'helloIcon';
  src: url('//at.alicdn.com/t/c/font_.woff2?t=') format('woff2'),
       url('//at.alicdn.com/t/c/font_.woff?t=') format('woff'),
       url('//at.alicdn.com/t/c/font_.ttf?t=') format('truetype');
}

cdn 地址被我删减了一部分,这里仅做演示

接着声明一个类,这个类使用刚刚的 helloIcon 字体作为字体

.iconfont{
    font-family:"helloIcon" !important;
    font-size:16px;font-style:normal;
    -webkit-font-smoothing: antialiased;
    -webkit-text-stroke-width: 0.2px;
    -moz-osx-font-smoothing: grayscale;
  }

然后在代码中,我们给标签加上一个对应的 class,就能将 Icon 当成一个字体使用了。

<i class="iconfont">&#x33;</i>

浏览器渲染内容的时候,会将字体对应的字形渲染出来。

更多关于 @font-face 的介绍: https://www.zhangxinxu.com/wordpress/2017/03/css3-font-face-src-local/

SVG Symbol

SVG Symbol 是一个新的组织 SVG 的形式,其思路和雪碧图类似,通过将多个 SVG 组合成同一个文件,以减小整体的 SVG 体积。

SVG Symbol 原理

简单来说就是利用 Symbol 标签,将 SVG 分成一个一个的“子组件”,并且给上 ID 编号,然后在使用 SVG 的时候再通过 Use 标签引用对应的“子组件”。

例子如下:

<svg>
    <symbol id="first"><!-- 第1个图标路径形状之类代码 --></symbol>
    <symbol id="second"><!-- 第2个图标路径形状之类代码 --></symbol>
    <symbol id="third"><!-- 第3个图标路径形状之类代码 --></symbol>
</svg>

在使用 SVG 的时候,使用下面这种方式使用:

<svg aria-hidden="true">
    <use xlink:href="#fist"></use>
</svg>

拓展阅读:https://www.zhangxinxu.com/wordpress/2014/07/introduce-svg-sprite-technology/

兼容性方面,SVG Symbol 兼容现代浏览器。

image.png

SVG Symbol 如何使用

一般通过使用 svg-sprite-loader 将项目中的 SVG 处理成 SVG Symbol。

// webpack.config.js
{
  test: /\.svg$/,
  use: [
    { loader: 'svg-sprite-loader', options: { ... } },
    'svg-transform-loader',
    'svgo-loader'
  ]
}

整体的使用也较为简单,细节可以参考文档:svg-sprite-loader

SVG Symbol 不会用在图标组件库,因为使用了 SVG Symbol 等于把 SVG 合成了一个整体,如果图标组件库使用了 SVG Symbol,将会失去 Tree Shaking 带来的体积优化。

方案优劣对比

SVG 的优缺点

优点:

  1. 支持的功能更完整,字体图标本身也是 SVG,但是有很多限制,直接使用 SVG 则不存在这些限制。
  2. 没有被转换成 font,前端能获取到 svg,对代码会有更强的控制权(解决一些奇怪的 bug)

缺点:

  1. SVG 体积比 Icon 大
  2. SVG 渲染性能不如 Icon(待考证,出处为下方链接)
  3. 兼容性不如 Icon font

渲染性能出处:https://www.iconfont.cn/help/article_detail?article_id=7

古老的 IE 浏览器干啥啥不行,于是人们想出了将 SVG 转换成 Font 的方式,去规避兼容性问题。但对于现代浏览器来说,不存在兼容性问题。

image.png

字体图标的优缺点

优点:

  1. 不考虑多色图标的情况下,兼容性优秀
  2. 体积更小

缺点:

  1. 相较于 SVG 的使用存在诸多限制
    1. 常规方式下不支持多色
    2. 不支持透明度
    3. 尺寸相对固定,尺寸不对可能会出现锯齿
  2. 制作字体图标的 SVG 需要遵守许多规则

image.png

Icon Font 已经支持了多色图标: 参考链接:https://www.iconfont.cn/help/article_detail?article_id=7

决策参考

我个人认为如果业务场景不需要极致优化 SVG 体积,那么不需要使用字体图标,直接使用 SVG 会更简单高效。

图标优化方案

SVGO

SVGO 是一个开源库,提供了很多的配置项,专门用于精简 SVG 的体积。

SVGO 提供了一套默认的规则,帮助你优化 SVG 体积。我们在上文提到的 SVGR 便使用了 SVGO 来优化 SVG 的体积。

image.png

使用方法

用法很简单,只需要引入 svgo 即可,兼容 node 与 browser 环境。

import svgo from "svgo";

const content = 'svg string'

const result = svgo.optimize(content, {
  plugins: [
    {
      name: 'preset-default',
    },
  ],
}).data;

注意点

虽然 SVGO 提供了一套默认的规则,但是有一些规则需要注意:

removeViewBox 规则需要关闭

去掉 Viewbox 将会导致图标缩放出现问题,有时候会出现裁剪问题。SVGO 的官方文档也提到了这个问题。

image.png

removeUselessStrokeAndFill 规则需要关闭

比如下面这个图标,当你将 fill="none" 去掉后,浏览器会使用默认的 fill 属性值,通常是黑色 (#000000)。

image.png image.png

SVGO 版本问题

从 3.2.0 版本的 SVGO 在处理一些 svg 会报错,看 issues 作者已经修复了,不过我还是用不了,解决方案是退回到 3.1.0 版本。

SVGO-loader 版本问题

最新的 SVGO loader 使用了 webpack 5 内置的 getOptions 方法获取环境信息。而我们使用的 webpack 大版本是 4,因此不能使用最新版的 SVGO loader。

SVG Symbol

在上文我们已经介绍过了 SVG Symbol,以及可以在项目中使用 svg-sprite-loader 进行体积优化。这里主要讲述 svg-sprite-loader 和 svgo-loader 配合使用时的一些注意事项。

loader 加载顺序

两个 loader 执行存在先后顺序,需要 svgo loader 先执行,否则插件在编译的时候会出现错误。

不能启动 svgo loader 的 prefixIds 规则

svg-sprite-loader 会将每个 SVG 的 def 提取出来,而 svgo-loader 的 prefixIds 将会修改 id,导致无法和 def 对应上。

字体图标

目前看,字体图标最大的优势在于同等数量下,字体图标的体积最小。关于字体图标的工程化方案我并没有尝试过,但是我依然找到了几个工具库,可以实现将 SVG 图标转换为字体图标。

体积对比

测试两种 Icon,一种是单色图标,一种是多色图标。主要对比打包之后的体积大小。

图标类型IconSVG 集合SVG Symbol
单色

(78个)
image.png
42KB
image.png
90KB
image.png

77KB
多色1

(40个)
image.png


277 KB
image.png

586KB
image.png

606KB
多色 2

(32 个)
image.png

29KB
image.png

61KB
image.png

56 KB

单色图标:icon < SVG Symbol < SVG 多色图标 1:icon < SVG < SVG Symbol 多色图标 2:icon < SVG Symbol < SVG

总的来看,无论单色还是多色图标,Icon 的体积会比 SVG 小。

SVG 与 SVG Symbol 相比,一般是 SVG Symbol 体积更小。

但是多色图标 1 是 SVG 更小,SVG Symbol 更大,猜测是当 SVG 本身比较复杂的时候,SVG Symbol 的提升有限。这点需要更多的佐证,从理论上分析应该是 SVG Symbol 更小才对,但是实际情况又有出入。

总结

以上便是关于图标使用的方案总结。

我个人给出的建议是:

  1. 项目中的业务图标使用 svgo-loader 和 svg-sprite-loader 做优化。
  2. 公共的图标则可以模仿 IconPark 那样,提供多个版本的图标库 NPM 包。
  3. 如果对图标体积敏感则建议使用字体图标。