前言
本文部分灵感来自vbenjs-admin
svg icon
拥有很多优秀的特性,在我司的项目中也大量使用了这种icon
方案,本文就特地将我在使用中的一些经验分享给大家,希望对大家有所帮助。
安装 vite-plugin-svg-icons
shell
pnpm add vite-plugin-svg-icons fast-glob -D
安装 fast-glob
的原因是 vite-plugin-svg-icons
依赖了它。
然后配置vite.config.ts
js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
import path from "path";
export default defineConfig({
plugins: [
vue(),
createSvgIconsPlugin({
// 指定目录
iconDirs: [path.resolve(process.cwd(), "src/icons")],
// 使用svg图标的格式
symbolId: "icon-[dir]-[name]",
}),
],
});
然后需要在src
目录下创建icons
目录,我们在项目中使用的svg
图标都放在这里面,这里我们放三个图标 home.svg
、 address.svg
、 setting.svg
的图标进去。
然后在入口处引入插件
ts
// main.ts
import "virtual:svg-icons-register";
然后我们在模板中就可以引用了
html
<template>
<div>
<svg>
<use xlink:href="#icon-home"></use>
</svg>
</div>
</template>
到这里可以看到 svg-icon
已经成功渲染出来了:
封装 SvgIcon 组件
目前我们使用的方式还比较原始,为了方便使用,我们将他们封装起来。
在 svg/components
目录下面创建一个文件SvgIcon.vue
html
<template>
<svg
:style="{ width: sizeRef + 'px', height: sizeRef + 'px' }"
class="svg-icon-wrapper"
>
<use :xlink:href="prefix + name" :fill="colorRef"></use>
</svg>
</template>
<script setup lang="ts">
import { computed } from "vue";
defineOptions({
name: "SvgIcon",
});
export type IconSize = "default" | "small" | "large";
export type IconColor =
| "primary"
| "default"
| "success"
| "warn"
| "error"
| (string & {});
const props = withDefaults(
defineProps<{
/** icon 的前缀 */
prefix?: string;
/** icon 名称 */
name: string;
/** icon 的颜色 */
color?: IconColor;
/** icon 的尺寸 */
size?: IconSize | number;
}>(),
{
prefix: "#icon-",
size: "default",
color: "default",
}
);
const sizeMap: Record<IconSize, number> = {
default: 16,
small: 12,
large: 24,
};
const colorMap: Record<IconColor, string> = {
primary: "#409EFF",
success: "#67C23A",
error: "#bb1b1b",
warn: "#F56C6C",
default: "#333333",
};
const colorRef = computed(() => {
return colorMap[props.color] || props.color;
});
const sizeRef = computed(() => {
if (typeof props.size === "string") {
return sizeMap[props.size];
}
return props.size;
});
</script>
<style>
.svg-icon-wrapper {
display: inline-block;
}
</style>
这个组件的封装了一些常用功能,并且设置了一些内置的值,让我们可以更好地统一写法,比如 size
和 color
, 这里需要提一句的是color
的类型定义:
ts
export type IconColor = "primary" | "default" | "success" | "warn" | "error" | (string & {});
这里的意思这 color
有几个预设值,但是也接受其它string
类型的值,这里大家可能会问为什么不写 string
,答案是这样写可以有类型提示,看图:
这里是一个小技巧,感兴趣的同学可以查一下 typescript
的相关资料。
然后再同级目录下创建一个 index.ts
,创建一个插件,将组件导出去
ts
// src/components/SvgIcon/index.ts
import type { App } from "vue";
import SvgIcon from "./SvgIcon.vue";
/**
* 让 SvgIcon 具有类型提示
*/
declare module "vue" {
export interface GlobalComponents {
SvgIcon: typeof SvgIcon;
}
}
const SvgIconPlugin = (app: App) => {
app.component(SvgIcon.name, SvgIcon);
};
export { SvgIcon, SvgIconPlugin };
这里需要注意,由于我们要将组件注册在全局,所以这里要拓展一下类型,即:
ts
/** * 让 SvgIcon 具有类型提示 */
declare module "vue" {
export interface GlobalComponents {
SvgIcon: typeof SvgIcon;
}
}
然后在入口文件引用
ts
import { SvgIconPlugin } from "./components/SvgIcon";
createApp(App).use(SvgIconPlugin).mount("#app");
我们在 App.vue
文件中使用一下:
html
<template>
<div class="wrapper">
<SvgIcon name="home" color="primary" size="large"></SvgIcon>
<SvgIcon name="setting" color="success" size="small"></SvgIcon>
<SvgIcon name="address" color="error" :size="30"></SvgIcon>
</div>
</template>
下面是效果:
进一步使用会发现 ts
提示也很友好
到这里,我们就已经编写了一个很好用的 SvgIcon
组件了。
编写自定义 vite 插件,增强 name 字段类型提示
上面的代码其实已经比较优雅了,但是还是有可以进一步优化的点,那就是 name
字段的类型提示,我们可以看到,由于 name
的类型签名是string
,导致我们没法得到输入的建议;
我们希望它能提示我们输入home setting address
,然而现实却不尽如人意。 对于一个成熟的typescript
爱好者而言,这是万万不可的,那么有没有办法去优化这一块呢?
答案是有的,由于src/icons
下面的目录都是些静态文件,我们可以通过文件系统读取到这些文件,并且把他们写到一个 d.ts
文件中,并且把它暴露到全局,这样一来我们就可以用到它了。
vite
插件开发不是本文的重点,笔者也正在学习这一块,所以这里大家直接参考我的代码就可以了。
我们在根目录下创建一个 vite/vite-plugin-icon-dts.ts
文件,
把这个文件放在 tsconfig.json
的 include
选项中(有可能你的项目已经按照一些规则包含了这个文件,所以这里不是必要的);
然后在这个文件中编写插件代码:
先安装一个依赖
ts
pnpm add fs-extra -D
ts
// vite/vite-plugin-icon-dts.ts
import fs from "fs-extra";
import glob from "fast-glob";
import path from "path";
const PLUGIN_NAME = "vite-plugin-icon-dts";
const error = (...args: any[]) => {
console.error(`${PLUGIN_NAME}: `, ...args);
};
function debounce(fn: () => void, wait: number) {
let timer: any = null;
return function () {
if (timer !== null) {
clearTimeout(timer);
}
timer = setTimeout(fn, wait);
};
}
// glob 默认只支持 / 作为路径分隔符,windows 下会出现问题
const normalizePath = (path: string) => path.replace(/\\/g, "/");
interface IconDtsOptions {
/** 监听的目录 */
directory: string;
/** 输出的dts文件 */
dts: string;
/** 监听变化的延迟时间 */
delay: number;
/** 接口名称 */
interfaceName: string;
}
const defualtOptions: IconDtsOptions = {
directory: "src/icons/",
dts: "icons-dts.d.ts",
delay: 200,
interfaceName: "ISvgIconPath",
};
export const iconDts = (options: Partial<IconDtsOptions> = {}) => {
const finalOptions: IconDtsOptions = Object.assign(
{},
defualtOptions,
options
);
const { delay, interfaceName } = finalOptions;
let { directory, dts } = finalOptions;
directory = normalizePath(directory);
dts = normalizePath(dts);
return {
name: PLUGIN_NAME,
configureServer: async () => {
if (!fs.existsSync(directory)) {
error(`directory ${directory} not exist, please check`);
return;
}
const update = () => {
let assets: any = glob.sync(`${directory}/**/*.svg`, {});
assets = assets.map((i: string) => i.replace(directory, ""));
assets = assets.map((i: string) => i.replace(".svg", ""));
assets = assets.map((i: string) => i.replace("/", "-"));
let output = `/* prettier-ignore-start */\n/* tslint:disable */\n/* eslint-disable */\ninterface ${interfaceName} {\n`;
for (let i = 0; i < assets.length; i++) {
const pic = assets[i];
output += ` '${pic}': string;\n`;
}
output += `}\n/* prettier-ignore-end */\n`;
const base = path.dirname(dts);
fs.ensureDirSync(base);
fs.writeFileSync(dts, output);
};
const debounceLogic = debounce(update, delay);
// 监听到文件变化,就重新写一遍
fs.watch(directory, { recursive: true }, () => {
debounceLogic();
});
update();
},
};
};
核心思路就是在 vite
启动的时候生成 dts
文件,后续再监听该目录,持续更新生成最新的代码。
不出意外的话,你会得到一个文件icons-dts.d.ts
文件
然后我们把这个文件同样加入到 tsconfig.json
的 include
选项中。
这样一来,我们就可以在 ts
的类型空间中,得到一个 ISvgIconPath
的类型,接下来我们去处理我们 SvgIcon
文件。
ts
export type IconName = keyof ISvgIconPath | (string & {});
{
...
/** icon 名称 */
name: IconName;
...
}
然后我们看看效果
在有文件夹嵌套的情况也能正常工作
完美!!!
总结
本文手把手带大家封装了一个实用的 SvgIcon
组件,能满足基础的功能,而且类型提示也足够智能,在这个基础上可以加以拓展,封装出更好用的组件,非常具有学习的意义,希望对大家有所帮助。
感兴趣的同学可以点击这里获取到完整的代码,
欢迎大家关注我的其它文章,后续会持续更新 vue3 +ts
的相关知识
码字不易,如果觉得有用的话,点个👍👍👍再走吧!