【山竹记账前端-vue】6.Vite SVG Sprites


大纲链接 §

[toc]


0. 解决图标加载不流畅方案:自制 Vite SVG Sprites 插件 v.s. 社区插件 ⇧

解决图标加载不流畅 logo 图标资源加载可能比较慢

  • 在第一页时,就提前加载所有页面的图标资源,需要一劳永逸地解决
  • 将所有分散的 SVG 资源统一放入一个 大 SVG,在 body 中预加载先,组件其他部分使用时就不需要再次加载
  • 使用 SVG Sprites 代替已经过时的 png 图片精灵图

1. 方案一:Vite SVG Sprites Plugin ⇧

安装与配置 ⇧

需要依赖两个基础的库

安装

  • pnpm add svgo@4.1.0 svgstore@3.0.1

实现插件 @/plugins/svgoStore.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import path from 'node:path'
import fs from 'node:fs'
// @ts-ignore
import svgstore from 'svgstore' // 用于制作 SVG Sprites
import { optimize, type Config as SvgoConfig } from 'svgo' // 用于优化 SVG 文件
import type { Plugin } from 'vite'

export interface SvgStoreOptions {
  cleanDefs?: boolean | string[]
  cleanSymbols?: boolean | string[]
  inline?: boolean
  svgAttrs?: boolean | Record<string, string | number | boolean>
  symbolAttrs?: boolean | Record<string, string | number | boolean>
  copyAttrs?: boolean | string[]
  renameDefs?: boolean
  [key: string]: unknown
}

export interface SvgoStoreOptions extends SvgStoreOptions {
  /**
   * SVG 图标源文件夹路径
   * @default 'src/assets/icons'
   */
  inputFolder?: string
  /**
   * SVGO 优化配置
   */
  svgoConfig?: SvgoConfig
}

export const svgoStore = (options: SvgoStoreOptions = {}): Plugin => {
  const inputFolder = options.inputFolder ?? 'src/assets/icons'
  return {
    name: 'svgoStore',
    resolveId(id: string) {
      if (id === '@svgoStore') {
        // 防止 vscode 报错
        return 'svg_bundle.js'
      }
      return null
    },
    load(id: string) {
      if (id === 'svg_bundle.js') {
        const sprites = svgstore(options)
        const iconsDir = path.resolve(inputFolder)
        for (const file of fs.readdirSync(iconsDir)) {
          const filepath = path.join(iconsDir, file)
          const svgid = path.parse(file).name
          const code = fs.readFileSync(filepath, { encoding: 'utf-8' })
          sprites.add(svgid, code)
        }
        const svgoConfig: SvgoConfig = options.svgoConfig || {
          plugins: [
            'cleanupAttrs',
            'removeDoctype',
            'removeComments',
            'removeTitle',
            'removeDesc',
            'removeEmptyAttrs',
            {
              name: 'removeAttrs',
              params: {
                attrs: '(data-name|data-xxx)',
              },
            },
          ],
        }
        const { data: code } = optimize(sprites.toString({ inline: options.inline }), svgoConfig)
        return `
          const div = document.createElement('div')
            div.innerHTML = \`${code}\`
            const svg = div.getElementsByTagName('svg')[0]
            if (svg) {
              svg.style.position = 'absolute'
              svg.style.width = 0
              svg.style.height = 0
              svg.style.overflow = 'hidden'
              svg.setAttribute("aria-hidden", "true")
            }
            // listen dom ready event
            document.addEventListener('DOMContentLoaded', () => {
              if (document.body.firstChild) {
                document.body.insertBefore(div, document.body.firstChild)
              } else {
                document.body.appendChild(div)
              }
            })
          `
      }
      return null
    },
  }
}
  • 声明插件 export const svgoStore = (options: SvgoStoreOptions = {}): Plugin => {}
    • 插件配置属性:
    • inputFolder SVG 图标源文件夹路径
    • svgoConfig SVGO 优化配置,见 svgo 预设插件
  • 忽略类型检查 // @ts-ignore
  • svg_bundle.js 统一存放的图标文件实际上在源代码中是不存在的,而所有图标文件放在 @src/assets/icons 目录下
    • import '@svgoStore' 解析 为 import 'svg_bundle.js' 绕过 vscode 报错
  • 加载 svg_bundle.js 逻辑
    • 使用第三方库 svgstore 来配置获取图标,传入配置选项 options
    • for (const file of fs.readdirSync(iconsDir)) 遍历所有图标路径
    • sprites.add(svgid, code) 加入到大svg文件中
    • 优化svg配置 svgoConfig 缩小 svg 体积
    • 删除部分属性 cleanupAttrs、removeDoctype、removeComments、removeTitle、removeDesc、removeEmptyAttrs、
    • 删除 ata-name 属性
  • 把解构出的代码 const { data: code } = optimize(sprites.toString({ inline: options.inline }), svgoConfig) 变成合法的 svg_bundle.js 文件
    • 使用 div 将 svg 包裹,并将样式属性变为不可见,放到 body

在 vite.config.ts 中引入配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { fileURLToPath, URL, } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import unoCSS from 'unocss/vite'
import jsxScoped from '@10coding/vite-plugin-jsx-scoped'
import { svgoStore } from './src/plugins/svgoStore.ts'

// https://vite.dev/config/
export default defineConfig({
  // base: '/mangosteen-fe-0-publish/',
  plugins: [
    unoCSS(),
    jsxScoped({ warnMultiScopedImport: true }),
    vue(),
    vueJsx({
      transformOn: true,
      mergeProps: true,
    }),
    vueDevTools(),
    svgoStore(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})

在 main.ts 中引入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from '@/router'
import { App } from '@/App.tsx'
import '@/assets/styles/main.scss'
import 'uno.css'
// @ts-ignore
import '@svgoStore'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.mount('#app')
  • 忽略类型检查 // @ts-ignore

安装配置完毕,重启 vite 服务

使用 svg 代替 img ⇧

服务重启后,查看页面元素,body 中多出一个标签

svg-spirits

  • 统一放到一个 svg 中,包含所有分散图标的 symbol 标签
  • 宽高 0,不可见,预加载,只下载一个资源 svg_bundle.js

svg-spirits-div

改写组件 <img /> 标签:<svg><use xlinkHref="#pig"></use></svg>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import type { FunctionalComponent } from 'vue'
import { RouterLink } from 'vue-router'
import wp from '@/modules/welcome/WelcomePage.module.scss'

export const Welcome1stPage: FunctionalComponent = () => (
  <>
    <svg class={wp.logo}>
      <use xlinkHref="#pig"></use>
    </svg>
    <div class={wp.description}>
      <h2>会挣钱</h2>
      <h2>还要会省钱</h2>
    </div>
    <div class={wp['to-next-page']}>
      <RouterLink to="/welcome/2">下一页</RouterLink>
    </div>
  </>
)

Welcome1stPage.displayName = 'Welcome1stPage'
  • 查看页面,修改部分 svg 宽高样式即可

查看打包产物页面是否正常 ⇧

  • 运行 pnpm build
  • 预览 pnpm preview
  • 访问 http://localhost:4173/

补充一点 ⇧

  • 此插件是运行在 node 上,需要补充ts配置
  • tsconfig.node.json

    • TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
    • Include Node.js types and avoid accidentally including other@types/*packages.

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      
      {
      "extends": "@tsconfig/node24/tsconfig.json",
      "include": [
      "vite.config.*",
      "eslint.config.*",
      "src/plugins/**/*"
      ],
      "exclude": [],
      "compilerOptions": {
      "module": "preserve",
      "moduleResolution": "bundler",
      "allowImportingTsExtensions": true,
      "types": ["node"],
      "noEmit": true,
      "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
      }
      }

参考


2. 方案二:使用社区插件 vite-plugin-svg-icons-ng ⇧

注意次插件依赖版本需要 Vite >=5,Node.js 18 / 20 / 22+

安装与配置 ⇧

1
2
3
4
pnpm add -D vite-plugin-svg-icons-ng@1.10.0

# 安装 skill
pnpx skills add yangxu52/vite-plugin-svg-icons-ng # For global installation, add -g

配置 @vite.config.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { fileURLToPath, URL, } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons-ng'

// https://vite.dev/config/
export default defineConfig({
  // base: '/mangosteen-fe-0-publish/',
  plugins: [
    vue(),
    vueJsx({
      transformOn: true,
      mergeProps: true,
    }),
    vueDevTools(),
    createSvgIconsPlugin({
      iconDirs: ['src/assets/icons']
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})

替换组件中 svg 的 xlinkHref

1
2
3
<svg class={wp.logo}>
  <use xlinkHref="#icon-pig"></use>
</svg>

重启vite服务查看编译结果

svg-spirits-ng


封装可复用组件 ⇧

@/components/icons/SvgIcon.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { defineComponent } from 'vue'
import { cn } from 'cn'

export const SvgIcon = defineComponent({
  name: 'SvgIcon',
  // inheritAttrs: false,
  props: {
    name: { type: String, required: true },
  },
  setup(props, _ctx) {
    // const { class: parentClass } = _ctx.attrs
    return () => (
      <svg
        class={cn(
          'h-1em w-1em align-[-0.15em] fill-[currentColor] overflow-clip',
          // parentClass, // 默认即组件外部样式覆盖内部默认样式
        )}
        aria-hidden="true">
        <use xlinkHref={`#icon-${props.name}`}/>
      </svg>
    )
  },
})

替换使用封装图标组件 @/modules/welcome/Welcome1stPage.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import type { FunctionalComponent } from 'vue'
import { RouterLink } from 'vue-router'
import wp from '@/modules/welcome/WelcomePage.module.scss'
import { SvgIcon } from '@/components/icons/SvgIcon.tsx'

export const Welcome1stPage: FunctionalComponent = () => (
  <>
    <SvgIcon class={wp.logo} name="pig"/>
    <div class={wp.description}>
      <h2>会挣钱</h2>
      <h2>还要会省钱</h2>
    </div>
    <div class={wp['to-next-page']}>
      <RouterLink to="/welcome/2">下一页</RouterLink>
    </div>
  </>
)

Welcome1stPage.displayName = 'Welcome1stPage'

组件复用参考


配置 Typescript 支持 ⇧

tsconfig.app.json

1
2
3
4
5
6
7
8
{
  "compilerOptions": {
    "types": [
      "vite/client",
      "vite-plugin-svg-icons-ng/client"
    ]
  }
}

Typescript 支持参考


配置 svgo 优化选项 ⇧


特点 ⇧

  • 开始默认将 SVG Sprite 自动注入 index html,即使不导入虚拟模块也可以获得 SVG Sprite

其他方案参考


·未完待续·

参考文章

相关文章

  • 无

  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名