vue:Vite项目中高效管理纯色SVG图标的方案

1、安装vite-plugin-svg-icons和fast-glob

bash 复制代码
npm install vite-plugin-svg-icons fast-glob -D

2、在 vite.config.js 中配置插件参数

iconDirs是svg文件存储路径,symbolId是图标ID生成规则。

javascript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'

export default defineConfig({
  plugins: [
    vue(),
    createSvgIconsPlugin({
      iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
      symbolId: 'icon-[name]'
    })
  ]
})

3、创建可复用的 SvgIcon.vue 组件

javascript 复制代码
<template>
  <svg :style="{ width: size + 'px', height: size + 'px' }">
    <use :href="`#icon-${name}`" :fill="color" />
  </svg>
</template>

<script setup>
defineProps({
  name: { type: String, required: true },
  color: { type: String, default: '#ff0000' },
  size: { type: [String, Number], default: 24 }
})
</script>

4、虚拟模块引入

在main.js里注册svg图标,这句话非常重要,一定要加。

javascript 复制代码
import 'virtual:svg-icons-register'

5、全局注册(可选)

如果不想全局注册组件,也可以单独在需要使用svgicon的页面引入。

javascript 复制代码
import { createApp } from 'vue'
import 'virtual:svg-icons-register' //非常重要,一定要加
import App from './App.vue'
import SvgIcon from '@/components/SvgIcon.vue'

const app = createApp(App)
app.component('SvgIcon', SvgIcon)
app.mount('#app')

6、在页面中使用

html 复制代码
 <SvgIcon name="fire" color="#00ff00" :size="36"></SvgIcon>

7、问题整理

(1)color失效

我发现,并不是每个svg都能设置color,经过一番查找,使用记事本打开svg代码后发现有的svg有fill属性,而有的没有,有fill属性的svg,color设置会失效。

解决办法,在SvgIconsPlugin里设置移除fill stroke等属性,特别注意,由于我们移除了fill和stroke属性,所以本文的svg icon封装方法只能用于纯色svg文件,多色svg文件不可使用这个方法。

javascript 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'

export default defineConfig({
  plugins: [
    vue(),
    createSvgIconsPlugin({
      iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
      symbolId: 'icon-[name]',
      svgoOptions: {
        plugins: [
          { 
            name: 'removeAttrs', 
            params: { attrs: ['fill', 'stroke'] }
          }
        ]
      }
    })
  ],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  }
})

(2)color异常值处理

比如color设置为1234,undefined,null,""等异常值,或者没设置color,我们需要优化代码,使用默认颜色。

html 复制代码
<SvgIcon name="fire" color="1234" :size="36"></SvgIcon>
<SvgIcon name="fire" :size="36"></SvgIcon>
javascript 复制代码
<template>
  <svg :style="{ width: size + 'px', height: size + 'px' }">
    <use :href="`#icon-${name}`" :fill="safeColor" />
  </svg>
</template>

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

const DEFAULT_COLOR = '#ff0000'

const props = defineProps({
  name: { type: String, required: true },
  color: { type: String},
  size: { type: [String, Number], default: 24 }
})

const safeColor = computed(() => {
  const colorValue = props.color
  
  if (!colorValue || typeof colorValue !== 'string') {
    return DEFAULT_COLOR
  }
  
  const trimmedColor = colorValue.trim()
  return trimmedColor || DEFAULT_COLOR
})
</script>

(3)name异常值处理

比如name没设置,或者设置的值在icons目录下找不到,我们需要优化代码,使用默认图标。

当name等于undefined,null,""等异常值,或者没设置color,name设置为默认值default,提前要在icons目录下放default.svg文件,比如<SvgIcon name="123"></SvgIcon>,则显示这个提前放置的default.svg。(如果忘记提前准备default.svg,则也会显示为默认图形)

当设置的值在icons目录下找不到,这种情况的处理,我花了一点时间,最后观察到这种情况发生的时候use标签的长宽都是0,所以最后使用的是检测use标签长宽来确定svg是否加载成功。

比如<SvgIcon name="123"></SvgIcon> 本地目录不存在123这个svg文件,那么就会显示一个默认颜色的默认图形,比如我这里设置为五角星。

html 复制代码
<SvgIcon name="123"></SvgIcon>
   <SvgIcon></SvgIcon>
javascript 复制代码
<template>
  <svg :style="{ width: size + 'px', height: size + 'px' }">
    <use 
      ref="useRef"
      :href="`#icon-${safeName}`" 
      :fill="safeColor"
    />
	<polygon 
	  v-if="iconLoadFailed"
	  points="16,2 20,10 29,11 22,18 24,27 16,22 8,27 10,18 3,11 12,10"
	  :fill="safeColor"
	/>
  </svg>
</template>

<script setup>
import { computed, ref, watch, nextTick } from 'vue'

const DEFAULT_COLOR = '#ff0000'
const DEFAULT_ICON_NAME = 'default'

const props = defineProps({
  name: { type: String },
  color: { type: String },
  size: { type: [String, Number], default: 30 }
})

const useRef = ref(null)
const iconLoadFailed = ref(false)

const safeName = computed(() => {
  const iconName = props.name
  if (!iconName || typeof iconName !== 'string') {
    return DEFAULT_ICON_NAME
  }
  const trimmedName = iconName.trim()
  return trimmedName || DEFAULT_ICON_NAME
})

const safeColor = computed(() => {
  const colorValue = props.color
  if (!colorValue || typeof colorValue !== 'string') {
    return DEFAULT_COLOR
  }
  const trimmedColor = colorValue.trim()
  return trimmedColor || DEFAULT_COLOR
})

// 检测 use 元素是否加载成功
const checkIconLoaded = () => {
  if (!useRef.value) return
  
  // 获取 use 元素的 bounding client rect
  const rect = useRef.value.getBoundingClientRect()
  
  // 如果宽高都是 0,说明图标加载失败
  if (rect.width === 0 && rect.height === 0) {
     iconLoadFailed.value = true
  } else {
    iconLoadFailed.value = false
  }
}

// 监听 safeName 变化,重新检测
watch(safeName, async () => {
  iconLoadFailed.value = false
  await nextTick()
  checkIconLoaded()
}, { immediate: true })
</script>

(4)name大小写不敏感处理

<SvgIcon name="CAMERA"></SvgIcon>

<SvgIcon name="camera"></SvgIcon> 这两种写法,都能匹配到camera.svg,CAMERA.svg,容错性更高。

javascript 复制代码
<template>
  <svg :style="{ width: size + 'px', height: size + 'px' }">
    <use 
      ref="useRef"
      :href="`#icon-${safeName}`" 
      :fill="safeColor"
    />
	<polygon 
	  v-if="iconLoadFailed"
	  points="16,2 20,10 29,11 22,18 24,27 16,22 8,27 10,18 3,11 12,10"
	  :fill="safeColor"
	/>
  </svg>
</template>

<script setup>
import { computed, ref, watch, nextTick } from 'vue'

const DEFAULT_COLOR = '#ff0000'
const DEFAULT_ICON_NAME = 'default'

const props = defineProps({
  name: { type: String },
  color: { type: String },
  size: { type: [String, Number], default: 30 }
})

const useRef = ref(null)
const iconLoadFailed = ref(false)

// 保存原始名称(用于尝试不同大小写)
const originalName = ref('')

const safeName = computed(() => {
  const iconName = props.name
  if (!iconName || typeof iconName !== 'string') {
    return DEFAULT_ICON_NAME
  }
  const trimmedName = iconName.trim()
  if (!trimmedName) {
    return DEFAULT_ICON_NAME
  }
  
  // 保存原始名称
  originalName.value = trimmedName
  
  // 先返回原始名称(保留大小写)
  return trimmedName
})

const safeColor = computed(() => {
  const colorValue = props.color
  if (!colorValue || typeof colorValue !== 'string') {
    return DEFAULT_COLOR
  }
  const trimmedColor = colorValue.trim()
  return trimmedColor || DEFAULT_COLOR
})

// 尝试不同大小写组合来检测图标是否存在
const checkIconWithCase = () => {
  if (!useRef.value) return false
  
  const rect = useRef.value.getBoundingClientRect()
  
  // 如果当前大小写能正常显示,返回 true
  if (rect.width !== 0 || rect.height !== 0) {
    return true
  }
  
  return false
}

// 尝试切换大小写重新加载图标
const tryDifferentCase = async (originalNameValue) => {
  // 定义需要尝试的大小写变体
  const variants = [
    originalNameValue,                        // 原始大小写
    originalNameValue.toLowerCase(),          // 全小写
    originalNameValue.toUpperCase(),          // 全大写
    originalNameValue.charAt(0).toUpperCase() + originalNameValue.slice(1).toLowerCase(), // 首字母大写
    originalNameValue.charAt(0).toLowerCase() + originalNameValue.slice(1).toUpperCase()  // 首字母小写其余大写
  ]
  
  // 去重
  const uniqueVariants = [...new Set(variants)]
  
  for (const variant of uniqueVariants) {
    if (variant === originalNameValue) continue // 跳过已经尝试过的原始名称
    
    // 动态修改 href
    const useElement = useRef.value
    if (useElement) {
      useElement.setAttribute('href', `#icon-${variant}`)
      await nextTick()
      
      // 检查是否加载成功
      const rect = useElement.getBoundingClientRect()
      if (rect.width !== 0 || rect.height !== 0) {
        // 找到可用的变体,更新 safeName 的逻辑
        console.warn(`SvgIcon: 图标 "${originalNameValue}" 使用变体 "${variant}" 加载成功`)
        return true
      }
    }
  }
  
  return false
}

// 检测 use 元素是否加载成功
const checkIconLoaded = async () => {
  if (!useRef.value) return
  
  // 获取 use 元素的 bounding client rect
  const rect = useRef.value.getBoundingClientRect()
  
  // 如果宽高都是 0,说明图标加载失败,尝试其他大小写
  if (rect.width === 0 && rect.height === 0) {
    // 尝试其他大小写变体
    const found = await tryDifferentCase(originalName.value)
    
    if (!found) {
      iconLoadFailed.value = true
      if (import.meta.env.DEV) {
        console.warn(`SvgIcon: 图标 "${originalName.value}" 不存在(已尝试多种大小写),使用后备五角星`)
      }
    } else {
      iconLoadFailed.value = false
    }
  } else {
    iconLoadFailed.value = false
  }
}

// 监听 safeName 变化,重新检测
watch(safeName, async () => {
  iconLoadFailed.value = false
  await nextTick()
  checkIconLoaded()
}, { immediate: true })
</script>

(5)配置多个图标源目录

当svg文件较多,想放在不同文件夹里的时候,可以设置多个图标源目录,页面里使用组件的方法不变。

javascript 复制代码
createSvgIconsPlugin({
			iconDirs: [
				path.resolve(process.cwd(), 'src/assets/svgs'),
				path.resolve(process.cwd(), 'src/assets/icons')
				],
			symbolId: 'icon-[name]',
			svgoOptions: {
				plugins: [{
					name: 'removeAttrs',
					params: {
						attrs: ['fill', 'stroke']
					}
				}]
			}
		})

(6)增加渐变色属性

javascript 复制代码
<template>
  <svg :style="{ width: size + 'px', height: size + 'px' }">
    <defs v-if="isGradient && gradient">
      <linearGradient 
        :id="gradient.id || 'svg-gradient'"
        :x1="gradient.x1 || '0%'"
        :y1="gradient.y1 || '0%'"
        :x2="gradient.x2 || '100%'"
        :y2="gradient.y2 || '100%'"
      >
        <stop 
          v-for="(stop, index) in gradient.stops"
          :key="index"
          :offset="stop.offset"
          :stop-color="stop.color"
          :stop-opacity="stop.opacity !== undefined ? stop.opacity : 1"
        />
      </linearGradient>
    </defs>
    
    <use 
      ref="useRef"
      :href="`#icon-${safeName}`" 
      :fill="finalFillColor"
    />
    
    <polygon 
      v-if="iconLoadFailed"
      points="16,2 20,10 29,11 22,18 24,27 16,22 8,27 10,18 3,11 12,10"
      :fill="finalFillColor"
    />
  </svg>
</template>

<script setup>
import { computed, ref, watch, nextTick } from 'vue'

const DEFAULT_COLOR = '#ff0000'
const DEFAULT_ICON_NAME = 'default'

const props = defineProps({
  name: { type: String },
  color: { type: String },
  size: { type: [String, Number], default: 30 },
  isGradient: { type: Boolean, default: false },
  gradient: {
    type: Object,
    default: null,
    // 示例格式:
    // {
    //   id: 'myGradient',
    //   x1: '0%', y1: '0%', x2: '100%', y2: '100%',
    //   stops: [
    //     { offset: '0%', color: '#ff6b6b', opacity: 1 },
    //     { offset: '100%', color: '#4ecdc4', opacity: 1 }
    //   ]
    // }
  }
})

const useRef = ref(null)
const iconLoadFailed = ref(false)

const safeName = computed(() => {
  const iconName = props.name
  if (!iconName || typeof iconName !== 'string') {
    return DEFAULT_ICON_NAME
  }
  const trimmedName = iconName.trim()
  return trimmedName || DEFAULT_ICON_NAME
})

const safeColor = computed(() => {
  const colorValue = props.color
  if (!colorValue || typeof colorValue !== 'string') {
    return DEFAULT_COLOR
  }
  const trimmedColor = colorValue.trim()
  return trimmedColor || DEFAULT_COLOR
})

// 最终填充颜色
const finalFillColor = computed(() => {
  if (props.isGradient && props.gradient && props.gradient.stops && props.gradient.stops.length >= 2) {
    const gradientId = props.gradient.id || 'svg-gradient'
    return `url(#${gradientId})`
  }
  return safeColor.value
})

// 检测 use 元素是否加载成功
const checkIconLoaded = () => {
  if (!useRef.value) return
  
  // 获取 use 元素的 bounding client rect
  const rect = useRef.value.getBoundingClientRect()
  
  // 如果宽高都是 0,说明图标加载失败
  if (rect.width === 0 && rect.height === 0) {
    iconLoadFailed.value = true
  } else {
    iconLoadFailed.value = false
  }
}

// 监听 safeName 变化,重新检测
watch(safeName, async () => {
  iconLoadFailed.value = false
  await nextTick()
  checkIconLoaded()
}, { immediate: true })
</script>

使用案例:

javascript 复制代码
<template>
	<h1 class="gradient-text">渐变效果</h1>
	<div class="icon-container">
		<!-- 纯色 -->
		<SvgIcon name="CAMERA" color="#aa00ff" :size="iconSize"></SvgIcon>
		<!-- 渐变色图标 -->
		<SvgIcon
			name="CAMERA"
			:is-gradient="true"
			:gradient="{
				id: 'starGradient',
				x1: '0%',
				y1: '0%',
				x2: '100%',
				y2: '100%',
				stops: [
					{ offset: '0%', color: '#ff6b6b' },
					{ offset: '100%', color: '#feca57' }
				]
			}"
			:size="iconSize"
		/>

		<!-- 三色渐变 -->
		<SvgIcon
			name="CAMERA"
			:is-gradient="true"
			:gradient="{
				id: 'heartGradient',
				x1: '0%',
				y1: '0%',
				x2: '100%',
				y2: '0%',
				stops: [
					{ offset: '0%', color: '#ff6b6b' },
					{ offset: '50%', color: '#feca57' },
					{ offset: '100%', color: '#48dbfb' }
				]
			}"
			:size="iconSize"
		/>

		<!-- 带透明度的渐变 -->
		<SvgIcon
			name="CAMERA"
			:is-gradient="true"
			:gradient="{
				id: 'logoGradient',
				x1: '0%',
				y1: '0%',
				x2: '0%',
				y2: '100%',
				stops: [
					{ offset: '0%', color: '#ff0000', opacity: 1 },
					{ offset: '50%', color: '#00ff00', opacity: 0.5 },
					{ offset: '100%', color: '#0000ff', opacity: 0.2 }
				]
			}"
			:size="iconSize"
		/>
	</div>
</template>

<script setup>
import { ref } from 'vue';
const iconSize = ref(48);
</script>

<style>
.icon-container {
	display: flex;
	justify-content: space-around;
	align-items: center;
	gap: 20px;
}
#app {
	font-family: Avenir, Helvetica, Arial, sans-serif;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
	text-align: center;
	color: #2c3e50;
	margin-top: 60px;
}
.gradient-text {
	/* 设置渐变背景 */
	background-image: linear-gradient(90deg, #ff6b6b, #4ecdc4);
	/* 背景仅裁剪到文字区域 */
	background-clip: text;
	/* 让文字原本的颜色透明,露出渐变背景 */
	color: transparent;
}
</style>

效果:

相关推荐
FlyWIHTSKY1 小时前
JavaScript 和 TypeScript 分别是什么,可以相互写吗
javascript·ubuntu·typescript
卤蛋fg61 小时前
vxe-table 列宽与行高拖拽调整:让表格布局极其灵活,拖拽功能非常强大
vue.js
YHHLAI1 小时前
JavaScript 数据结构精讲:数组底层与实战避坑
开发语言·javascript·数据结构
moMo2 小时前
Promise 的本质:不是异步处理,而是流程控制
javascript
dotnet902 小时前
PDF 页面尺寸上限是 14400。iText 直接加载成功的大图可能超过这个限制,需要在 setPageSize 之前等比缩放。
前端·javascript·html
threelab2 小时前
Three.js 几何图形变换 | 三维可视化 / AI 提示词
开发语言·前端·javascript·人工智能·3d·着色器
道友可好2 小时前
写给 AI 的入职手册,AGENTS.md
前端·人工智能·后端
吠品2 小时前
处理 Python 类继承中那些变来变去的初始化参数
linux·前端·python
云水一下2 小时前
TypeScript 从零基础到精通(七):从配置到全栈项目落地
前端·javascript·typescript