本文已参与「新人创作礼」活动,一起开启掘金创作之路。
在日常开发中,图标是大多数开发者都会有需求的,但在一个项目中,我们一般只引入一个 UI 组件库,如 Element 、 Antd ,组件库提供的图标往往是有限的,并不能满足所有的需要。所以在本文中,介绍如何基于 Iconify 二次封装一个 Icon 组件。
Iconify
Iconify 可以访问 80 多个流行的开源图标集,其中有超过 5000 个图标可供选择,基本上能满足了我们项目中的所有需求了。在 Iconify 上,你可以查询到你想要的组件库图标并使用,如 Element 、 Antd。
Icon 组件
安装依赖
首先我们先来安装一下所需要的依赖
pnpm i @iconify/json @purge-icons/generated vite-plugin-purge-icons vite-plugin-svg-icons -D
pnpm i @iconify/iconify -S
| 依赖 | 版本 | 作用 |
|---|---|---|
| @iconify/iconify | 2.2.1 | Iconify 核心库 |
| @iconify/json | 2.1.28 | Iconify 所有图标集 |
| @purge-icons/generated | 0.8.1 | 提取使用的图标名称,然后将图标的数据 (SVG) 捆绑到您的代码中 |
| vite-plugin-purge-icons | 0.8.1 | PurgeIcons 的 vite 插件 |
| vite-plugin-svg-icons | 2.0.1 | 用于本地生成 svg 雪碧图 |
配置
安装完成之后,在 vite.config.ts 中引入 vite-plugin-svg-icons 与 vite-plugin-purge-icons
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import PurgeIcons from 'vite-plugin-purge-icons'
然后在 plugins 字段中添加以下代码
createSvgIconsPlugin({
// 本地 svg 存放路径
iconDirs: [pathResolve('src/assets/svgs')],
symbolId: 'icon-[dir]-[name]',
svgoOptions: true
}),
PurgeIcons()
组件编写
配置好依赖后,接下来,我们就来讲解下如何基于 Iconify 封装 Icon 组件。
首先创建一个 Icon.vue 组件,初始结构如下
<script setup lang="ts">
</script>
<template>
</template>
由于我们项目中都会使用一个 UI 组件库,为了保证与该图标样式一致性,我们会基于该组件库原本的 Icon 组件稍微封装一下,这里我们以 element-plus 为例。
并且 Icon 组件 需要对外暴露三个参数 icon 、 color 、 size,分别对应 图标名称 、 图标颜色 、 图标尺寸 。
<script setup lang="ts">
import { ElIcon } from 'element-plus'
const props = defineProps({
// icon name
icon: {
type: String,
default: ''
},
// icon color
color: {
type: String,
default: ''
},
// icon size
size: {
type: Number,
default: 16
}
})
</script>
<template>
</template>
本地图标
然后我们针对本地的 svg 进行一个处理。
在上一步中,我们通过了 createSvgIconsPlugin 生成了本地的 svg 雪碧图。所以我们在 Icon.vue 中需要添加判断,判断是否是本地图标,这里我们需要约定下规则,props.icon 传入的字符串,如果以 svg-icon: 开头的,代表的就是本地图标,反之,则是 Iconify 图标。
在 Icon.vue 中添加以下代码
<script setup lang="ts">
// ...
import { computed, unref } from 'vue'
// 判断是否是本地图标
const isLocal = computed(() => props.icon.startsWith('svg-icon:'))
// 如果是本地图标拆分出 svg-icon: 后面的字段
const symbolId = computed(() => {
return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : props.icon
})
// ...
</script>
<template>
<ElIcon :size="size" :color="color">
<svg v-if="isLocal" aria-hidden="true">
<use :xlink:href="symbolId" />
</svg>
</ElIcon>
</template>
到这里,本地图标就可以使用并渲染了
<script setup lang="ts">
import Icon from '@/components/Icon.vue'
</script>
<template>
<div>
<Icon icon="svg-icon:peoples" />
<Icon icon="svg-icon:money" />
<Icon icon="svg-icon:message" />
<Icon icon="svg-icon:shopping" />
</div>
</template>
Iconify 图标
处理完本地 svg 图标后,我们还需要处理下 Iconify 。
在 Icon.vue 中添加以下代码
<script setup lang="ts">
// ...
import { computed, unref, ref, watch, nextTick } from 'vue'
import Iconify from '@purge-icons/generated'
// ...
const elRef = ref<ElRef>(null)
// 设置 Iconify 样式
const getIconifyStyle = computed(() => {
const { color, size } = props
return {
fontSize: `${size}px`,
color
}
})
// 更新 Iconify
const updateIcon = async (icon: string) => {
if (unref(isLocal)) return
const el = unref(elRef)
if (!el) return
await nextTick()
if (!icon) return
const svg = Iconify.renderSVG(icon, {})
if (svg) {
el.textContent = ''
el.appendChild(svg)
} else {
const span = document.createElement('span')
span.className = 'iconify'
span.dataset.icon = icon
el.textContent = ''
el.appendChild(span)
}
}
watch(
() => props.icon,
(icon: string) => {
updateIcon(icon)
}
)
</script>
<template>
<ElIcon :class="prefixCls" :size="size" :color="color">
// ...
<span v-else ref="elRef" :class="$attrs.class" :style="getIconifyStyle">
<span class="iconify" :data-icon="symbolId"></span>
</span>
</ElIcon>
</template>
由于我们 Iconify 的渲染方式是插入到元素中的,所以当 props.icon 变化的时候,是没办法监听到并重新渲染的,所以我们需要 watch 下 props.icon 的变化,并执行 updateIcon 函数重新渲染 Iconify 。
然后我们看下效果
<script setup lang="ts">
import Icon from '@/components/Icon.vue'
</script>
<template>
<div>
<Icon icon="ep:aim" />
<Icon icon="ep:alarm-clock" />
<Icon icon="ep:baseball" />
<Icon icon="ep:chat-line-round" />
</div>
</template>
这样我们就完成了 Iconify 图标的渲染了。
总结
后续如果需要其他组件库的图标,可以在 Iconify 上搜索,然后复制想要的图标名称,传入到 Icon 组件即可。
文章相关代码,可查看 vue-element-plus-admin