一、为啥需要重构,第三方库不香吗?
首先基础技术栈:
- vite
- vue3
适配vite+vue3
的svg loader
目前已经有很多相对成熟的方案了,但是考虑到特殊适应场景、开发便捷、易于维护、加载性能、自定义特性等等方面,各大插件还是有各自不足,下面是痛苦的尝试过程:
1. vite-plugin-svg-icons
插件信息 | 描述 |
---|---|
github地址 | 传送门 |
github star | 🌟861 |
release version | v2.0.1 |
开发者 | vbenjs |
是否活跃 | 3 year ago |
使用方式参考github readme,这里就不多说了,主要实现原理:
- 递归遍历配置目录及其子目录,找出所有svg文件;
- 解析处理svg文件内容(去除空格、注释);
- 生成svg雪碧图。雪碧图包含多个
<symbol>
元素,每个<symbol>
对应一个原始的svg文件,有唯一的id
属性,用于后续引用; - 注入雪碧图到html;
- 提供使用方法。
优势:
- 通过雪碧图减少http请求,缓存效率高;
- 集中管理图标,易于维护,支持多层级目录分类;
- 支持动态切换图标,基于
<use>
元素。
缺点:
- 初始加载提及较大。如果项目用到大量的svg图标,生成的雪碧图文件会比较大,导致首次加载时间增加,尤其在网络不好的情况下,需要很长时间才能看到图标;
- 不适合动态图标内容。该插件是生成静态的雪碧图,需要改变图标的颜色、形状需要通过js来修改svg属性;
- 调试难度大。所有图标都合并在一个文件中,如果某个图标出现异常,调试困难,需要仔细查看雪碧图的解构和引用关系来定位问题。
我的痛点:
- 项目使用的
svg有点多
,所有的svg直接生成在一个文件里,并且被注入到了html中
,导致页面首次打开巨慢; - 我的项目是
多webview
的,每打开一个页面都会新开一个webview,再次加载一遍html,又打开巨慢,没有发挥到缓存的优势; - 部分复杂图标
显示不全
或渲染不正确
,这个一般是不能接受的。
请看大屏幕,页面只有3个svg图标,但是配置的icon目录中所有的svg都注入到html中了,非常影响html首次渲染时间,我们在做性能优化的时候发现这个是瓶颈之一。而且第二个和第三个svg是渲染有问题的。
2. vite-svg-loader
插件信息 | 描述 |
---|---|
github地址 | 传送门 |
github star | 🌟619 |
release version | v5.1.0 |
开发者 | jpkleemans |
是否活跃 | 2 years ago |
使用方式也暂不多说,主要原理:
- 拦截svg文件加载,传递给vite-svg-loader处理;
- 解析和转换svg文件,将svg文件转换为js模块,如果是vue项目则会转换为vue组件;
- 返回处理后的模块,将vue组件返回给vite, vite再整合到项目的构建流程中。
优势:
- 灵活的使用方式。可以作为
URL
,Raw
,Component
的方式导入使用,方便进行样式定制和交互操作; - 无缝集成 Vite。无需额外复杂的配置,开发者可以在 Vite 项目中快速使用 SVG 文件。
- 提高开发效率。可以直接在代码中导入 SVG 文件,而无需手动处理 SVG 的加载和转换,减少了开发过程中的繁琐步骤。
- 支持按需加载,提高性能。在构建过程中,插件可以根据项目的实际使用情况对 SVG 进行优化处理,只打包项目中实际使用的 SVG 文件,避免不必要的资源浪费。
缺点:
- 配置项复杂。为了实现不同的导入形式和功能,插件提供了较多的配置选项,对于初学者来说,可能需要花费一定的时间来理解和配置这些选项。
- 调试困难。由于插件对 SVG 文件进行了转换,当出现问题时,调试相对困难,需要深入了解插件的转换逻辑才能定位问题。
- id重复导致渲染有问题。因内部没有处理svg属性或引用ID唯一性,导致多个相同svg渲染出现引用混乱从而出现样式渲染不对。
我的痛点:
svg ID重复问题
。因为项目中有tabs,每个tab里可能有相同的图标,导致切换的时候出现样式混乱,svg内部ID引用出错。
请看大屏幕,vite-svg-loader
插件在切换的时候第1个和第3个直接渲染异常了,在tab1的时候第3个还直接不显示了,其实就是因为ID冲突的问题导致的。
3. unplugin-icons (原vite-plugin-icons)
插件信息 | 描述 |
---|---|
github地址 | 传送门 |
github star | 🌟4.3k |
release version | v22.1.0 |
开发者 | antfu |
是否活跃 | 活跃2025 |
这个插件是 antfu
大佬写的,他的作品包括 unocss, vueuse, nuxt
等等。
此插件功能强大,主要特性包括:
- 通用。适用任何图标集,由Iconify支持;
- 主要构建工具——Vite、Webpack、Rollup、Nuxt、Rspack 等。由unplugin提供支持;
- 主要框架 - Vanilla、Web Components、React、Vue 3、Vue 2、Solid、Svelte 等。
- 按需加载
- SSR / SSG 友好
- 可样式化 —按照样式和类别的方式更改大小、颜色,甚至添加动画
- 自定义图标
- 自动导入
此插件使用方式也参考官方文档,主要实现原理:
- 插件预先集成了大量常见的图标集,如 Material Icons、Font Awesome 等。它支持从不同的图标库获取图标数据,并且允许开发者自定义图标集。开发者可以通过配置指定要使用的图标集;
- 图标数据下载与缓存:如果使用的图标集在本地没有缓存,插件会从对应的图标库源下载图标数据,并将其缓存到本地,以便后续使用;
- 动态组件创建:在构建过程中,插件会根据开发者在代码中使用的图标名称,动态生成对应的图标组件;
- 代码转换:插件会对代码进行转换,将图标组件的引用替换为实际的 SVG 代码或经过优化处理后的图标渲染代码。对于不同的框架(如 Vue、React 等),会生成适配该框架的组件代码;
- 构建集成:插件会在构建工具的特定钩子中执行上述操作。以 Vite 为例,它会在 transform 钩子中对代码进行处理,确保图标组件能正确地被转换和打包到最终的构建产物中。
优势:
- 丰富的图标集支持,也支持自定义图标集;
- 组件化使用,
<i-mdi:home />
; - 按需加载,优化打包体积;
- 多框架支持
缺点:
- 配置复杂,图标集配置和自定义图标配置复杂;
- 性能开销,虽然插件实现了动态生成图标组件,但运行时动态生成会带来性能问题;
- 组件化使用虽然方便,但是对于图标切换能操作还是要引入多个组件,不太方便;
- 自定义图标集不支持多层级目录,一次只能设置一个目录(不支持递归目录)
- 不支持动态导入多级目录的自定义svg
- 虽然插件解决了ID唯一性的问题,但是没有处理style标签里ID引用,导致有的svg渲染出错
我的痛点:
- 自定义图标集只支持一个个文件夹单独配置,不能自动按文件夹分namespace;
- 插件没有处理svg style样式中的id引用问题,对style标签里的id没有做唯一性处理。
请看大屏幕,其他图标是ok的,第三个图标因为svg中的style标签里的id引用没有被转换导致不能正常显示。
二、告别第三方束缚,自主打造svg-loader
经过第一阶段折磨后发现,并非所有的第三方插件都满足我们的需求,但是我们可以从别人的插件中汲取教训,把别人的优势加上我们发现的缺陷补齐不就能打造完美的方案了吗。
经过几个插件的对比发现unplugin-icons
更接近我们的需求,所以就参考它的实现,站在巨人的肩膀上继续开发。
插件原理
- 拦截svg文件加载,支持虚拟路径
- 加载svg文件,取svg文件路径作为collection
- 处理svg id问题,给svg文件加一个ref,根据属性中的ID生成
{id:随机hash}
映射表,将id替换为映射表表示;根据style中的id同样生成映射表,也使用模板字符串占位符,将style标签替换为<component is="style" ${attrs}></component>
防止样式丢失;将id属性替换为vue的动态属性绑定; - 构建注入脚本:在mounted钩子中生成svg唯一ID,创建svg style标签,替换style中的模板字符串,改为map映射表中的值,删除旧style,插入新style到svg中;
@vue/compiler-sfc
的compileTemplate方法编译生成code;- 在code后面拼接
markRaw
方法并在setup方法中执行注入脚本
难点:
- 通过注入ref,最后获取svg dom,才能注入style;
- style中的唯一id不能像svg属性那样使用vue的动态绑定,需要通过模板字符串替换的方式;
- 需要给style中的所有class样式也加上svg的id来隔离样式;
- 唯一ID需要在编译后再生成,否则同一个svg经过编译后ID都是一样的。
业务组件二次封装
单独import
的方式可以使用,为了能方便业务使用,做了以下封装:
<script lang="ts" setup>
const props = defineProps({
// 名称
name: {
type: String,
default: '',
required: true,
},
// 大小
size: {
type: Number,
default: 24,
},
// 颜色
color: {
type: String,
default: '',
},
// class
className: {
type: String,
default: '',
},
// 无障碍朗读文本
ariaTitle: {
type: String,
default: '',
},
})
const svgStyle = computed(() => {
return {
width: props.size + 'px',
height: props.size + 'px',
color: props.color,
}
})
const svgModules = import.meta.glob('@/assets/svgs/**/*.svg')
const svgIconMap: Record<string, () => Promise<any>> = {}
Object.keys(svgModules).forEach((path) => {
const relativePath = path.replace('/src/assets/svgs/', '').replace('.svg', '')
// 子文件夹里svg 命名空间
const iconName = relativePath.replace(/\//g, '-')
svgIconMap[iconName] = svgModules[path]
})
const iconComponent = computed(() => {
const component = defineAsyncComponent(svgIconMap[props.name])
return component
})
</script>
<template>
<Suspense>
<template #default>
<component
v-if="iconComponent"
:is="iconComponent"
class="svg-icon"
:style="svgStyle"
:class="className"
:aria-hidden="ariaTitle ? 'false' : 'true'"
role="img"
:aria-label="ariaTitle"
></component>
</template>
<template #fallback>
<div
class="svg-icon-placeholder"
:style="{ width: svgStyle.width, height: svgStyle.height }"
></div>
</template>
</Suspense>
</template>
<style scoped>
.svg-icon,
.svg-icon-placeholder {
display: inline-block;
vertical-align: middle;
}
.svg-icon-placeholder {
background-color: #ccc;
}
</style>
使用方式:
<my-icon name="customer"></my-icon>
<my-icon name="dashboard"></my-icon>
<my-icon name="accept" aria-title="accept" :size="100"></my-icon>
实现特性:
- svg文件转换为vue组件
- 按需加载
- svg文件style样式隔离
- svg文件ID唯一化
- 支持虚拟路径"~icons"
- 支持多级目录svg
- 无障碍属性处理
- svg骨架屏预渲染
以下是自定义插件的实现效果,完美渲染svg:
三、完整vite-svg-loader重构流程图
四、SVG组件化的破局收获
- vite插件中可使用vue编译器改变渲染结果;
- 编译器结合js注入配合能修改编译后的dom;
- 三方插件在享受方便的同时,也承担着风险,可能有bug、可能不满足需求、可能不易于扩展功能。在探索第三方库的过程中也会尝试理解作者意图,学习作者开发方式,对自研插件有很大帮助;
- 再难的问题总有解决之道,善用工具打开思路。