如何在 vue 项目中更优雅的使用 icon

1,383 阅读6分钟

背景

最近项目中需要添加大量的 icon,最开始是这么实现的。

<div class="item">
  <img src="@/assets/images/File.svg" />
  <p>File</p>
</div>

确实也能实现。

但是使用起来不是很方便,并且总觉得不够优雅,所以就整理了一下如何更好的在项目中使用 icon。

基础使用

unicode

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

  • 兼容性最好,支持 ie6+,以及所有的现代浏览器。
  • 支持按字体的方式去动态调整图标大小,颜色等,简单来说,看成一种字体就行。
  • 但是因为是字体,所以不支持多色,只能使用单色图标,就算项目里有多色图标也会自动去色。
  • 在不同浏览器下的表现不同,需要做各种兼容。

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

// 这几个 url 对应着不同的浏览器兼容
@font-face {font-family: 'iconfont';
    src: url('iconfont.eot');/* IE9*/
    src: url('iconfont.eot?#iefix') format('embedded-opentype'),/* IE6-IE8 */
    url('iconfont.woff') format('woff'),/* chrome、firefox */
    url('iconfont.ttf') format('truetype'),/* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
    url('iconfont.svg#iconfont') format('svg');/* iOS 4.1- */
}

css 是可以自定义字体的,所以这就相当于加载我们自己的字体。

当然因为兼容性问题,不同浏览器需要加载不同格式的字体,所以在 iconfont 导出的内容同时支持四种字体。

当然这里还有一个需要注意的点就是:

iconfont 平台给出的 font-face 定义默认都是 iconfont 的 font-family ,建议使用的时候改掉,避免与其他字体冲突。

关于如何生成 font-face,你可以在 iconfont 中生成在线链接,或者是下载到本地自行引用两种方式。

还有其他想了解的 font-face的内容,可以戳张鑫旭大佬的这篇《真正了解CSS3背景下的@font face规则》。

第二步:定义使用 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">&#x33;</i>

每个图标都有对应的 unicode,比如我们在web上输入 跟输入&#x6211;是一样的效果,浏览器会自动找到对应的图形去渲染。

这是上面的效果示例图:

#iefix 有什么作用呢?

IE9 之前的版本没有按照标准解析字体声明,当 src 属性包含多个 url 时,只会读取类似 src:url() 这样的格式,所以 IE 6-8 会把第一个引号到最后一个引号之间的内容都当做字体的 URL,结果就会返回一个 404错误,而其他浏览器会自动采用自己适用的 url。因此把仅 IE9 之前支持的 EOT 格式放在第一位,然后在 url 后加上 ?,这样 IE9 之前的版本会把问号之后的内容当作 url 的参数。至于 #iefix 的作用,一是起到了注释的作用,二是可以将 url 参数变为锚点,减少发送给服务器的字符。


为何有两个src?

同时细心的同学可以注意到,在 font-face中有两个 src。这是为什么呢?

绝大多数情况下,第一个 src 是可以去掉的,除非需要支持 IE9 下的兼容模式。在 IE9 中可以使用 IE7 和 IE8 的模式渲染页面,微软修改了在兼容模式下的 CSS 解析器,导致使用 ? 的方案失效。由于 CSS 解释?器是从下往上解析的,所以在上面添加一个不带问号的 src 属性便可以解决此问题。

简单来说,这两个问题都是为了兼容 IE 而产生的兜底方案。

font class

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

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

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

第一步:拷贝项目下面生成的 fontclass 代码(在 iconfont 中的生成方式可参考上面 unicode 方式):

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

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

<i class="iconfont icon-xxx"></i>
// css 
.icon-xxx:before {
    content: "\e626";
}

看到这大家应该都明白了,就是加了个伪元素,然后把字符图标放进去,使其更加语义化。

效果图:

symbol

这也是目前比较推荐的一种做法,与上面相比具有如下特点:

  • 支持多色图标了,不再受单色限制。
  • 通过一些技巧,支持像字体那样,通过 font-size, color 来调整样式。
  • 支持 ie9+, 及现代浏览器。

第一步:拷贝项目下面生成的 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 标签包裹着 use 标签,使用起来也不是很方便,我们可以封装一下。

symbol 进阶

封装组件

为了更方便 icon 的使用,我们可以将其封装成一个组件。

<template>
  <svg class="svg-icon" aria-hidden="true" @click="$emit('click')">
    <use :xlink:href="iconClass"></use>
  </svg>
</template>

<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';
@Component({
  name: 'SvgIcon',
})
export default class SvgIcon extends Vue {
  @Prop() readonly iconClass!: string;
  get iconName() {
    return `#icon-${this.iconClass}`;
  }
}
</script>

<style lang="scss" scoped>
.svg-icon {
  width: 1em;
  height: 1em;
  fill: currentColor;
  overflow: hidden;
}
</style>

svg-sprite-loader 和 svgo-loader

虽然方便了很多,但是这也有这样一些问题:

  1. 如果我需要修改或者添加 icon ,那我还得重新上传到平台上,再生成新的 js 去替换旧的 js;
  2. 生成的 iconfont.js 的 svg 代码很不直观;
  3. 不能按需加载。

这时候我们可以使用 svg-sprite-loader插件和svgo-loader插件,svg-sprite-loader用来打包 svg 图标,svgo-loader来精简我们的 svg 内容。

首先我们可以把所有的 icon 都以 svg 的形式都放在 src/assets/icon目录中。

然后再去 vue.config.js 中添加配置

chainWebpack: (config) => {
    // set svg-sprite-loader
    const svgPath = resolve('src/assets/icon');
    config.module.rule('svg').exclude.add(svgPath).end();
    config.module
      .rule('svg-icon')
      .test(/.svg$/)
      .include.add(svgPath)
      .end()
      .use('svg-sprite-loader')
      .loader('svg-sprite-loader')
      .options({
        symbolId: 'icon-[name]',
      })
      .end()
      // remove origin svg fill attr
      .use('svgo-loader')
      .loader('svgo-loader')
      .tap((options) => ({
        ...options,
        plugins: [{ name: 'removeAttrs', params: { attrs: 'fill' } }],// 删除svg中fill
      }))
      .end();
  },

这里有一点需要说明一下,删除了 svg 中的 fill 属性,就可以通过传入颜色可以修改 icon 的颜色,

但其实不建议,这样有些多色的图标也会被影响(如下图),当然你可以写个规则只处理单色的svg。

示例:会由变成

传入background-color: #303844之后,就直接变成了,所以如果真有这种情况,还需要单独处理下。

自动引入

自动引入 src/assets/icon中的 icon,需要用到 webpack 中的 require.context

// src/assets/icon/index.ts
import Vue from 'vue';
import SvgIcon from '@/components/svg-icon';

// register globally
Vue.component('svg-icon', SvgIcon);

const requireAll = (requireContext: any) =>
  requireContext.keys().map(requireContext);
const req = require.context('../icon', false, /.svg$/);
requireAll(req);

// 使用
<svg-icon iconClass="xxx" />

require.context 有三个参数:一个要搜索的目录,一个标记表示是否还搜索其子目录, 以及一个匹配文件的正则表达式。

我们可以把 require.context('../icon', false, /.svg$/)requireContext.keys()出来看一下,就能理解这个方法为什么能导入目录下的全部 svg 了。

// require.context('../icon', false, /.svg$/)
ƒ webpackContext(req) {
	var id = webpackContextResolve(req);
	return __webpack_require__(id);
}

// requireContext.keys(),这里的每一项是文件的名称
['./iconfont.svg', './iconfont1.svg', './iconfont2.svg']

就是遍历目录下的每个 svg 文件,然后在通过 __webpack_require__导入。

封装好了之后,不管之后是增删改,都会方便很多,只需要在 src/assets/icon进行操作,也便于后期的维护。当然并不是说 symbol 就是最好的方案,更多的还是需要结合自己的项目和业务场景,毕竟提高开发效率才是重点。

参考资料

juejin.cn/post/684490…

purplebamboo.github.io/2016/08/16/…

blog.csdn.net/weixin_3372…