Vue3 + Vite 封装 Icon 组件

4,835 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

在日常开发中,图标是大多数开发者都会有需求的,但在一个项目中,我们一般只引入一个 UI 组件库,如 ElementAntd ,组件库提供的图标往往是有限的,并不能满足所有的需要。所以在本文中,介绍如何基于 Iconify 二次封装一个 Icon 组件。

Iconify

Iconify 可以访问 80 多个流行的开源图标集,其中有超过 5000 个图标可供选择,基本上能满足了我们项目中的所有需求了。在 Iconify 上,你可以查询到你想要的组件库图标并使用,如 ElementAntd

Icon 组件

安装依赖

首先我们先来安装一下所需要的依赖

pnpm i @iconify/json @purge-icons/generated vite-plugin-purge-icons vite-plugin-svg-icons -D
pnpm i @iconify/iconify -S
依赖版本作用
@iconify/iconify2.2.1Iconify 核心库
@iconify/json2.1.28Iconify 所有图标集
@purge-icons/generated0.8.1提取使用的图标名称,然后将图标的数据 (SVG) 捆绑到您的代码中
vite-plugin-purge-icons0.8.1PurgeIcons 的 vite 插件
vite-plugin-svg-icons2.0.1用于本地生成 svg 雪碧图

配置

安装完成之后,在 vite.config.ts 中引入 vite-plugin-svg-iconsvite-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 组件 需要对外暴露三个参数 iconcolorsize,分别对应 图标名称图标颜色图标尺寸

<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>

1650261574(1).png

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 变化的时候,是没办法监听到并重新渲染的,所以我们需要 watchprops.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>

1650263284(1).png

这样我们就完成了 Iconify 图标的渲染了。

总结

后续如果需要其他组件库的图标,可以在 Iconify 上搜索,然后复制想要的图标名称,传入到 Icon 组件即可。

文章相关代码,可查看 vue-element-plus-admin