编程崽

登录

一叶在编程苦海沉沦的扁舟之上,我是那只激情自射的崽

Svg转IconFont

Svg转IconFont

这是一个提供给Vite作为编译工具时的一个工具,作用是在项目中放入Svg类型的文件,通过在这个工具,转为可以继承父组件的font-size、color等属性的类IconFont来使用。

支持vue和react,下面主要用vue来距离,会提供react时的代码。

主要原理时,在vite编译时,运行新写的脚本,去获取某个文件夹下的svg文件们,把他们的内容都写到页面的body中。

后面在开发时,要使用哪个图标,就使用固定的组件,传入name,name的值就是svg文件的名称。

有两个缺陷:

  • 目标文件夹下的svg文件们都会一次性加载到body下,不用时看不到,但是会占空间,所以只适合图标文件的svg,体积小,影响小。
  • 每次在目标文件夹下内增删了svg文件,都需要重启一下项目。

创建好放置Svg文件的路径

指定一个路径放置需要处理的Svg图标文件们:src/assets/icons/

这个文件夹中的svg文件,在项目运行时,都会被处理。

添加主要文件

需要添加两个文件,一个是供 vite.config.ts 中使用的「编译脚本」,一个是用来展示IconFont的图标组件。

这两个文件都放在 src/components/SvgIcon/ 文件夹中。

编译脚本内容

路径:src/components/SvgIcon/localSvg.ts

ts 复制代码
// @ts-nocheck
import { readFileSync, readdirSync } from 'fs';
import path from 'path';

let idPerfix = '';
const iconNames: string[] = [];
const svgTitle = /<svg([^>+].*?)>/;
const clearHeightWidth = /(width|height)="([^>+].*?)"/g;
const hasViewBox = /(viewBox="[^>+].*?")/g;
const clearReturn = /(\r)|(\n)/g;
const replaceFill = /(fill="[^>+].*?")/g; // 重置 svg 的 fill

function findSvgFile(dir: string): string[] {
  const svgRes = [] as any;
  const dirents = readdirSync(dir, {
    withFileTypes: true,
  });
  for (const dirent of dirents) {
    if (!dirent.name.toLowerCase().endsWith('.svg')) continue
    iconNames.push(`${idPerfix}-${dirent.name.replace('.svg', '')}`);
    let fullPath = path.join(dir, dirent.name)
    if (dirent.isDirectory()) {
      svgRes.push(...findSvgFile(fullPath));
    } else {
      let svgStr = readFileSync(fullPath).toString()

      // 是否存在 fill 属性
      const hasFill = replaceFill.test(svgStr)
      // 如果存在,直接 replace
      if (hasFill) {
        svgStr = svgStr.replaceAll(replaceFill, 'fill="currentColor"')
      }

      svgStr = svgStr.replace(clearReturn, '')
        .replace(svgTitle, (_: string, $2: string) => {
          let width = 0;
          let height = 0;
          let content = $2.replace(clearHeightWidth, (_: string, s2: string, s3: number) => {
            if (s2 === 'width') {
              width = s3;
            } else if (s2 === 'height') {
              height = s3;
            }
            return '';
          });
          if (!hasViewBox.test($2)) {
            content += `viewBox="0 0 ${width} ${height}"`
          }
          if (!hasFill) {
            content += ` fill="currentColor"`
          }
          return `<symbol id="${idPerfix}-${path.basename(fullPath, '.svg')}" ${content}>`;
        })
        .replace('</svg>', '</symbol>');
      svgRes.push(svgStr);
    }
  }
  return svgRes;
}

export const svgBuilder = (path: string, perfix = 'local') => {
  if (perfix) {
    idPerfix = perfix;
  }
  const svgRes = findSvgFile(path);
  return {
    name: 'svg-transform',
    transformIndexHtml(html: string) {
      return html.replace(
        '<body>',
        `
          <body>
            <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" style="position: absolute; width: 0; height: 0; overflow: hidden; display: none;">
              <defs>
                ${svgRes.join('')}
              </defs>
            </svg>
        `,
      );
    },
  };
};

编译脚本用法

需要在 vite.config.ts 中引入,然后放置到 plugins 中执行:

ts 复制代码
import { defineConfig, ConfigEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import { svgBuilder } from './src/components/SvgIcon/localSvg'

const viteConfig = defineConfig((mode: ConfigEnv) => {
  return {
    plugins: [
      vue(),
      // 下面这个是引入的脚本的使用方式,指定让这个脚本去处理 src/assets/icons 路径下的svg文件们
      svgBuilder(path.resolve(process.cwd(), 'src/assets/icons')),
    ],
  }
})

export default viteConfig;

图标组件的内容-vue

路径:src/components/SvgIcon/index.vue

sh 复制代码
<template>
  <svg class="flex-none box-content" :width="size || '1em'" :height="size || '1em'">
    <use :href="`#local-${name}`" />
  </svg>
</template>

<script setup lang="ts">
defineProps<{
  name: string, // svg 图标组件名字
  size?: string | number,
}>()

</script>

图标组件的内容-react

路径:src/components/SvgIcon/index.tsx

tsx 复制代码
import clsx from 'clsx';
import React from 'react';

type PropsBase = {
  className?: string,
  style?: React.CSSProperties
  children?: React.ReactNode,
  onClick?: (e: any) => void,
  onMouseEnter?: (e: any) => void,
  onMouseLeave?: (e: any) => void,
}

const SvgIcon: React.FC<PropsBase & {
  name: string, // svg 图标组件名字
  size?: string | number,
}> = ({ className, name, size, onClick }) => {

  return (
    <svg onClick={(e) => onClick?.(e)} className={clsx('flex-none box-content', className)} width={size || '1em'} height={size || '1em'}>
      <use href={`#local-${name}`} />
    </svg>
  );
};

export default SvgIcon; 

图标组件的用法-vue

用法就是普通vue组件的用法,像下面的方式使用后,展示的图标的颜色就是红色,大小就是宽高为24px;

sh 复制代码
<span class="text-red text-[24px]">
  <SvgIcon name="add" />
</span>