几个月前我刚接触vue3时就研究过,一直搞忘了分享。

起因

我的vue项目一般都使用动态导入的svg-sprite,意思是:既要是个雪碧图,用<use> 标签复用;而且页面的svg应该是动态按需加载的。我觉得这样性能比较好,在vue2中实现比较简单,但是vue3+vite下我并未找到合适的解决方案,只有雪碧图没有动态导入。于是乎我就想自己做一个。

思路

就两点

  • 首先要做一个vite plugin,作为一个loader加载.svg文件,读取svg文件的内容,类似raw-loader
  • 然后需要一个component,它去动态加载svg文件,并把svg文件的内容拼接到雪碧图里。

代码

vite.config.ts中这样写:

import {defineConfig, Plugin} from 'vite'
import vue from '@vitejs/plugin-vue'
import fs from "fs";
import {dataToEsm} from "rollup-pluginutils";

const rawSvgPlugin:Plugin = {
  name: 'raw-svg-file-loader',
  transform(svg: string, filepath: string) {
    // 判断后缀是否为svg
    if (filepath.slice(-4) !== '.svg') return null;
    const content = fs.readFileSync(filepath).toString()
    return {
      // 直接返回svg文件的原始内容
      code: dataToEsm(content)
    }
  },
}
export default defineConfig({
  plugins: [vue(), rawSvgPlugin],
})

IconSvg.vue文件:

<template>
  <svg aria-hidden="true">
    <use :href="getName"></use>
  </svg>
</template>

<script lang="ts">
import {defineComponent} from "vue";
const svgParser = new DOMParser();

export default defineComponent({
  name: "IconSvg",
  props: {
    name: {
      type: String,
      default: ''
    }
  },
  data (){
    return {
      getName: ''
    }
  },
  watch: {
    // 监听 name 变化
    '$props.name': {
      // 首次执行
      immediate: true,
      async handler (){
        // 拼接svg文件名
        const getId = `icon-${this.name}`
        const name = `#${getId}`
        // 动态加载
        const res = await import(`../svg/${this.name}.svg`);
        // 雪碧图的DOM容器
        let container = document.querySelector('#_SVG_SPRITE_CONTAINER_');
        if (!container || !container.querySelector(name)) {
          if (!container) {
            // 如果还未创建容器,就创建一个。(此处也可以直接写在index.html里)
            container = document.createElement('div');
            container.id = '_SVG_SPRITE_CONTAINER_'
            container.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
            container.setAttribute('style', 'position: absolute; width: 0; height: 0;overflow: hidden')
            document.body.insertBefore(container, document.body.children[0]);
          }
          if (!container.querySelector(name)) {
            // 如果容器内没有该svg,则解析并制作该svg的雪碧图
            const svgElement = svgParser.parseFromString(res.default, "image/svg+xml").querySelector('svg');
            if (svgElement) {
              //删除影响样式的属性
              for (const key of ['width', 'height', 'x', 'y']) {
                svgElement.removeAttribute(key)
              }
              svgElement.id = getId
              // 插入到容器里
              container.appendChild(svgElement as SVGSVGElement)
            }
          }
        }
        this.getName =  name;
      }
    }
  },
})
</script>

main.ts里只需要全局注册IconSvg组件就行了:

import { createApp } from 'vue'
import App from './App.vue'
import IconSvg from "./assets/svg/IconSvg.vue";

createApp(App).component('svg-icon', IconSvg).mount('#app')

这样使用:

<!-- 对应home.svg -->
<svg-icon name="home"/>

小结

这样做问题是解决了,可以动态导入svg并生成雪碧图,但是方式有点不优雅,有点投机取巧的感觉