基于svg-sprite的svg icon方案实践

2,620 阅读11分钟
原文链接: tech.lede.com

Icon 发展历程

初期 img 的方案

小时候,我们都是这样在前端应用一个 icon 的:

  1. 网上找到相关的图片资源或者由视觉同学给出
  2. 下载到本地 assets/img 文件夹, 重命名为 index.png
  3. 页面上使用 <img src="assets/img/index.png" />,或者直接引用一个远程的图片地址
  4. 使用 css 控制 img 标签的样式

还未曾长大,有人就说了,孩子,你这样不行啊。页面中要是有很多 Icon 的话岂不请求中都是图片请求。听大人一说,当初的自己吓得抓紧百度了一下。有一篇文章《Best Practices for Speeding Up Your Web Site》,也就是现在大家熟知的雅虎前端性能优化准则。第一条:

Minimize HTTP Requests (减少/最小化 http 请求数)

显然因为Icon的展示,就导致了页面中那么多的 http 请求,有点不合情理,大人说的很对啊。

于是乎,又打开了百度,怎么优雅的使用的icon呢? css sprite ,这是个什么鬼?

CSS Sprites are a means of combining multiple images into a single image file for use on a website, to help with performance.
( CSS Sprites 是为优化性能而将多个图片合并到一个图片在网站中使用的方式。)

有了 css sprite, 前端在使用时,只请求一次图片,通过 css 的 background-imgbackground-position 属性控制显示 icon。很显然,减少了 http 的请求次数,终于又可以开开心心的玩耍了。

然而还没开心多久,蛋疼的事情来了。项目中其他功能需要添加额外的 icon,UI 只能是重新再搞图,如果不会影响之前 icon的位置还好,若是调整了之前icon 的位置,以前写的css又得无奈的改动。

如果每次这样的修改都让 UI 给调整,估计 UI 早早的就疯了。还好社区内很快出现了相应的工具来自动化的完成这些工作。Sprity 一个根据相应配置自动合成 sprite 图的工具,在当时也提供了基于Gulp/Grant 的插件。很显然这种技术在目前已经不怎么实用了,Sprity github 上的项目最近一次提交也已经是2年前的事情了。《CSS Sprites: What They Are, Why They’re Cool, and How To Use Them》 这篇文章中给出了在当时如何使用工具自动生成 sprite 图片的方法。

在当时为了优化性能,还有一种技术 Data URIs,它可以将图片编码后内联于样式表中,避免了额外的 http 请求,同时还能避免配置 background-position。为优化性能引入的这种技术,可能还存在一定的性能问题,具体可以查看这篇文章给出的论述: 《Data URIs》

Icon Font

Web Font 的发展得益于 CSS3 的@font-face属性。

允许网页开发者为其网页指定在线字体。 通过这种作者自备字体的方式,@font-face 可以消除对用户电脑字体的依赖。

Icon Font 的思想来自于 Web Font,使用字体的方式设计 Icon。其中阿里巴巴开源的图标库 IconFont 应用广泛。下面以 Iconfont 图标库为例,介绍其使用方法。

登录 IconFont 后可以搜索自己想要的图标并添加至购物车,购物车中的图标可以添加至项目。Iconfont 提供了以项目进行管理图标的功能。项目中的 web 端图标使用方式有三种:Unicode, Font Class, Symbol。

Unicode引用

unicode 是字体在网页端最原始的应用方式,特点是:

  • 兼容性最好,支持 ie6+,及所有现代浏览器。
  • 支持按字体的方式去动态调整图标大小,颜色等等。
  • 但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去色。

使用步骤:

第一步:拷贝项目下面生成的 font-face

@font-face {font-family: 'iconfont';
    src: url('iconfont.eot');
    src: url('iconfont.eot?#iefix') format('embedded-opentype'),
    url('iconfont.woff') format('woff'),
    url('iconfont.ttf') format('truetype'),
    url('iconfont.svg#iconfont') format('svg');
}

第二步:定义使用 iconfont 的样式

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

第三步:挑选相应图标并获取字体编码,应用于页面

<i class="iconfont">3</i>

font-class引用

font-class 是 unicode 使用方式的一种变种,主要是解决 unicode 书写不直观,语意不明确的问题。

与 unicode 使用方式相比,具有如下特点:

  • 兼容性良好,支持 ie8+,及所有现代浏览器。
  • 相比于 unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。
  • 因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 unicode 引用。
  • 不过因为本质上还是使用的字体,所以多色图标还是不支持的。

使用步骤如下:

第一步:拷贝项目下面生成的 fontclass 代码:

//at.alicdn.com/t/font_8d5l8fzk5b87iudi.css

第二步:挑选相应图标并获取类名,应用于页面

<i class="iconfont icon-xxx"></i>

symbol 引用

这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。 这种用法其实是做了一个svg的集合,与上面两种相比具有如下特点:

  • 支持多色图标了,不再受单色限制。
  • 通过一些技巧,支持像字体那样,通过 font-size,color 来调整样式。
  • 兼容性较差,支持 ie9+,及现代浏览器。
  • 浏览器渲染 svg 的性能一般,还不如 png。

使用步骤如下:

第一步:拷贝项目下面生成的 symbol 代码:

//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js

第二步:加入通用 css 代码(引入一次就行):

<style type="text/css">
    .icon {
       width: 1em; height: 1em;
       vertical-align: -0.15em;
       fill: currentColor;
       overflow: hidden;
    }
</style>

第三步:挑选相应图标并获取类名,应用于页面:

<svg class="icon" aria-hidden="true">
    <use xlink:href="#icon-xxx"></use>
</svg>

Svg Icon

因为 IconFont 在显示图标方面的缺陷,开发者开始使用 SVG 作为其替代方案展示 Icon。

其中 Inline SVG vs Icon Fonts 一文中给出了详细的 Inline Svg 与 Icon Fonts之间的区别。

Iconfont主要的缺陷:

  1. 浏览器将其视为文字进行抗锯齿优化,不同系统下对文字的渲染显示效果可能不同

  2. Icon 作为字体进行显示时,其显示的大小、位置都可能会受到 font-size,line-height, word-spacing 等css属性影响,其容器的css样式也会可能影响到该字体icon的位置等。

  3. Iconfont 仅仅支持单色,且高分辨率下的显示效果不佳。

下面列举了项目中使用 SVG 的几种方式,各有优缺点:

Img/object 标签

早期使用 svg 的一种方式。缺点在于每个图标都需要保存成一个 svg 文件,使用时单独请求。项目中图标过多的化会带来过多的 http 请求。

Inline svg

顾名思义,将 svg 直接写进 html,这种方法简单暴力,可以减少 http 的请求。

优点: 可以直接使用 class 进行 svg 的样式定制,可控性强

缺点: 复用性差,效率低

Data URIs

css 中直接使用 base64 编码后的 svg

.icon{ 
  background: url(data:text/svg+xml;base64,<base64 encoded data>)
}

优点: 不需要额外引用 SVG 文件

缺点:可控性差,无法使用 css 进行样式定制,可能会存在潜在的效率问题

svg sprite

初期最基础的 svg sprite 技术类似于 css sprite,通过
background-position 等属性控制其显示的位置。类似于css sprite, 社区中也出现了对应的工具和在线网站提供生成 svg sprite 的方法。

优点: 减少 http 请求,可以 fallback 到 css sprite

缺点: 可控性差,无法方便的通过css进行控制样式

基于svg symbols 的svg sprite

svg symbols 是定义 svg 引用的一种方式,基于该方式下的 svg sprite 是在传统 svg sprite 上的改进,改进了在 sprite 中获取单一 Icon 的调用方式,之前是根据位置,基于 svg symbol 下的调用是根据引用。

<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">

    <symbol id="circle-cross" viewBox="0 0 32 32">
      <title>circle-cross icon</title>
      <path d="M16 1.333q2.99 0 5.703 1.161t4.677 3.125 3.125 4.677 1.161 5.703-1.161 5.703-3.125 4.677-4.677 3.125-5.703 1.161-5.703-1.161-4.677-3.125-3.125-4.677-1.161-5.703 1.161-5.703 3.125-4.677 4.677-3.125 5.703-1.161zm0 2.667q-2.438 0-4.661.953t-3.828 2.557-2.557 3.828-.953 4.661.953 4.661 2.557 3.828 3.828 2.557 4.661.953 4.661-.953 3.828-2.557 2.557-3.828.953-4.661-.953-4.661-2.557-3.828-3.828-2.557-4.661-.953zm3.771 6.885q.552 0 .948.391t.396.943-.396.948l-2.833 2.833 2.833 2.823q.396.396.396.938 0 .552-.396.943t-.948.391-.938-.385l-2.833-2.823-2.823 2.823q-.385.385-.948.385-.552 0-.943-.385t-.391-.938q0-.563.385-.948l2.833-2.823-2.833-2.833q-.385-.385-.385-.938t.391-.948.943-.396.948.396l2.823 2.833 2.833-2.833q.396-.396.938-.396z"/>
    </symbol>

    <symbol id="circle-check" viewBox="0 0 32 32">
      <title>circle-check icon</title>
      <path d="M16 1.333q2.99 0 5.703 1.161t4.677 3.125 3.125 4.677 1.161 5.703-1.161 5.703-3.125 4.677-4.677 3.125-5.703 1.161-5.703-1.161-4.677-3.125-3.125-4.677-1.161-5.703 1.161-5.703 3.125-4.677 4.677-3.125 5.703-1.161zm0 2.667q-2.438 0-4.661.953t-3.828 2.557-2.557 3.828-.953 4.661.953 4.661 2.557 3.828 3.828 2.557 4.661.953 4.661-.953 3.828-2.557 2.557-3.828.953-4.661-.953-4.661-2.557-3.828-3.828-2.557-4.661-.953zm4.49 7.99q.552 0 .943.391t.391.943-.396.948l-5.656 5.656q-.385.385-.938.385-.563 0-.948-.385l-2.833-2.823q-.385-.385-.385-.948 0-.552.391-.943t.943-.391.948.396l1.885 1.885 4.708-4.719q.396-.396.948-.396z"/>
    </symbol>

    <!-- .... -->
</svg>

每个Symbol设置一个id作为其引用的名字。

使用方法:

第一步: 将上述 svg 作为 body 的第一个子元素插入。

第二步: 在需要引用 icon 的地方通过 use xlink:href 的方式使用 svg

<svg class="icon">
  <use xlink:href="#circle-cross"></use>
</svg>

上述基于 Symbol 制作 svg-sprite 的方式,使用起来方便,通过使用 id 引用对应的svg,避免了使用background-position进行 svg 的引用。即使后期需要重新合并新的 svg-sprite,只需要合并前后对应svg的symbol id 不发生变化,合并前后业务中已经使用的 svg 就不用做任何变化。

webpack集成svg-sprite

vue-svg-icon 这个 demo 中详细给出了基于 webpack 的 vue 单页面项目中如何集成 svg-icon 方案的步骤。

第一步:制作svg-sprite:

webpack 中添加 svg-sprite-loader, 并添加如下配置:


{
  test: /\.svg$/,
  include: [resolve('src/components/svg-icon/icons')],
  use: [
    {
      loader: 'svg-sprite-loader',
      options: {
        symbolId: 'icon-[name]'
      }
    }
  ]
},

svg-sprite-loader 将我们引用的指定文件夹下的 svg 制作成 svg sprite 并插入 html 的 body 中。

需要注意,此处我们应指定文件夹存放我们项目中所有 svg icon,vue-cli 中还提供了 url-loader 处理 svg,因此我们应添加如下配置,避免 icon 下的 svg 文件被 url-loader 处理。

{
    test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
    loader: 'url-loader',
    exclude: [resolve('src/components/svg-icon/icons')], // 默认不处理该文件夹的命中的文件
    options: {
        limit: 10000,
        name: utils.assetsPath('img/[name].[hash:7].[ext]')
    }
},

通过 exclude 的配置可以避免指定icon文件夹下的svg文件被url-loader处理。

第二步: 封装使用时的 svg component

上面给出了在生成 svg sprite 后,通过 use 使用 svg 的方法。为方便项目中引用,封装 SvgIcon.vue 组件。

<!-- svg-icon 组件,业务组件中直接使用该组件展示 icon -->
<template>
  <svg :class="svgClass" aria-hidden="true">
    <use :xlink:href="iconName"></use>
  </svg>
</template>

<script>

// 引入所有的svg的文件
const requireAll = requireContext => requireContext.keys().map(requireContext);
const req = require.context('./icons', false, /\.svg$/);
requireAll(req);

export default {
  name: 'svg-icon',
  props: {
    iconClass: {
      type: String,
      required: true,
    },
    className: {
      type: String,
    },
  },
  computed: {
    iconName() {
      return `#icon-${this.iconClass}`;
    },
    svgClass() {
      if (this.className) {
        return `svg-icon ${this.className}`;
      }
      return 'svg-icon';
    },
  },
};
</script>

<style scoped>
.svg-icon {
  width: 40px;
  height: 40px;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}
</style>

使用svg sprite可能会遇到的问题

svg文件精简的问题

为什么要进行 svg 文件精简?

UI同学通过工具导出的SVG文件通常包含大量冗余且无用的信息,如编辑器元数据,注释,隐藏元素,默认值以及其他可以删除且不影响svg正常渲染的内容。

类似iconmoon.ioIconfont 都提供了在线精简svg的功能。

在多人协作以及需要频繁改动svg文件的中大型项目中,显然这种依赖手动流程去精简svg的方法已经无法满足快速开发的需要。因此我们需要在我们的工作流中集成类似的精简svg的工具。

svgo(svg optimizer) 是一个基于Nodejs的svg文件优化工具,其通过一系列的配置项可以实现定制化的精简svg的需求。

svgo-loader 基于webpack以及svgo的用于优化svg的loader。

使用方式:

webpack.base.conf.js

// 引入svgo的配置文件
const svgoConfig = require('../config/svgo-config.json');

...

{
    test: /\.svg$/,
    include: [resolve('src/components/svg-icon/icons')],
    use: [
      {
        loader: 'svg-sprite-loader',
        options: {
          symbolId: 'icon-[name]'
        }
      },
      {
        loader: 'svgo-loader',
        options: svgoConfig,
      }
    ]
},

svgo-config.json(定义了精简svg的规则)

{
  "plugins": [
    { "cleanupAttrs": true },
    { "cleanupEnableBackground": true },
    { "cleanupIDs": true },
    { "cleanupListOfValues": true },
    { "cleanupNumericValues": true },
    { "collapseGroups": true },
    { "convertColors": true },
    { "convertPathData": true },
    { "convertShapeToPath": true },
    { "convertStyleToAttrs": true },
    { "convertTransform": true },
    { "mergePaths": true },
    { "removeComments": true },
    { "removeDesc": true },
    { "removeDimensions": true },
    { "removeDoctype": true },
    { "removeEditorsNSData": true },
    { "removeEmptyAttrs": true },
    { "removeEmptyContainers": true },
    { "removeEmptyText": true },
    { "removeHiddenElems": true },
    { "removeMetadata": true },
    { "removeNonInheritableGroupAttrs": true },
    { "removeRasterImages": true },
    { "removeTitle": true },
    { "removeUnknownsAndDefaults": true },
    { "removeUselessDefs": true },
    { "removeUnusedNS": true },
    { "removeUselessStrokeAndFill": true },
    {
      "removeAttrs": { "attrs": "fill"} //移除fill属性
    },
    { "removeXMLProcInst": true },
    { "removeStyleElement": true },
    { "removeUnknownsAndDefaults": true},
    { "sortAttrs": true }
  ]
}

状态相关的svg-icon以及多色问题的思考

在 web 开发的过程中,我们经常会遇到一些状态相关的 icon。比如,当用户 click 或者 hover 时,我们需要对 icon 的颜色进行相应的变化。采用 Iconfont 方案时,因为其字体的本质,我们可以直接对字体的颜色使用 css 进行控制。

svg-icon 方案的使用过程中 svg 的颜色是一种填充色机制。通过fill属性将具体的路径进行颜色填充。如相关路径未指定fill属性,则其继承父元素的color属性进行填充。

因此 svg-icon 方案下状态颜色的变化分为以下两种情况:

  1. 多色 svg-icon 的状态变化非单一 path 的颜色变化,状态变化时,直接替换不同状态下的 icon
  2. 多色 svg-icon 的状态变化仅仅涉及单一 path 下的颜色变化,此时可以通过该 path 的 fill 属性留空,通过css修改其父元素的 color 属性,达到修改 icon 颜色的目的。

总结

基于 svg-sprite 的svg-icon 方案在 14年的时候就已经出现,鉴于当时浏览器兼容性等原因,并没有得到大规模采用。如今随着技术的更新,兼容性显然已经不在是svg-icon应用的阻碍。移动端android 3.x, IE 9+ 都可以进行采用 svg-icon 的 icon 方案。通过调研,发现目前已经应用了 svg-icon 技术方案的有:

  • Github 全站采用了svg作为其icon展示方案, inline svg 的使用方式
  • iconfont 主站采用了inine svg 的方式,展示其所有的icon
  • 京东 部分icon (话费、机票等)采用了基于svg symbol的svg sprite 方案,展示icon
  • 腾讯视频 部分icon(暂停、播放)采用了基于svg symbol的svg sprite 方案

任何技术方案的讨论都脱离不了其应用场景。在实际的开发中,因为各种原因,可能会存在多种icon方案并行的情况,因此在实际的开发过程中应具体问题具体分析。


参考链接: