手动封装一个svg图标集(symbol雪碧图),并在vue项目中应用

5702次阅读 424人点赞 作者: WuBin 发布时间: 2025-10-28 13:53:57
扫码到手机查看

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 雪碧图)的方式更优,原因是:

  1. 减少 DOM 节点数量,降低渲染和内存开销;
  2. 避免 JS 文件体积膨胀;
  3. 资源复用率高,性能更稳定。

如果需要图标的动态交互(如变色、动画),可以折中选择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>

这种方式好处:

  1. 性能优秀:所有图标只定义一次(在 SVG Sprite 中),v-for 循环时仅通过<use>引用,DOM 节点数量极少,渲染成本低。
  2. 样式灵活

    可通过 CSS 直接修改图标颜色(color)、尺寸(width/height)、hover 效果等,无需修改 SVG 源码。

  3. 文件体积小:图标集中管理,避免重复代码,JS/CSS 文件体积不会因图标数量增加而膨胀。
  4. 复用性高:一次定义,全项目可用,维护成本低(修改图标只需改 SVG Sprite)。
  5. DOM 开销:极低:SVG symbol 本质是 “一次定义,多次引用”:所有图标只在 Sprite 中定义一次(DOM 中只有一份完整的 SVG 结构),而<use>标签只是 “引用指针”,并非复制整个 SVG 内容(即使在 v-for 中循环 100 次,DOM 结构也只是 100 个 <svg><use></use></svg> 节点,每个节点非常轻量(没有复杂的 <path> 等子元素),远少于直接嵌入完整 SVG 的 DOM 数量)。

  6. 渲染开销:接近 CSS 背景图

    浏览器渲染 <use> 引用的图标时,相当于 “绘制同一个模板的副本”,渲染逻辑比解析全新的 SVG 节点更高效。对比 CSS 背景图:两者渲染性能接近(都避免了重复解析资源),但 SVG symbol 支持更多动态样式(如通过 CSS 改色、动画),而背景图的样式修改受限(如需改色可能需要多份 SVG 文件)。对比直接嵌入 SVG DOM:减少了大量重复的路径解析和绘制,尤其在图标数量多、样式复杂时,性能优势更明显。

  7. 兼容性与边缘情况:现代浏览器(Chrome、Firefox、Safari、Edge 等)对 SVG symbol 和 <use> 的支持非常完善,不存在兼容性问题。唯一需要注意的是:如果通过单独文件引入 Sprite(如 <object data="sprite.svg">),在本地开发时可能因跨域问题导致引用失败(部署到服务器后通常解决),推荐直接嵌入到 HTML 中避免此问题。

注意事项

  • 确保 SVG 图标内部没有硬编码fillstroke颜色(如需动态变色),让其继承 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一致(注意:如果原viewBox0 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(可以直接用)。
经过我实测,不能将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>
点赞 支持一下 觉得不错?客官您就稍微鼓励一下吧!
关键词:svg雪碧图,symbol
推荐阅读
  • python基础-操作列表和迭代器

    python基础笔记-操作列表和迭代器的相关方法

    6702次阅读 164人点赞 发布时间: 2024-06-13 13:26:27 立即查看
  • uniapp实现被浏览器唤起的功能

    当用户打开h5链接时候,点击打开app若用户在已经安装过app的情况下直接打开app,若未安装过跳到应用市场下载安装这个功能在实现上主要分为两种场景,从普通浏览器唤醒以及从微信唤醒。

    12096次阅读 819人点赞 发布时间: 2022-12-14 16:34:53 立即查看
  • PHP

    【正则】一些常用的正则表达式总结

    在日常开发中,正则表达式是非常有用的,正则表达式在每个语言中都是可以使用的,他就跟JSON一样,是通用的。了解一些常用的正则表达式,能大大提高你的工作效率。

    15311次阅读 636人点赞 发布时间: 2021-10-09 15:58:58 立即查看
  • 【中文】免费可商用字体下载与考证

    65款免费、可商用、无任何限制中文字体打包下载,这些字体都是经过长期验证,经得住市场考验的,让您规避被无良厂商起诉的风险。

    16017次阅读 1279人点赞 发布时间: 2021-07-05 15:28:45 立即查看
  • Vue

    Vue3开发一个v-loading的自定义指令

    在vue3中实现一个自定义的指令,有助于我们简化开发,简化复用,通过一个指令的调用即可实现一些可高度复用的交互。

    18624次阅读 1495人点赞 发布时间: 2021-07-02 15:58:35 立即查看
  • JS

    关于手机上滚动穿透问题的解决

    当页面出现浮层的时候,滑动浮层的内容,正常情况下预期应该是浮层下边的内容不会滚动;然而事实并非如此。在PC上使用css即可解决,但是在手机端,情况就变的比较复杂,就需要禁止触摸事件才可以。

    16808次阅读 1358人点赞 发布时间: 2021-05-31 09:25:50 立即查看
  • Vue

    Vue+html2canvas截图空白的问题

    在使用vue做信网单页专题时,有海报生成的功能,这里推荐2个插件:一个是html2canvas,构造好DOM然后转canvas进行截图;另外使用vue-canvas-poster(这个截止到2021年3月...

    32540次阅读 2555人点赞 发布时间: 2021-03-02 09:04:51 立即查看
  • Vue

    vue-router4过度动画无效解决方案

    在初次使用vue3+vue-router4时候,先后遇到了过度动画transition进入和退出分别无效的情况,搜遍百度没没找到合适解决方法,包括vue-route4有一些API都进行了变化,以前的一些操...

    28441次阅读 2193人点赞 发布时间: 2021-02-23 13:37:20 立即查看
交流 收藏 目录