# svg 图标的组件化使用

正常情况下,svg 图标的体验式使用大概率是直接放在 dom 结构中即可。体验很棒,但是如果项目中真的这么使用,当一个图标在多处使用时,那源码结构会显得很臃肿。毕竟一个 svg 的路径信息大概时这样:

# Svg Sprite

看到 sprite 有没有很眼熟?没错,有点像“远古”开发时使用的 css雪碧图(css sprite)。

Svg Sprite 的使用有两大标志:symbol 标签use 标签

# symbol 标签

symbol 标签 可以理解成 Svg Sprite 的组件,symbol 元素将 svg 的路径信息进行包裹,并为每一个 symbol 元素设置唯一的 id。然后将这些 symbol 元素统一放在一个 svg 标签中进行包裹管理。

<svg>
  <symbol id="symbol标签唯一id" viewbox="xxx">
    <!-- svg 图标路径信息 -->
  </symbol>
  <symbol id="symbol标签唯一id" viewbox="xxx">
    <!-- svg 图标路径信息 -->
  </symbol>
  <symbol id="symbol标签唯一id" viewbox="xxx">
    <!-- svg 图标路径信息 -->
  </symbol>
</svg>

# use 标签

use 标签 负责调用需要使用的 svg 图标。在需要使用相关图标的地方使用 svg 包裹 use 标签,然后为 use 标签设置 symbol 的 id 属性即可,并且可以在多处重复调用

比如可以直接在 .html 文件的body中这样:

<svg>
  <symbol id="user">
    <path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/>
  </symbol>

  <use xlink:href="#user" x="10" y="50" />
  <use xlink:href="#user" x="130" y="50" />
</svg>

打开html文件,页面上就会有两个 user 图标,如下:

更牛掰的是,use 标签还可以跨 svg 元素进行调用,形式类似于这样:

<!-- 有一个地方定义了 svg 的信息 -->
<svg>
  <symbol id="symbol-1">
    <!-- svg 图标路径信息1 -->
  </symbol>
  <symbol id="symbol-2">
    <!-- svg 图标路径信息2 -->
  </symbol>
</svg>

<!-- 有一个地方想使用定义的svg -->
<svg>
  <use xlink:href="#symbol-X"></use>
</svg>

上述的使用方式,只要包裹 symbol 标签的 svg 被放在 body 中,在文档中的任何地方都可以通过 use 标签调用对应的 symbol 唯一 id 标识进行图标使用

# Svg 图标组件化流程

经过上述对 svg 图标使用的了解,接下来就可以开始进行 vue 组件化封装的处理了。

先来梳理一下有哪些工作要做?

  1. svg 图标封装的组件使用形式是什么样子?需要的 props 参数有哪些?
  2. svg 图标如何存放?如何引入到开发环境中?
  3. 我们如何将引入的 svg 图标一一进行 symbol 标签的包裹并给予唯一的id,最后再统一放在一个 svg 标签中放入body下?

# 组件的形式与参数

  <svg-icon icon="XXX" clasName="XXX" />
Props 名称 类型 必填 备注
icon String svg 图标symbol id
className String 调整图标样式的class类名

# svg-icon.vue 组件

<template>
  <svg class="svg-icon" :class="className" aria-hidden="true">
    <use :xlink:href="iconName" />
  </svg>
</template>

<script setup>
import { defineProps, computed } from 'vue'

const props = defineProps({
  // icon 图标
  icon: {
    type: String,
    required: true
  },
  // 图标类名
  className: {
    type: String,
    default: ''
  }
})

// 图标名称经过 svg-sprite-loader 处理后统一名称形式为 icon-文件名
const iconName = computed(() => `#icon-${props.icon}`)
</script>

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

.svg-external-icon {
  background-color: currentColor;
  mask-size: cover !important;
  display: inline-block;
}
</style>

# svg 图标统一引入

将从设计师哪里得来的 svg 图标放在项目中,一般我们会默认放在 vue-cli 项目中约定的静态资源文件夹 assets 中,假设当前是在 src/assets/icons/svg 中。

我们需要做的是将 svg 文件统一引入到项目的上下文环境中,而 webpack 的 require.context API 恰好提供了该功能。

在 svg 文件同级的目录中创建 index.js 做 svg 图标的统一引入,src/assets/icons 目录下的文件结构如下:

.
├── index.js # 这里做 svg 文件的统一引入
└── svg # svg 图标文件夹
    ├── eye-open.svg
    ├── eye.svg
    └── user.svg
    └── ...(更多svg)

# icons/index.js

import SvgIcon from '@/components/svg-icon.vue'

// https://webpack.docschina.org/guides/dependency-management/#requirecontext
// 通过 require.context() 函数来创建自己的 context
const svgRequire = require.context('./svg', false, /\.svg$/)

// 此时返回一个 require 的函数,可以接受一个 request 的参数,用于 require 的导入。
// 该函数提供了三个属性,可以通过 require.keys() 获取到所有的 svg 图标
// 遍历图标,把图标作为 request 传入到 require 导入函数中,完成本地 svg 图标的导入
svgRequire.keys().forEach((path) => svgRequire(path))

export default (app) => {
  app.component('svg-icon', SvgIcon)
}

icons/index.js 其实做了两件事:

  1. 通过 require.context() 统一将 svg 图标模块的导入到本地,请参考 require.context函数
  2. 通过 app.component() 将先前的 svg-icon 组件注册成了全局组件

# main.js

在 项目入口文件 main.js 中按照 element-plus 引入的方式,导入 svg icon 的模块引用。

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

// vue add element-plus 自动添加
import installElementPlus from './plugins/element'

// 导入 svgIcon
import installIcons from '@/assets/icons'

const app = createApp(App)

// vue add element-plus 自动添加
installElementPlus(app)

// 添加 SvgIcon
installIcons(app)

app.use(store).use(router).mount('#app')

# svg-sprite-loader 的整合

上一步通过 webpack 的 require.context() 方法只是将 svg 图标导入到了本地。但针对 .svg 结尾的文件还需要做进一步的webpack配置处理。

svg-sprite-loader 的作用就是实现工作中的第三步,它会将统一引入的 svg 使用 symbol 标签包裹,并赋予唯一id。然后统一整合到 svg 标签中,同时把最大的 svg 标签放入到页面的 body 中供后续通过 use 标签使用。

svg-sprite-loader 的处理需要调整 webpack 配置。

注意:如果没有对特别指定 symbol 的 id 值,图标默认就是文件名称

以 vue3 为例,对 vue3 的webpack 配置要放在根目录中的 vue.config.js 文件中。下面的配置可以说是一个通用的配置文案,做一个ctrl + cctrl + v 程序员就好。

const path = require('path')

function resolve(dir) {
  return path.resolve(__dirname, dir)
}

module.exports = {
  chainWebpack(config) {
    config.module
      .rule('svg') // 获取到原来针对svg的规则
      .exclude.add(resolve('src/assets/icons/svg')) // 将改规则排除 src/assets/icons 目录下的svg文件
      .end() // 结束当前规则的调整

    config.module
      .rule('icons') // 重新配置一个新的规则名字 icons
      .test(/\.svg$/) // 匹配以 .svg 为结尾的图标
      .include.add(resolve('src/assets/icons/svg')) // 针对的目录是 src/assets/icons/svg
      .end() // 结束新规则配置
      .use('svg-sprite-loader') // 针对以上规则使用 loader
      .loader('svg-sprite-loader') // 加载 loader
      .options({
        symbolId: 'icon-[name]' // 唯一id标识符配置,这个 icon-xxx 的形式,是在组件注册时针对 svg 图标配置的
      })
      .end()
  }
}

最后我们可以在页面中看到如下情况,在 vue 编译的dom节点 #app 外部,放入了一个 svg 包裹的所有 svg 图标的整合块,每个 svg 图标被 symbol 标签包裹,唯一 id 正式我们设置的 #icon-文件名 的形式。