如何引入 icon

1,263 阅读5分钟

我们经常会用到各种各样的小图标,也有各种奇技淫巧可以用来引入图标,这里将简单介绍几种我们常用的图标引入方式,里面有古代的,也有现代的。

雪碧图

雪碧图可以说是一种比较古老的引入图片的方式了,这里只是介绍一下他的原理。

在当初没有比较方便的图标管理方式的时候,我们可能需要使用 CSS 或者 JS 来一个一个引入诸如 png 格式的图标,同时如果一个图标多次引用,可能需要多次请求,导致性能不太好。

于是就出现了一种解决方案,他将所有的小图标放到一个图片里面,通过 background-image 引入图片,通过 background-position 来控制是显示的哪一个图标,这样实际上所有的图标都是使用的同一张图片了。

但是有个很大的问题是管理和维护起来比较麻烦,因为你不知道引入的到底是谁,后续增加图标难度也比较大,因此现在用的人已经比较少了。

iconfont

iconfont 是一个不错的图标管理方案,有多种使用场景:

  1. 我们想要找一个图标:
    1. 输入关键词
    2. 加入购物车
    3. 在购物车中结算(加入项目)
    4. 在项目中查看,iconfont 提供了 Unicode、Font class、Symbol 三种不同的引入方案,并且附有教程
  2. 我想管理我的图标:
    1. 上传 SVG
    2. 像之前那样用 :)

好了,因为 iconfont 太方便了,所以话题结束。

…………

当然不是!如果没有 iconfont,我们该如何引入图标呢?或者说 iconfont 的 symbol 是怎么引入的呢?且看下文。

SVG

SVG 是什么?

看来我们还得从 SVG 说起。

可缩放矢量图形(Scalable Vector Graphics,SVG),是一种用于描述基于二维的矢量图形的,基于 XML 的标记语言。本质上,SVG 相对于图像,就好比 HTML 相对于文本。

这样看来,SVG 实际上是比较复杂复杂的,现在我们只讨论其中一部分对我们有用的东西,先来看看一个 SVG 图标的代码:

<svg t="1581476689547" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2005" width="200" height="200">
  <path d="M466.88 908.96L113.824 563.296a270.08 270.08 0 0 1 0-387.392c108.8-106.56 284.896-106.56 393.696 0 1.504 1.472 2.976 2.944 4.448 4.48 1.472-1.536 2.944-3.008 4.448-4.48 108.8-106.56 284.896-106.56 393.696 0a269.952 269.952 0 0 1 34.016 347.072l-387.392 385.6a64 64 0 0 1-89.92 0.384z" p-id="2006"/>
</svg>

一个 <svg> 标签,包裹着一个 <path> 标签,其中 <path> 就定义了图片的形状。

我们可以像普通图片一样使用 svg

<img src="./src/heart.svg">

SVG 雪碧图

使用 <symbol>,我们可以轻松构建 SVG 雪碧图,就像这样:

<svg>
  <symbol id="icon1" viewBox="0 0 1024 1024">
    <path/>
  </symbol>
  <symbol id="icon2" viewBox="0 0 1024 1024">
    <path/>
  </symbol>
  <symbol id="icon3" viewBox="0 0 1024 1024">
    <path/>
  </symbol>
</svg>

每一个 <symbol> 里面就装了一个图标。现在就不能像之前那样直接用 <img> 标签来使用啦,我们可以先在 HTML 中某处引入整个 SVG 图片,再通过 <use> 来使用。

<svg class="icon">
  <use xlink:href="#icon1"/>
</svg>

这跟之前提到的雪碧图有点像,所有的图标其实都在一个 SVG 源上,主要的优点在于:

  1. 跟雪碧图一样,不需要多次加载
  2. 代码简洁方便维护
  3. 尺寸可以任意拉伸
  4. 可以通过 fill 或者父元素的 color 属性很方便地调整颜色

最大的缺点是兼容性,IE 9 以下没办法很好地使用,但现在这当然不是问题啦。

引入一个 icon

但是我们总不可能每次都自己搞一个 SVG,然后每来一个新图标就装到 symbol 里面吧,然后对于向 Vue 这样的框架我们由该怎么引入呢?或者说我们如何在 JS 而不是 HTML 中引入呢?

我们可以试一下直接在 JS 中 import 一个 SVG,发现他将会得到一个路径,而不是我们想要的图片,也没办法直接在 template 或者 JSX 里面用。

为了解决这个问题,我们可以使用 svg-sprite-loader

如果是使用的 webpack,我们需要按照文档里面提供的方法进行配置 webpack.config.js

const SpriteLoaderPlugin = require('svg-sprite-loader/plugin')
module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        exclude: path.resolve(__dirname, 'src/assets/icons')
      },
      {
        test: /\.svg$/,
        loader: 'svg-sprite-loader',
        include: path.resolve(__dirname, 'src/assets/icons'),
        options: { extract: false }
      }
    ]
  },
  plugins: [
    new SpriteLoaderPlugin({ plainSprite: true })
  ]
}

如果是使用的 vue-cli,我们需要这样配置 vue.config.js

module.exports = {
  chainWebpack: config => {
    config.module
      .rule('svg-sprite')
      .test(/\.svg$/)
      .include.add(path.resolve(__dirname, 'src/assets/icons')).end()
      .use('svg-sprite-loader').loader('svg-sprite-loader').options({ extract: false }).end()
    config.plugin('svg-sprite')
      .use(require('svg-sprite-loader/plugin'), [{ plainSprite: true }])
    config.module
      .rule('svg')
      .exclude.add(path.resolve(__dirname, 'src/assets/icons')).end()
  }
}

弄好 svg-sprite-loader 之后再在 JS 中 import SVG,就会发现 HTML 中多出来了一个 <svg> 标签,里面是一些 <symbol>,这意味着我们就可以通过 <use> 来使用他们啦!

引入一整个 icon 目录

我们可以通过 webpack 的 require.context 来引入一整个文件夹,他接受三个参数:第一个是文件夹,第二个是是否使用子文件,第三个是匹配文件的正则。

let importAll = (requireContext) => requireContext.keys().forEach(requireContext)
try {
  importAll(require.context('./assets/icons', true, /\.svg$/))
} catch (error) {
  console.log(error)
}

封装一个 Icon 组件

这里使用 Vue 做代码演示,先写他的 template,非常简单,就是一个 <svg> 里面有一个 <use>:xlink:href 的值就是对应 <symbol>id

<template>
  <svg class="icon" @click="$emit('click',$event)">
    <use :xlink:href="'#'+name"/>
  </svg>
</template>

然后是中间的 JS,接受一个 Prop 即可:

  let importAll = (requireContext) => requireContext.keys().forEach(requireContext);
  try {
    importAll(require.context('../assets/icons', true, /\.svg$/));
  } catch (error) {
    console.log(error);
  }
  export default {
    name: 'Icon',
    props: ['name']
  };

最后是从 iconfont 里面抄的 icon 的基本样式:

  .icon {
    width: 1em; height: 1em;
    vertical-align: -0.15em;
    fill: currentColor;
    overflow: hidden;
  }

这样以后我们就可以通过 Icon 组件来使用图标了,如果需要新加图标,下好之后就放在图标文件夹里面就可以了。

<Icon name="heart"/>

小坑

有时候咱们下载下来的 SVG 里面是自带了颜色(fill 属性)的,这样我们之后就没办法修改颜色了,这个时候可以使用 svgo-loader 来统一去除颜色。

webpack.config.js 中:

const SpriteLoaderPlugin = require('svg-sprite-loader/plugin')
module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        exclude: path.resolve(__dirname, 'src/assets/icons')
      },
      {
        test: /\.svg$/,
        use: [
          { loader: 'svg-sprite-loader', options: { extract: false } },
          {
            loader: 'svgo-loader',
            options: { plugins:[{removeAttrs: {attrs: 'fill'}}] }
          }
        ]
        include: path.resolve(__dirname, 'src/assets/icons'),
      }
    ]
  },
  plugins: [
    new SpriteLoaderPlugin({ plainSprite: true })
  ]
}

vue.config.js 中:

module.exports = {
  chainWebpack: config => {
    config.module
      .rule('svg-sprite')
      .test(/\.svg$/)
      .include.add(path.resolve(__dirname, 'src/assets/icons')).end()
      .use('svg-sprite-loader').loader('svg-sprite-loader').options({ extract: false }).end()
      .use('svgo-loader').loader('svgo-loader')
      .tap(options => ({ ...options, plugins: [{ removeAttrs: {attrs: 'fill'} }] }))
    config.plugin('svg-sprite')
      .use(require('svg-sprite-loader/plugin'), [{ plainSprite: true }])
    config.module
      .rule('svg')
      .exclude.add(path.resolve(__dirname, 'src/assets/icons')).end()
  }
}