最近看项目视频的时候对里面使用 svg 的方式感到很好奇,于是去网上查了一下,发现 svg 竟然也有类似于 css 雪碧图一样的用法,也就是 svg-sprite(孤陋寡闻了),而且配合插件后能够以组件化的方式使用 svg,非常方便。这里记录下一些相关用法。

视频里的做法是注册这样的一个全局组件:

// icon.vue
<svg>
	<defs>
        <symbol id="icon1">
        	<path></path>
        </symbol>
        <symbol id="icon2">
        	<path></path>
        </symbol>
        ......
    </defs>
</svg>

css 雪碧图中是把多个背景图片放在一张大的图片中,而 svg 雪碧图则是把多个 symbol 放在一个大的 svg 中,每个 symbol 代表了一个图标,以后每次想要使用图标,只需要写这么一段代码即可:

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

但是这里有两个问题:

  1. 从图标库(比如阿里的 iconfont)下载下来的通常是 .svg 文件,如何根据多个单独的 .svg 文件生成 svg 雪碧图?
  2. 每次要使用图标都得写这么一段代码,并不是很方便,是否可以像使用组件那样使用图标?

这里的关键是使用 svg-sprite-loader 这个插件。

安装插件

首先 npm 安装:

npm i svg-sprite-loader --save

接着我们用一个文件夹专门放各种需要用到的 .svg 文件,这里以 src/assets/img/icons 为例,从 iconfont 下载 .svg 文件后放到这个文件夹即可。

修改配置

vue-cli3 默认会通过 file-loader.svg 文件进行处理,这里我们并不想让它处理我们的 .svg 图标文件,但是有的 .svg 文件又确确实实需要用它处理(总不可能所有的 svg 文件都用来做图标吧),所以我们要排除掉 file-loadersrc/assets/img/icons 这个文件夹的处理。在 vue.config.jsmodule.exports 中新增:

module.exports = {
  chainWebpack(config){
    config.module
        //排除对于 icons 目录中 svg 文件的处理
        .rule('svg')
   			.exclude
        		.add('/src/assets/img/icons')
		        .end()
  }
}

接着,指定 svg-sprite-loader 对图标文件夹里面的 .svg 文件进行处理:

module.exports = {
  chainWebpack(config){
    config.module
        //排除对于 icons 目录中 svg 文件的处理
        .rule('svg')
   			.exclude
        		.add('/src/assets/img/icons')
	    	    .end()
      		.end()
        //设置 svg-sprite-loader 处理 icons 目录中的 svg
        .rule('icons')
            .test(/\.svg$/)
            .include
        		.add(resolve('src/assets/img/icons'))
            	.end()
            .use('svg-sprite-loader')
            	.loader("svg-sprite-loader")
            	.options({symbolId:'icon-[name]'})
  }
}

这样其实已经可以生成 svg 雪碧图了,之后这个雪碧图会作为 svg 元素注入到 html 中:

接下来封装图标组件。

封装图标组件

components/common/icon 下新建一个 SvgIcon.vue 文件:

<template>
  <svg :fill="iconColor" class="svg-icon">
    <use :xlink:href="iconNameCp" />
  </svg>
</template>
<script>
export default {
  name: 'svgIcon',
  props: {
    // 图标类型
    iconName: {
      type: String,
      required: true
    },
    // 图标颜色
    iconColor:{
      type:String,
      default:'#666'
    }
  },
  computed: {
    iconNameCp() {
      return `#icon-${this.iconName}`
    }
  } 
}
</script>

使用组件的时候可以通过传值 iconNameiconColor 确定图标的类型和颜色。

全局注册组件

因为可能很多地方都会用到图标,这里选择全局注册 SvgIcon.vue 组件。在 src/assets/img/icons 文件夹下新建 index.js 文件:

// 全局引入 svgIcon 组件
import Vue from 'vue'
import SvgIcon from '@/components/common/icon/SvgIcon.vue'
Vue.component('svg-icon', SvgIcon)

// 让 icons/svg下面的图片自动导入,而不是每次手动导入
const req = require.context('./svg', false, /\.svg$/);
req.keys().map(req);

之后,在 main.js 中引入 index.js 文件:

import 'assets/img/icons'

使用组件

以后每次要使用图标就非常方便了,只需要一行代码即可:

<svg-icon class="icon" :icon-name="xxxxx" :icon-color="xxxxx"></svg-icon>

样式修改

从 iconfont 下载下来的图标文件默认没有内联的 fill 属性,所以可以像上面那样直接为 svg 元素指定 fill 属性,fill 会继承给子元素;如果下载的时候选择了颜色,就会多出来内联的 fill 属性,此时需要显式指定子元素的 fill 继承自父元素(否则继承的权重很低,样式无法被应用):

svg path {
    fill:inherit
}

为什么这里不能写成下面这样呢?

.icon path {
    fill:inherit
}

这是因为 svg->use 里面会生成一个 shadow dom,这个 shadow dom 包含了 svg->path,它是无法通过 css 选择器拿到的,所以上面这个样式声明不会起效果。

当然还可以用 currentColor 修改图标颜色。因为在元素自身没有 color 属性的时候,它的 currentColor 会继承父元素的 color 属性,所以可以给 .icon 设置 color,并指定每一个 pathfill 属性都是 currentColor

.icon {
   color:#fff
}

svg path {
   fill:currentColor
}

补充

iconfont 本身可以根据添加的图标自动生成 js 代码,之后只需将 js 文件引入项目中即可,这种方式同样可以将 svg 注入到 html 中:

但是这种方式不利于代码的维护,不可能说每一次新增图标都到 iconfont 重新生成一遍代码,再重新引入到项目中,这样太麻烦了。所以才使用了 svg-sprite-loader 插件,这样每次新增图标,只需要下载图标并放到对应文件夹即可。