这是一个提供给Vite作为编译工具时的一个工具,作用是在项目中放入Svg类型的文件,通过在这个工具,转为可以继承父组件的font-size、color等属性的类IconFont来使用。
支持vue和react,下面主要用vue来距离,会提供react时的代码。
主要原理时,在vite编译时,运行新写的脚本,去获取某个文件夹下的svg文件们,把他们的内容都写到页面的body中。
后面在开发时,要使用哪个图标,就使用固定的组件,传入name,name的值就是svg文件的名称。
有两个缺陷:
指定一个路径放置需要处理的Svg图标文件们:src/assets/icons/
。
这个文件夹中的svg文件,在项目运行时,都会被处理。
需要添加两个文件,一个是供 vite.config.ts
中使用的「编译脚本」,一个是用来展示IconFont的图标组件。
这两个文件都放在 src/components/SvgIcon/
文件夹中。
路径:src/components/SvgIcon/localSvg.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
中执行:
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;
路径:src/components/SvgIcon/index.vue
<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>
路径:src/components/SvgIcon/index.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组件的用法,像下面的方式使用后,展示的图标的颜色就是红色,大小就是宽高为24px;
<span class="text-red text-[24px]">
<SvgIcon name="add" />
</span>