Vue3 组件库实战(五):Icon 图标组件的设计与实现

0 阅读8分钟

请添加图片描述

Vue3 组件库实战:Icon 图标组件的设计与实现

本文将带你深入理解一个企业级 Icon 组件的设计思路和实现细节,适合 Vue 3 初学者阅读。

📖 目录


为什么需要 Icon 组件

在现代 Web 应用中,图标无处不在:按钮上的勾选图标、导航栏的菜单图标、提示信息的警告图标等等。如果每次使用图标都要手写 SVG 代码或者引入图片,会带来以下问题:

  1. 代码冗余:每个地方都要复制粘贴相同的 SVG 代码
  2. 维护困难:如果要统一修改图标样式,需要改很多地方
  3. 不够灵活:很难动态控制图标的大小、颜色等属性
  4. 不够规范:团队成员可能使用不同来源的图标,导致风格不统一

因此,我们需要一个统一的 Icon 组件来解决这些问题。


组件设计思路

我们的 Icon 组件基于以下设计原则:

1. 简单易用

<!-- 只需要一个 name 属性就能使用图标 -->
<MyIcon name="check" />

2. 高度可定制

<!-- 支持自定义大小和颜色 -->
<MyIcon name="home" :size="24" color="#409eff" />

3. 扩展性强

<!-- 如果内置图标不够用,可以通过插槽自定义 -->
<MyIcon :size="24">
  <svg><!-- 自定义 SVG --></svg>
</MyIcon>

核心功能实现

让我们逐步拆解这个组件的实现,看看每一部分是如何工作的。

第一步:定义组件属性(Props)

const props = defineProps({
  // 图标名称
  name: {
    type: String as PropType<string>,
    default: undefined,
  },
  // 图标大小,支持数字(px)或字符串(如 '2em')
  size: {
    type: [Number, String] as PropType<number | string>,
    default: undefined,
  },
  // 图标颜色
  color: {
    type: String,
    default: undefined,
  },
})

解释:

  • name:用户通过这个属性指定要显示哪个图标,比如 "check""home"
  • size:控制图标大小,可以传数字(会自动加 px 单位)或字符串(如 "2em"
  • color:控制图标颜色,支持任何 CSS 颜色值(如 "#409eff""red" 等)

为什么 size 要支持两种类型?

  • 传数字更方便:<MyIcon :size="24" />
  • 传字符串更灵活:<MyIcon size="2em" /> 可以使用相对单位

第二步:创建图标映射表

// 首先从 Ant Design Icons 导入需要的图标
import {
  CheckOutlined,
  CloseOutlined,
  InfoCircleOutlined,
  SearchOutlined,
  // ... 更多图标
} from '@ant-design/icons-vue'

// 创建一个映射表,将简单的名称映射到实际的图标组件
const iconMap: Record<string, Component> = {
  'check': CheckOutlined,
  'close': CloseOutlined,
  'info': InfoCircleOutlined,
  'search': SearchOutlined,
  'user': UserOutlined,
  'setting': SettingOutlined,
  'home': HomeOutlined,
  'delete': DeleteOutlined,
  'edit': EditOutlined,
  'plus': PlusOutlined,
  'minus': MinusOutlined,
  'up': UpOutlined,
  'down': DownOutlined,
  'left': LeftOutlined,
  'right': RightOutlined,
  'loading': LoadingOutlined,
  'check-circle': CheckCircleOutlined,
  'close-circle': CloseCircleOutlined,
  'exclamation-circle': ExclamationCircleOutlined,
  'warning': WarningOutlined,
}

解释:

这个映射表是整个组件的核心!它的作用是:

  1. 简化使用:用户只需要记住简单的名称(如 "check"),而不需要记住完整的组件名(CheckOutlined
  2. 统一管理:所有可用的图标都在这里定义,方便维护和扩展
  3. 类型安全:使用 TypeScript 的 Record<string, Component> 类型,确保映射的值都是 Vue 组件

什么是 Record 类型?

Record<string, Component> 是 TypeScript 的一个工具类型,表示:

  • 键(key)是字符串类型
  • 值(value)是 Component 类型(Vue 组件)

相当于:

{
  [key: string]: Component
}

第三步:计算图标样式

const iconStyle = computed<CSSProperties>(() => {
  const style: CSSProperties = {}

  if (props.size) {
    // 如果是数字,添加 px 单位;否则直接使用字符串值
    style.fontSize
      = typeof props.size === 'number' ? `${props.size}px` : props.size
  }

  if (props.color) {
    style.color = props.color
  }

  return style
})

解释:

这是一个计算属性(computed),它会根据 props 动态生成 CSS 样式对象。

为什么使用 computed?

  1. 响应式:当 props.sizeprops.color 变化时,样式会自动更新
  2. 缓存:只有依赖的数据变化时才重新计算,提高性能
  3. 类型安全:使用 CSSProperties 类型,确保生成的样式对象符合 CSS 规范

代码逻辑详解:

// 1. 创建一个空的样式对象
const style: CSSProperties = {}

// 2. 如果用户传了 size 属性
if (props.size) {
  // 判断 size 是数字还是字符串
  style.fontSize = typeof props.size === 'number'
    ? `${props.size}px`  // 数字:24 → "24px"
    : props.size         // 字符串:直接使用 "2em"
}

// 3. 如果用户传了 color 属性
if (props.color) {
  style.color = props.color  // 直接设置颜色
}

// 4. 返回最终的样式对象
return style

为什么用 fontSize 控制图标大小?

因为 Ant Design Icons 是基于字体图标(Icon Font)的原理,图标的大小由 font-size 控制,颜色由 color 控制。

第四步:获取对应的图标组件

const iconComponent = computed(() => {
  if (props.name && iconMap[props.name]) {
    return iconMap[props.name]
  }
  return null
})

解释:

这也是一个计算属性,用于根据用户传入的 name 查找对应的图标组件。

代码逻辑:

  1. 检查用户是否传了 name 属性
  2. 检查 iconMap 中是否存在这个名称的图标
  3. 如果都满足,返回对应的图标组件
  4. 否则返回 null(表示没有找到图标)

为什么要返回 null?

因为在模板中,我们会根据 iconComponent 是否为 null 来决定是渲染图标还是使用插槽内容。

第五步:渲染模板

<template>
  <span :class="ns.b()" :style="iconStyle">
    <!-- 如果指定了 name 属性,渲染对应的 Ant Design 图标 -->
    <component :is="iconComponent" v-if="iconComponent" />
    <!-- 否则使用插槽,允许自定义图标内容 -->
    <slot v-else />
  </span>
</template>

解释:

这是组件的渲染逻辑,让我们逐行分析:

1. 外层容器
<span :class="ns.b()" :style="iconStyle">
  • 使用 <span> 作为容器(行内元素,不会独占一行)
  • :class="ns.b()" 是 BEM 命名规范的工具函数,会生成类名 my-icon
  • :style="iconStyle" 应用我们计算好的样式(大小和颜色)
2. 动态组件渲染
<component :is="iconComponent" v-if="iconComponent" />

这是 Vue 的动态组件语法:

  • <component :is="xxx" /> 可以动态渲染不同的组件
  • v-if="iconComponent" 只有当找到对应图标时才渲染
  • 相当于:如果用户传了 name="check",就渲染 <CheckOutlined /> 组件

为什么不直接写 <CheckOutlined />

因为我们不知道用户会传什么 name,需要根据 name 动态决定渲染哪个图标组件。

3. 插槽后备内容
<slot v-else />
  • <slot /> 是 Vue 的插槽语法,允许用户传入自定义内容
  • v-else 表示:如果没有找到对应的图标(iconComponentnull),就使用插槽内容

使用场景:

<!-- 场景 1:使用内置图标 -->
<MyIcon name="check" />  <!-- 渲染 CheckOutlined -->

<!-- 场景 2:使用自定义图标 -->
<MyIcon :size="24">
  <svg><!-- 自定义 SVG --></svg>
</MyIcon>  <!-- 渲染插槽内容 -->

使用示例

基础用法

<template>
  <!-- 最简单的用法 -->
  <MyIcon name="check" />

  <!-- 设置大小 -->
  <MyIcon name="home" :size="24" />

  <!-- 设置颜色 -->
  <MyIcon name="user" color="#409eff" />

  <!-- 同时设置大小和颜色 -->
  <MyIcon name="setting" :size="32" color="red" />
</template>

在按钮中使用

<template>
  <button>
    <MyIcon name="check" :size="16" />
    <span>确认</span>
  </button>

  <button>
    <MyIcon name="close" :size="16" />
    <span>取消</span>
  </button>
</template>

<style scoped>
button {
  display: flex;
  align-items: center;
  gap: 8px;
}
</style>

使用自定义图标

<template>
  <MyIcon :size="24" color="#67c23a">
    <svg viewBox="0 0 1024 1024" fill="currentColor">
      <path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448..." />
    </svg>
  </MyIcon>
</template>

注意: 自定义 SVG 时,使用 fill="currentColor" 可以让图标继承父元素的 color 属性。

动态切换图标

<script setup>
import { ref } from 'vue'

const isVisible = ref(false)
</script>

<template>
  <button @click="isVisible = !isVisible">
    <MyIcon :name="isVisible ? 'up' : 'down'" />
    <span>{{ isVisible ? '收起' : '展开' }}</span>
  </button>
</template>

最佳实践

1. 统一图标大小

在实际项目中,建议定义统一的图标大小规范:

// constants.ts
export const ICON_SIZE = {
  SMALL: 16,
  MEDIUM: 20,
  LARGE: 24,
  XLARGE: 32,
}
<template>
  <MyIcon name="check" :size="ICON_SIZE.MEDIUM" />
</template>

2. 使用语义化的颜色

// theme.ts
export const ICON_COLOR = {
  PRIMARY: '#409eff',
  SUCCESS: '#67c23a',
  WARNING: '#e6a23c',
  DANGER: '#f56c6c',
  INFO: '#909399',
}
<template>
  <MyIcon name="check-circle" :color="ICON_COLOR.SUCCESS" />
  <MyIcon name="close-circle" :color="ICON_COLOR.DANGER" />
</template>

3. 封装常用图标组合

<!-- SuccessIcon.vue -->
<template>
  <MyIcon name="check-circle" :size="20" color="#67c23a" />
</template>

<!-- ErrorIcon.vue -->
<template>
  <MyIcon name="close-circle" :size="20" color="#f56c6c" />
</template>

4. 添加无障碍支持

<template>
  <MyIcon
    name="delete"
    role="img"
    aria-label="删除"
  />
</template>
  • role="img":告诉屏幕阅读器(如视障用户使用的读屏软件)这个元素是一个图标,而非普通文本或装饰性元素。
  • aria-label="删除":为图标提供文字描述。因为图标本身没有文字内容,屏幕阅读器读到该元素时会朗读"删除",帮助视障用户理解图标的含义。
  • 由于组件使用了 <script setup>,Vue 3 会自动将未声明的 attrs(如 rolearia-label)透传到根元素 <span> 上,无需额外处理。

技术要点总结

1. TypeScript 类型定义

// PropType 用于定义 props 的类型
type: String as PropType<string>
type: [Number, String] as PropType<number | string>

// CSSProperties 用于定义 CSS 样式对象的类型
const style: CSSProperties = {}

// Record 用于定义对象映射的类型
const iconMap: Record<string, Component> = {}

2. Vue 3 Composition API

// computed:计算属性,自动缓存和响应式更新
const iconStyle = computed(() => { /* ... */ })

// defineProps:定义组件属性
const props = defineProps({ /* ... */ })

// defineOptions:定义组件选项(如 name)
defineOptions({ name: 'MyIcon' })

3. 动态组件渲染

<!-- 根据变量动态渲染不同的组件 -->
<component :is="iconComponent" />

4. 插槽(Slot)

<!-- 允许父组件传入自定义内容 -->
<slot />

5. 条件渲染

<!-- v-if 和 v-else 实现条件渲染 -->
<component :is="iconComponent" v-if="iconComponent" />
<slot v-else />

扩展思考

如何添加新图标?

只需要在 iconMap 中添加新的映射:

import { SmileOutlined } from '@ant-design/icons-vue'

const iconMap: Record<string, Component> = {
  // ... 现有图标
  'smile': SmileOutlined,  // 添加新图标
}

如何支持图标旋转动画?

可以添加一个 spin 属性:

const props = defineProps({
  // ... 现有属性
  spin: {
    type: Boolean,
    default: false,
  },
})
<template>
  <span
    :class="[ns.b(), { 'is-spin': spin }]"
    :style="iconStyle"
  >
    <!-- ... -->
  </span>
</template>

<style>
.my-icon.is-spin {
  animation: icon-spin 1s linear infinite;
}

@keyframes icon-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}
</style>

如何支持图标点击事件?

组件本身不需要处理,父组件直接绑定即可:

<MyIcon name="delete" @click="handleDelete" />

Vue 会自动将事件绑定到组件的根元素(<span>)上。


总结

通过这个 Icon 组件的实现,我们学到了:

  1. 组件设计原则:简单易用、高度可定制、扩展性强
  2. TypeScript 类型系统:PropType、CSSProperties、Record 等类型的使用
  3. Vue 3 核心特性:Composition API、computed、动态组件、插槽
  4. 工程化思维:通过映射表统一管理图标,提高可维护性

这个组件虽然代码不多(约 110 行),但包含了很多实用的设计模式和最佳实践,非常适合作为学习 Vue 3 组件开发的案例。

希望这篇文章能帮助你更好地理解组件的设计与实现!


相关资源