在vue项目中优雅的使用svg, 使用 svg-sprite-loader 实现

2,495 阅读3分钟

使用 svg-sprite-loader 在vue项目中优雅的使用svg, 类似于elementUi 中使用icon一样简单, 支持 webpack 和 vite 两种构建工具

Svg Sprite 是什么

  • 将多个 svg 打包成 svg-sprite。svg 雪碧图。类似于 CSS 中的 Sprite 技术。图标图形整合在 一起,实际呈现的时候准确显示特定图标。

在 vue2 配合 webpack 中使用 当然使用 vue3 配合 webpack 也同样适用

  • 这里以 vue2 示例
  • webpack 配置基本一致, 区别 cli 版本不同写法不一致 这里以新版本示例

下载依赖

npm install svg-sprite-loader -D

svg-sprite-loader 配置

  • 在vue.config.js中配置
const path = require('path') // 导入模块

// 获取路径
function resolve (dir) {
  return path.join(__dirname, dir)
}

module.exports = defineConfig({
...,

     chainWebpack: config => {
        config.module
          .rule('svg')
          .exclude.add(resolve('src/icons')) // 这里是svg文件路径
          .end()
        config.module
          .rule('icons')
          .test(/\.svg$/)
          .include.add(resolve('src/icons'))
          .end()
          .use('svg-sprite-loader')
          .loader('svg-sprite-loader')
          .options({
            symbolId: 'icon-[name]'
          })
          .end()
      },
      
 ...,     

})

svgIcon插件

  • 在src下创建icons文件夹 (图标管理文件夹)
  • icons 下创建index.js (插件js文件)
import SvgIcon from '@/components/SvgIcon' // 封装的svgIon组件 (路径根据实际情况更改)


export default (vue) => {
  vue.component('svg-icon', SvgIcon)
  // 统一批量引入
  const req = require.context('./svg', false, /\.svg$/) // ./svg -> svg文件所在路径
  const requireAll = requireContext => requireContext.keys().map(requireContext)
  requireAll(req)
}

  • 在icons下创建 svg文件夹 管理所有的svg文件

image.png

封装 svg-icon 全局组件 (分为vue2/vue3)

  • components 下创建 SvgIcon 组件

vue2 版本

<template>
  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon"
       v-on="$listeners" > </div>
  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
    <use :xlink:href="iconName" />
  </svg>
</template>

<script>

export default {
  name: 'SvgIcon',
  props: {
    iconClass: {
      type: String,
      required: true
    },
    className: {
      type: String,
      default: ''
    }
  },
  computed: {
  // 判断是否是svg文件链接
    isExternal () {
      return  /^(https?:|mailto:|tel:)/.test(this.iconClass)
    },
    iconName () {
      return `#icon-${this.iconClass}`
    },
    svgClass () {
      if (this.className) {
        return 'svg-icon ' + this.className
      } else {
        return 'svg-icon'
      }
    },
    styleExternalIcon () {
      return {
        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
      }
    }
  }
}
</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>

vue3 版本

<template>
  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-bind="$attrs"></div>
  <svg v-else aria-hidden="true" class="svg-icon" v-bind="$attrs">
    <use :xlink:href="iconName" />
  </svg>
</template>

<script setup>
import { computed } from 'vue';

const props = defineProps({
  iconClass: {
    type: String,
    required: true
  }
})

const isExternal = computed(() => {
  return /^(https?:|mailto:|tel:)/.test(props.iconClass)
})
const iconName = computed(() => `#icon-${props.iconClass}`)

const styleExternalIcon = computed(() => {
  return {
    'mask': `url(${props.iconClass}) no-repeat 50% 50%`,
    '-webkit-mask': `url(${props.iconClass}) no-repeat 50% 50%`
  }
})

</script>

<style lang="scss" 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>

main.js 中注册插件

import svgIconPlugin from '@/icons/index.js'
Vue.use(svgIconPlugin)

组件使用

  • icon-class -> svg文件名称 class -> 自定义类名 控制样式
  • svgo 压缩svg文件 并移除svg默认填充颜色 可以通过类名控制, 如果不压缩则不能通过class控制
<template>
  <div>
    <svg-icon icon-class="hot" class="fs20" />
    <svg-icon icon-class="hot-01" class="fs20" />
  </div>
</template>
 

image.png

svgo 压缩svg文件 并移除svg默认填充颜色

  • 下载依赖
npm install svgo -D

配置svgo

  • 在icons文件夹下创建 svgo.config.js
module.exports = {
  plugins: [
    "preset-default", // 默认插件配置
    {
      name: "removeAttrs",
      params: {
        attrs: "(fill|stroke)"
      }
    }
  ]
}
  • 在package.json 文件中添加命令
 "scripts": {
   ...,
   
    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.config.js"
  },
  • 执行 npm run svgo 进行压缩

image.png

  • 每次添加svg图标文件都需要重新执行命令 移除默认填充的颜色防止类名控制失败

整体效果

image.png

image.png

在 vue3 配合 vite 中使用

下载依赖

npm install vite-plugin-svg-icons -D

vite.config.ts 配置

import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import path from "path"
import { createSvgIconsPlugin } from "vite-plugin-svg-icons"

export default defineConfig({
  resolve: {
    alias: [
      {
        find: "@",
        replacement: path.resolve(__dirname, "src")
      }
    ]
  },
  plugins: [
    vue(),
    ...,
    // 注册所有的svg文件生成svg雪碧图
    createSvgIconsPlugin({
      iconDirs: [path.resolve(process.cwd(), "src/icons/svg")], // icon存放的目录
      symbolId: "icon-[name]", // symbol的id
      inject: "body-last", // 插入的位置
      customDomId: "__svg__icons__dom__" // svg的id
    })
  ]
})
  • 在 main.ts 中引入注册脚本 import "virtual:svg-icons-register"
  • 注 如果报以下错误那么可以手动安装一下 fast-glob 包 有可能是幽灵依赖没有安装成功

image.png

  • 安装命令 npm install fast-glob -D

svg icon 组件

<template>
  <!-- 如果iconClass是带协议的图标链接 则通过style属性方式渲染-->
  <div
    class="svg-icon svg-external-icon"
    v-if="isExt"
    :style="styleExternalIcon"
    v-bind="$attrs"
  ></div>
  <!-- SVG icon 通过名称使用 -->
  <svg v-else :class="svgClass" aria-hidden="true" v-bind="$attrs">
    <use :xlink:href="iconName" />
  </svg>
</template>
<script setup lang="ts">
import { isExternal } from '@/utils/methods'
import { computed } from "vue"

const props = defineProps<{ iconClass: string; className?: string }>()
// 是否是带协议的图片链接
const isExt = computed(() => isExternal(props.iconClass || ""))
// 拼接成symbolId 在loader配置中指定了symbolId格式 icon-图标名称
const iconName = computed(() => `#icon-${props.iconClass}`)
// 添加类名 props.className外部传入自定义类名
const svgClass = computed(() =>
  props.className ? `svg-icon ${props.className}` : "svg-icon"
)
// 如果iconClass是带协议的图标链接 则通过style css属性方式渲染
const styleExternalIcon = computed(() => ({
  mask: `url(${props.iconClass}) no-repeat 50% 50%`,
  "-webkit-mask": `url(${props.iconClass}) no-repeat 50% 50%`
}))
</script>
<style lang="scss" 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>

  • isExternal 工具函数
export const isExternal = (path: string): boolean => {
  return /^(https?:|mailto:|tel:)/.test(path)
}

封装icon 插件

import { App } from "vue"
import SvgIcon from "@/components/SvgIcon/index.vue"

export default (app: App) => {
  // 全局注册svg-icon组件
  app.component("svg-icon", SvgIcon)
}
  • 引入svg插件通过 use 注册
import initSvgIcon from "@/icons/index" 

app.use(initSvgIcon)

简单使用

<template>
  <div>
    <!-- icon-class svg图标名称 class-name 额外的自定义类名 @click绑定事件 -->
    <svg-icon icon-class="bug"></svg-icon>
    <svg-icon icon-class="404" class-name="custom-class"></svg-icon>

  </div>
</template>
<script setup lang="ts">

</script>
<style lang="scss">
.custom-class {
  font-size: 200px;
  color: green;
}
</style>

  • 效果

image.png

  • svg 压缩同上