手动封装一个svg图标集(symbol雪碧图),并在vue项目中应用
svg使用分析
svg有几种使用方式:
- 当作文件,让img和background属性进行引用;
- 直接将svg元素内嵌在HTML中;
- 使用SVG symbol 雪碧图(划重点)
SVG DOM 结构 vs CSS 背景图
SVG DOM 结构的开销:每个
<svg>都是真实的 DOM 节点,在 v-for 循环中会生成大量独立的 DOM 元素。DOM 节点数量过多会导致:- 渲染成本高:浏览器需要解析和渲染每个 SVG 节点,内存占用增加,尤其在循环数量大(比如几十上百个)时,可能导致页面卡顿或重绘 / 回流频繁。
- 事件监听冗余:如果 SVG 上有交互(如点击),过多 DOM 节点会增加事件处理的负担。
CSS 背景图的开销:所有图标通过同一个
icon.svg(或雪碧图)加载,多个<i class="icon-bg">共享同一个背景图资源:- DOM 节点更轻量:
<i>是简单的空标签,没有复杂的子节点,DOM 树结构更简洁,渲染和维护成本低。 - 资源复用率高:
icon.svg只会被浏览器请求一次并缓存,后续复用无需重复加载,减少网络请求。
文件体积:JS 文件是否会更大?
SVG DOM 结构:每个 SVG 的源码(如
<svg width="20" ...>)会直接嵌入到组件中,随着图标数量增加,组件代码会越来越臃肿,最终导致打包后的 JS 文件体积增大(尤其 SVG 内容复杂时)。CSS 背景图:SVG 文件作为独立资源存在,不会混入 JS 代码中,JS 文件体积不受图标数量影响。CSS 中仅通过
url()引用,代码更简洁。
兼容性
两者兼容性都很好,但老旧浏览器(如 IE8 及以下)不支持 SVG 背景图,需注意兼容需求。
最好的方案
在 v-for 循环中(尤其是循环数量较多时),使用 CSS 背景图(或 SVG 雪碧图)的方式更优,原因是:
- 减少 DOM 节点数量,降低渲染和内存开销;
- 避免 JS 文件体积膨胀;
- 资源复用率高,性能更稳定。
如果需要图标的动态交互(如变色、动画),可以折中选择SVG 雪碧图(symbol 方式):将所有 SVG 定义在一个<symbol>中,通过<use xlink:href="#icon-id">引用,既保持 DOM 简洁,又支持动态样式修改,是更平衡的方案。
SVG symbol是一种将多个 SVG 图标整合为一个 “雪碧图” 的技术,通过<symbol>标签定义可复用的 SVG 图形,再用<use>标签引用,既能减少 DOM 节点数量,又保留了 SVG 的可操作性(如动态修改颜色、尺寸),是平衡性能和灵活性的最佳方案之一。
比如:
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<!-- 箭头左图标,id为arrow-left -->
<symbol id="arrow-left" viewBox="0 0 24 24">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12l4.58-4.59z" />
</symbol>
<!-- 箭头右图标,id为arrow-right -->
<symbol id="arrow-right" viewBox="0 0 24 24">
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
</symbol>
<!-- 可继续添加更多图标... -->
</svg>:隐藏整个 SVG 容器(只作为定义,不直接显示)。viewBox:定义图标的坐标范围,确保缩放时不失真。id:每个图标的唯一标识,后续通过此 id 引用。
并且,将icon-sprite.svg的内容直接写在项目的根 HTML(如index.html)中,一次加载,全局可用:
然后,在组件中通过<use>引用图标:
在需要使用图标的地方,用<svg>标签包裹<use>,通过xlink:href指向目标图标的id(格式:#图标id)。
<!-- 图标组件 -->
<svg class="base-icon-svg" :width="size" :height="size">
<!-- 引用对应的symbol -->
<use :xlink:href="`#${label}`"></use>
</svg>这种方式好处:
- 性能优秀:所有图标只定义一次(在 SVG Sprite 中),v-for 循环时仅通过
<use>引用,DOM 节点数量极少,渲染成本低。 - 样式灵活:
可通过 CSS 直接修改图标颜色(
color)、尺寸(width/height)、hover 效果等,无需修改 SVG 源码。 - 文件体积小:图标集中管理,避免重复代码,JS/CSS 文件体积不会因图标数量增加而膨胀。
- 复用性高:一次定义,全项目可用,维护成本低(修改图标只需改 SVG Sprite)。
DOM 开销:极低:SVG symbol 本质是 “一次定义,多次引用”:所有图标只在 Sprite 中定义一次(DOM 中只有一份完整的 SVG 结构),而
<use>标签只是 “引用指针”,并非复制整个 SVG 内容(即使在 v-for 中循环 100 次,DOM 结构也只是 100 个 <svg><use></use></svg> 节点,每个节点非常轻量(没有复杂的 <path> 等子元素),远少于直接嵌入完整 SVG 的 DOM 数量)。渲染开销:接近 CSS 背景图
浏览器渲染 <use> 引用的图标时,相当于 “绘制同一个模板的副本”,渲染逻辑比解析全新的 SVG 节点更高效。对比 CSS 背景图:两者渲染性能接近(都避免了重复解析资源),但 SVG symbol 支持更多动态样式(如通过 CSS 改色、动画),而背景图的样式修改受限(如需改色可能需要多份 SVG 文件)。对比直接嵌入 SVG DOM:减少了大量重复的路径解析和绘制,尤其在图标数量多、样式复杂时,性能优势更明显。
兼容性与边缘情况:现代浏览器(Chrome、Firefox、Safari、Edge 等)对 SVG symbol 和 <use> 的支持非常完善,不存在兼容性问题。唯一需要注意的是:如果通过单独文件引入 Sprite(如 <object data="sprite.svg">),在本地开发时可能因跨域问题导致引用失败(部署到服务器后通常解决),推荐直接嵌入到 HTML 中避免此问题。
注意事项
- 确保 SVG 图标内部没有硬编码
fill或stroke颜色(如需动态变色),让其继承 CSS 的color。 - 如果使用 Vue CLI 或 Vite,可通过插件(如
vite-plugin-svg-icons)自动生成 SVG Sprite,简化配置(以后研究明白再写一篇自动打包的)。 - 如果通过单独文件引入 Sprite(如 <object data="sprite.svg">),在本地开发时可能因跨域问题导致引用失败(部署到服务器后通常解决),推荐直接嵌入到 HTML 中避免此问题。
SVG 雪碧图(symbol 方式)
定义 SVG Sprite(集中存放所有图标)
创建一个包含所有图标的 SVG 文件(如icon-sprite.svg),用<symbol>标签包裹每个图标,并给每个<symbol>设置唯一的id(用于后续引用)。
如下例子,将两个向左向右的箭头,在文件中嵌入:
<svg xmlns="http://www.w3.org/2000/svg" hljs-string">none;">
<symbol id="arrow-left" viewBox="0 0 1024 1024">
<path d="M778.671 926.323a56.811 56.811 0 1 1-80.33 80.331L243.85 552.165a56.811 56.811 0 0 1 0-80.33l454.49-454.49a56.811 56.811 0 1 1 80.33 80.331L364.348 512 778.67 926.323z"></path>
</symbol>
<symbol id="arrow-right" viewBox="0 0 1024 1024">
<path d="M236.552013 926.853955a55.805997 55.805997 0 0 0 0 80.454996 59.682997 59.682997 0 0 0 82.794996 0l468.099978-455.081978a55.805997 55.805997 0 0 0 0-80.453996L319.348009 16.689999a59.682997 59.682997 0 0 0-82.794996 0 55.805997 55.805997 0 0 0 0 80.454996L663.401993 511.999975 236.624013 926.853955z"></path>
</symbol>
</svg>可以在阿里的https://www.iconfont.cn/ 下载或者复制svg结构,只获取path内容,和viewBox关键属性,另外必须要去掉fill这种填充色属性。可以删除或者保留的属性参考如下:
| 原属性 | 作用说明 | 是否需要加在<symbol>上 | 理由 |
|---|---|---|---|
t="1761616901904" | 可能是设计工具生成的时间戳(非标准属性) | 不需要 | 非 SVG 标准属性,无实际功能,会增加代码冗余。 |
class="icon" | 样式类名 | 不需要 | 样式应通过最终引用<svg>标签的class控制(如你封装的svg-icon组件),<symbol>作为模板无需单独加类。 |
viewBox="0 0 1024 1024" | 定义图标坐标范围(核心属性) | 必须保留 | 决定图标缩放是否失真,是<symbol>的核心属性,必须与原 SVG 的viewBox一致(注意:如果原viewBox是0 0 1024 1024,就不能改成24 24,需保持原数值)。 |
version="1.1" | SVG 版本号 | 不需要 | 现代浏览器默认支持 SVG 1.1,无需显式声明。 |
xmlns="http://www.w3.org/2000/svg" | XML 命名空间(标准属性) | 不需要 | 父级<svg>(Sprite 容器)已声明xmlns,<symbol>会继承,无需重复写。 |
p-id="1377" | 设计工具生成的唯一标识(非标准属性) | 不需要 | 非标准属性,无实际功能,可删除。 |
xmlns:xlink="http://www.w3.org/1999/xlink" | XLink 命名空间 | 不需要 | 父级<svg>已声明,<symbol>继承即可;且现代 SVG 对内部<path>已无需额外声明。 |
width="200" height="200" | 固定尺寸 | 不需要 | 图标尺寸应通过组件的size属性动态控制(如你封装的:size="24"),<symbol>无需固定尺寸。 |
经过我实测,不能将symbol雪碧图icon-sprict.svg,直接使用object进行引用,因为object以及iframe会在文档中创建独立的上下文,从而导致当前文档无法获取指定的svg,导致图标为空。
// 在APP.vue或者public/index.html中:
<object
data="/icon-sprite.svg" <!-- 路径对应 public 目录下的文件 -->
type="image/svg+xml"
<!-- 关键:隐藏这个 SVG 容器,只复用内部 symbol -->
></object>然后在组件中调用:
<svg
:class="['svg-icon', className]"
:width="100"
:height="100"
:fill="#fff"
:style="style"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<use :xlink:href="`#arrow-left`" />
</svg>在浏览器渲染后发现,svg外层是100 * 100的方块,里面的use是0 * 0!是没有内容的!
所以我的解决方法是将svg文件再封装成组件,然后将组件直接写入到App.vue中(或者直接将svg雪碧图代码结构写在index.html中)
<template>
<div class="svg-icons" hljs-string">none;">
<svg xmlns="http://www.w3.org/2000/svg" hljs-string">none;">
<symbol id="arrow-left" viewBox="0 0 1024 1024">
<path d="M778.671 926.323a56.811 56.811 0 1 1-80.33 80.331L243.85 552.165a56.811 56.811 0 0 1 0-80.33l454.49-454.49a56.811 56.811 0 1 1 80.33 80.331L364.348 512 778.67 926.323z"></path>
</symbol>
<symbol id="arrow-right" viewBox="0 0 1024 1024">
<path d="M236.552013 926.853955a55.805997 55.805997 0 0 0 0 80.454996 59.682997 59.682997 0 0 0 82.794996 0l468.099978-455.081978a55.805997 55.805997 0 0 0 0-80.453996L319.348009 16.689999a59.682997 59.682997 0 0 0-82.794996 0 55.805997 55.805997 0 0 0 0 80.454996L663.401993 511.999975 236.624013 926.853955z"></path>
</symbol>
</svg>
</div>
</template>
<script type="text/ecmascript-6">
export default {
name: "svg-icons"
}
</script>
<style lang="less" rel="stylesheet/less" scoped>
.svg-icons {}
</style>
推荐在App.vue里引入:
<template>
<section class="app-section">
<!-- 引入symol svg引用 -->
<svg-icons></svg-icons>
<router-view v-slot="{ Component, route }">...</router-view>
</section>
</template>这样就把svg图标集插入到了文档中。
封装基础组件
这里我再封装一个基础组件,方便直观的调用:
<template>
<div class="base-icon-svg">
<svg
:class="['svg-icon', className]"
:width="width"
:height="height"
:fill="fill"
:style="style"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<use :xlink:href="`#${label}`" />
</svg>
</div>
</template>
<script type="text/ecmascript-6">
export default {
name: "base-icon-svg",
props: {
label: {
type: String,
default() {
return '';
}
},
fill: {
type: String,
// 默认继承父元素的color属性
default() {
return "currentColor";
}
},
width: {
type: Number,
default() {
return 200;
}
},
height: {
type: Number,
default() {
return 200;
}
},
style: {
type: Object,
default: () => ({})
}
}
}
</script>
<style lang="less" rel="stylesheet/less" scoped>
.base-icon-svg {
display: inline-block;
/* height: 100%; 避免继承父元素高度导致尺寸异常 */
}
.svg-icon {
/* 用 props 的 width/height 作为优先值,避免 auto 或 100% 覆盖 */
width: inherit; /* 继承 svg 标签的 width 属性(即 props 的 width) */
height: inherit; /* 继承 svg 标签的 height 属性(即 props 的 height) */
/*fill: currentColor; 避免继承父元素的颜色*/
/*fill: inherit; !* 继承 svg 标签的 fill 属性(即 props 的 fill) *!*/
vertical-align: middle;
}
</style>对于svg雪碧图,我们可以使用CSS控制和props控制,都可以,为了方便我这里使用了props参数进行控制。如果需要使用CSS进行控制,颜色这里切记需要加上:
fill: currentColor; 避免继承父元素的颜色可以省很多事。
然后再将这个基础组件进行全局注册,这样就不用每个组件都import一遍了。
在main.js中:
import {createApp} from 'vue';
import App from './App.vue';
import router from './router/index';
import store from './store';
// 添加v-loading
import loadingDirective from '@/components/base/base-loading/directive';
// 全局组件
import BaseIconSvg from '@/components/base/base-icon-svg/base-icon-svg';
import '@/common/less/index.less';
const app = createApp(App);
app
.directive('loading', loadingDirective)
.use(store)
.use(router)
// 全局注册组件(第一个参数是组件名,第二个是组件对象) component() 方法只能由应用实例调用
.component('base-icon-svg', BaseIconSvg);
app.mount('#app');在页面中直接使用即可:
<div class="base-top-bar">
<base-icon-svg label="arrow-left"
fill="#fff"
class="icon-back"
v-if="showBack"
:width="20"
:height="20"
@click="goback"
>
</base-icon-svg>
<p>{{ title }}</p>
</div>
目录