Skip to content

图标类型生成

项目使用自定义 Vite 插件自动扫描图标 JSON 文件,生成 TypeScript 类型定义,为图标系统提供完整的类型安全和智能提示支持。

概述

图标类型生成是整个图标系统的基础,它解决了以下核心问题:

  • 类型安全:所有图标使用都经过 TypeScript 类型检查
  • 智能提示:IDE 自动补全所有可用图标
  • 运行时验证:提供工具函数验证图标代码有效性
  • 多源整合:统一 Iconfont 和 Iconify 两种图标源

核心特性

  • 自动化生成:Vite 插件在构建时自动生成类型
  • 热更新支持:开发模式下修改 JSON 即时重新生成
  • 递归扫描:支持多层目录结构
  • 去重机制:自动处理重复图标代码
  • 工具函数:提供图标验证、搜索等实用函数

核心插件

iconfont-types.ts

项目的类型生成由 vite/plugins/iconfont-types.ts 插件实现。

typescript
// vite/plugins/iconfont-types.ts
import type { Plugin } from 'vite'
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs'
import { join, resolve, dirname, relative } from 'node:path'

// 图标项接口定义
interface IconItem {
  code: string
  name: string
}

// Iconify 图标项扩展接口
interface IconifyIconItem extends IconItem {
  value: string // iconify 图标的 CSS 类名
}

// Iconfont JSON 中的图形定义
interface IconfontGlyph {
  font_class: string
  name: string
  unicode: string
  unicode_decimal: number
}

// Iconify JSON 中的图标定义
interface IconifyIcon {
  code: string
  name: string
  value: string
}

// Iconfont JSON 文件格式
interface IconfontJson {
  glyphs: IconfontGlyph[]
}

// Iconify JSON 文件格式
interface IconifyJson {
  type: 'iconify'
  icons: IconifyIcon[]
}

插件工作原理

插件在 Vite 构建的不同生命周期执行类型生成:

typescript
export default (): Plugin => {
  const iconsDir = 'src/assets/icons'      // 图标源目录
  const outputPath = 'src/types/icons.d.ts' // 输出文件路径
  let projectRoot = ''                      // 项目根目录

  return {
    name: 'iconfont-types',

    // 获取项目配置,确定项目根目录
    configResolved(config) {
      projectRoot = config.root
    },

    // 构建开始时生成类型
    buildStart() {
      writeTypeFile()
    },

    // 开发模式热更新支持
    handleHotUpdate({ file }) {
      const iconsPath = resolve(projectRoot, iconsDir)

      // 监听图标目录下的 JSON 文件变化
      if (file.startsWith(iconsPath) && file.endsWith('.json')) {
        console.log(`检测到图标文件变化: ${relative(projectRoot, file)}`)
        writeTypeFile()
      }
    }
  }
}

插件生命周期

生命周期触发时机执行操作
configResolvedVite 配置解析完成获取项目根目录
buildStart开发服务器启动 / 生产构建开始生成类型文件
handleHotUpdate文件变化触发热更新检测图标 JSON 变化并重新生成

数据源

1. Iconfont JSON 格式

Iconfont 图标来自阿里巴巴图标库导出的 JSON 文件。

文件位置: src/assets/icons/system/iconfont.json

json
{
  "id": "5022572",
  "name": "plus-ui",
  "font_family": "iconfont",
  "css_prefix_text": "icon-",
  "description": "ruoyi-plus-uniapp图标库",
  "glyphs": [
    {
      "icon_id": "25331853",
      "name": "电梯",
      "font_class": "elevator3",
      "unicode": "e8b9",
      "unicode_decimal": 59577
    },
    {
      "icon_id": "32102715",
      "name": "电量",
      "font_class": "battery",
      "unicode": "e6e5",
      "unicode_decimal": 59109
    },
    {
      "icon_id": "16322949",
      "name": "漂流瓶",
      "font_class": "floating-bottle",
      "unicode": "e7eb",
      "unicode_decimal": 59371
    }
  ]
}

字段说明

字段类型说明
idstring图标项目 ID
namestring图标项目名称
font_familystring字体族名称
css_prefix_textstringCSS 类名前缀
descriptionstring项目描述
glyphsarray图标列表

glyph 字段说明

字段类型说明
icon_idstring图标唯一 ID
namestring图标中文名称
font_classstringCSS 类名(不含前缀)
unicodestringUnicode 编码(十六进制)
unicode_decimalnumberUnicode 编码(十进制)

2. Iconify JSON 格式

Iconify 图标使用自定义 JSON 格式,包含图标代码、名称和实际 CSS 类名的映射。

文件位置: src/assets/icons/iconify/preset.json

json
{
  "name": "iconify-preset",
  "description": "iconify 预设图标集合",
  "type": "iconify",
  "icons": [
    {
      "code": "button",
      "name": "按钮",
      "value": "i-fluent:button-16-regular"
    },
    {
      "code": "cascader",
      "name": "级联选择",
      "value": "i-ion:ios-arrow-dropdown"
    },
    {
      "code": "dashboard",
      "name": "仪表盘",
      "value": "i-tabler:layout-dashboard"
    }
  ]
}

字段说明

字段类型说明
namestring预设集合名称
descriptionstring集合描述
type"iconify"类型标识,用于区分 Iconfont
iconsarray图标列表

icon 字段说明

字段类型说明
codestring业务代码,用于组件引用
namestring图标中文名称
valuestringIconify CSS 类名(如 i-ep-home

3. 两种格式的区别

特性IconfontIconify
来源阿里巴巴图标库Iconify 图标集
使用方式字体图标SVG 图标
类名格式icon-xxxi-集合-名称
颜色支持单色单色/多色
代码字段font_classcode
名称字段namename
额外字段unicodevalue

生成流程

1. 解析 Iconfont JSON

typescript
// 解析 iconfont JSON 文件
const parseIconfontJson = (jsonPath: string): IconItem[] => {
  try {
    const content = readFileSync(jsonPath, 'utf-8')
    const data: IconfontJson = JSON.parse(content)

    if (data.glyphs && Array.isArray(data.glyphs)) {
      return data.glyphs.map((glyph) => ({
        code: glyph.font_class || glyph.name,
        name: glyph.name
      }))
    }

    return []
  } catch (error) {
    console.warn(`解析 iconfont 文件失败: ${jsonPath}`, error)
    return []
  }
}

处理逻辑

  1. 读取 JSON 文件内容
  2. 解析 JSON 并提取 glyphs 数组
  3. 映射为统一的 IconItem 格式
  4. code 优先使用 font_class,回退到 name
  5. 错误时返回空数组并输出警告

2. 解析 Iconify JSON

typescript
// 解析 iconify JSON 文件
const parseIconifyJson = (jsonPath: string): IconifyIconItem[] => {
  try {
    const content = readFileSync(jsonPath, 'utf-8')
    const data: IconifyJson = JSON.parse(content)

    if (data.type === 'iconify' && data.icons && Array.isArray(data.icons)) {
      return data.icons.map((icon) => ({
        code: icon.code,
        name: icon.name,
        value: icon.value
      }))
    }

    return []
  } catch (error) {
    console.warn(`解析 iconify 文件失败: ${jsonPath}`, error)
    return []
  }
}

处理逻辑

  1. 读取 JSON 文件内容
  2. 验证 type 字段为 "iconify"
  3. 提取 icons 数组并映射
  4. 保留 value 字段用于 CSS 类名生成
  5. 错误时返回空数组

3. 递归扫描目录

typescript
// 获取所有图标
const getAllIcons = (iconsPath: string) => {
  const iconfontIcons: IconItem[] = []
  const iconifyIcons: IconifyIconItem[] = []
  const seenCodes = new Set<string>() // 用于去重

  if (!existsSync(iconsPath)) {
    console.warn(`图标目录不存在: ${iconsPath}`)
    return { iconfontIcons, iconifyIcons, allIcons: [] }
  }

  // 递归扫描目录
  const scanDirectory = (dirPath: string) => {
    try {
      const items = readdirSync(dirPath)

      items.forEach((item) => {
        const itemPath = join(dirPath, item)

        try {
          if (statSync(itemPath).isDirectory()) {
            scanDirectory(itemPath) // 递归扫描子目录
          } else if (item.endsWith('.json')) {
            // 优先尝试解析为 iconify 格式
            const iconifyIconsFromFile = parseIconifyJson(itemPath)
            if (iconifyIconsFromFile.length > 0) {
              console.log(`发现 iconify 图标文件: ${itemPath}, 图标数量: ${iconifyIconsFromFile.length}`)
              iconifyIcons.push(...iconifyIconsFromFile)
            } else {
              // 否则按 iconfont 格式解析
              const iconfontIconsFromFile = parseIconfontJson(itemPath)
              if (iconfontIconsFromFile.length > 0) {
                console.log(`发现 iconfont 图标文件: ${itemPath}, 图标数量: ${iconfontIconsFromFile.length}`)
                iconfontIcons.push(...iconfontIconsFromFile)
              }
            }
          }
        } catch (error) {
          console.warn(`跳过无法访问的文件: ${itemPath}`)
        }
      })
    } catch (error) {
      console.warn(`无法读取目录: ${dirPath}`)
    }
  }

  scanDirectory(iconsPath)

  // 合并去重
  const allIcons: IconItem[] = []

  // 先添加 iconfont 图标
  iconfontIcons.forEach((icon) => {
    if (!seenCodes.has(icon.code)) {
      seenCodes.add(icon.code)
      allIcons.push(icon)
    }
  })

  // 再添加 iconify 图标
  iconifyIcons.forEach((icon) => {
    if (!seenCodes.has(icon.code)) {
      seenCodes.add(icon.code)
      allIcons.push({ code: icon.code, name: icon.name })
    }
  })

  console.log(`图标扫描完成 - iconfont: ${iconfontIcons.length}, iconify: ${iconifyIcons.length}, 总计: ${allIcons.length}`)

  return {
    iconfontIcons: iconfontIcons.sort((a, b) => a.code.localeCompare(b.code)),
    iconifyIcons: iconifyIcons.sort((a, b) => a.code.localeCompare(b.code)),
    allIcons: allIcons.sort((a, b) => a.code.localeCompare(b.code))
  }
}

扫描流程

text
src/assets/icons/
├── system/
│   └── iconfont.json    ← 解析为 iconfont
├── iconify/
│   └── preset.json      ← 解析为 iconify
└── custom/
    ├── business.json    ← 自动检测格式
    └── modules/
        └── extra.json   ← 递归扫描

4. 去重策略

项目使用 Set 数据结构进行图标代码去重:

typescript
const seenCodes = new Set<string>()

// 添加图标时检查重复
if (!seenCodes.has(icon.code)) {
  seenCodes.add(icon.code)
  allIcons.push(icon)
}

去重规则

  1. Iconfont 优先:先添加 Iconfont 图标
  2. 后来覆盖:相同 code 的后续图标被忽略
  3. 跨文件去重:多个 JSON 文件间也会去重

5. 排序策略

所有图标列表按 code 字母顺序排序,便于查找和对比:

typescript
return {
  iconfontIcons: iconfontIcons.sort((a, b) => a.code.localeCompare(b.code)),
  iconifyIcons: iconifyIcons.sort((a, b) => a.code.localeCompare(b.code)),
  allIcons: allIcons.sort((a, b) => a.code.localeCompare(b.code))
}

类型文件生成

生成模板

typescript
const generateTypeContent = (iconData: ReturnType<typeof getAllIcons>): string => {
  const { iconfontIcons, iconifyIcons, allIcons } = iconData

  // 生成联合类型
  const iconCodes = allIcons.map((icon) => `    | '${icon.code}'`).join('\n')

  // 生成数组字面量
  const iconfontArray = iconfontIcons.map((icon) =>
    `  { code: '${icon.code}', name: '${icon.name}' }`
  ).join(',\n')

  const iconifyArray = iconifyIcons.map((icon) =>
    `  { code: '${icon.code}', name: '${icon.name}', value: '${icon.value}' }`
  ).join(',\n')

  const allArray = allIcons.map((icon) =>
    `  { code: '${icon.code}', name: '${icon.name}' }`
  ).join(',\n')

  return `/** 类型声明内容 */`
}

生成的文件结构

typescript
/**
 * 图标类型声明文件
 *
 * 此文件由 iconfont-types 插件自动生成
 * 扫描目录: src/assets/icons
 * 图标数量: 817 个图标 (iconfont: 644, iconify: 173)
 *
 * 请勿手动修改此文件,所有修改将在下次构建时被覆盖
 */

declare global {
  /** 图标代码类型 */
  type IconCode =
    | 'admin'
    | 'alipay'
    | 'arrow-down'
    | 'arrow-up'
    | 'audio'
    | 'bag'
    // ... 更多图标
}

/** 图标项接口 */
export interface IconItem {
  code: string
  name: string
}

/** iconify 图标项接口 */
export interface IconifyIconItem extends IconItem {
  value: string
}

/** iconfont 图标列表 (仅来自 iconfont JSON 文件) */
export const ICONFONT_ICONS: IconItem[] = [
  { code: 'elevator3', name: '电梯' },
  { code: 'battery', name: '电量' },
  { code: 'floating-bottle', name: '漂流瓶' },
  // ... 644 个图标
]

/** iconify 预设图标列表 (包含 value 属性) */
export const ICONIFY_ICONS: IconifyIconItem[] = [
  { code: 'admin', name: '管理员', value: 'i-clarity:administrator-line' },
  { code: 'alipay', name: '支付宝支付', value: 'i-ri-alipay-fill' },
  { code: 'arrow-down', name: '向下箭头', value: 'i-mdi:arrow-down' },
  // ... 173 个图标
]

/** 所有可用图标列表 (用于图标选择器) */
export const ALL_ICONS: IconItem[] = [
  { code: 'admin', name: '管理员' },
  { code: 'alipay', name: '支付宝支付' },
  // ... 817 个图标
]

写入类型文件

typescript
const writeTypeFile = (showLog = true) => {
  const iconsPath = resolve(projectRoot, iconsDir)
  const outputFilePath = resolve(projectRoot, outputPath)

  try {
    // 获取图标数据
    const iconData = getAllIcons(iconsPath)

    // 生成类型内容
    const typeContent = generateTypeContent(iconData)

    // 确保输出目录存在
    const outputDir = dirname(outputFilePath)
    if (!existsSync(outputDir)) {
      mkdirSync(outputDir, { recursive: true })
    }

    // 写入文件
    writeFileSync(outputFilePath, typeContent, 'utf-8')

    if (showLog) {
      console.log(`图标类型已生成: ${outputPath} (${iconData.allIcons.length} 个图标)`)
    }
  } catch (error) {
    console.error('生成图标类型时出错:', error)
  }
}

工具函数

生成的类型文件包含多个实用工具函数。

isValidIconCode

验证代码是否为有效的图标代码。

typescript
/** 检查代码是否为有效的图标 */
export const isValidIconCode = (code: string): code is IconCode => {
  return ALL_ICONS.some((icon) => icon.code === code)
}

// 使用示例
if (isValidIconCode('dashboard')) {
  console.log('有效的图标代码')
}

// 类型收窄
function renderIcon(code: string) {
  if (isValidIconCode(code)) {
    // 这里 code 的类型是 IconCode
    return <Icon icon={code} />
  }
  return null
}

isIconfontIcon

检查代码是否为 Iconfont 图标。

typescript
/** 检查代码是否为 iconfont 图标 */
export const isIconfontIcon = (code: string): boolean => {
  return ICONFONT_ICONS.some((icon) => icon.code === code)
}

// 使用示例
if (isIconfontIcon('elevator3')) {
  console.log('这是一个 Iconfont 图标')
}

isIconifyIcon

检查代码是否为 Iconify 图标。

typescript
/** 检查代码是否为 iconify 图标 */
export const isIconifyIcon = (code: string): boolean => {
  return ICONIFY_ICONS.some((icon) => icon.code === code)
}

// 使用示例
if (isIconifyIcon('dashboard')) {
  console.log('这是一个 Iconify 图标')
}

getIconifyValue

获取 Iconify 图标的 CSS 类名。

typescript
/** 获取 iconify 图标的 value */
export const getIconifyValue = (code: string): string | undefined => {
  return ICONIFY_ICONS.find((icon) => icon.code === code)?.value
}

// 使用示例
const cssClass = getIconifyValue('dashboard')
// 返回: 'i-tabler:layout-dashboard'

// 在组件中使用
<div :class="getIconifyValue('dashboard')"></div>

getIconName

根据代码获取图标的中文名称。

typescript
/** 根据代码获取图标名称 */
export const getIconName = (code: IconCode): string => {
  return ALL_ICONS.find((icon) => icon.code === code)?.name || code
}

// 使用示例
const name = getIconName('dashboard')
// 返回: '仪表盘'

// 用于界面显示
<span>{{ getIconName(selectedIcon) }}</span>

searchIcons

根据关键词搜索图标。

typescript
/** 搜索图标 */
export const searchIcons = (query: string): IconItem[] => {
  const searchTerm = query.toLowerCase()
  return ALL_ICONS.filter((icon) =>
    icon.code.toLowerCase().includes(searchTerm) ||
    icon.name.toLowerCase().includes(searchTerm)
  )
}

// 使用示例
const results = searchIcons('仪表')
// 返回: [{ code: 'dashboard', name: '仪表盘' }, ...]

// 搜索英文
const results2 = searchIcons('chart')
// 返回所有包含 'chart' 的图标

getAllIconCodes

获取所有图标代码数组。

typescript
/** 获取所有图标代码 */
export const getAllIconCodes = (): IconCode[] => {
  return ALL_ICONS.map((icon) => icon.code as IconCode)
}

// 使用示例
const allCodes = getAllIconCodes()
console.log(allCodes.length) // 817

// 用于图标选择器
const options = getAllIconCodes().map(code => ({
  label: getIconName(code),
  value: code
}))

插件配置

注册插件

插件在 vite/plugins/index.ts 中注册:

typescript
// vite/plugins/index.ts
import vue from '@vitejs/plugin-vue'
import createUnoCss from './unocss'
import createAutoImport from './auto-imports'
import createComponents from './components'
import createIcons from './icons'
import createIconfontTypes from './iconfont-types'

export default (viteEnv: any, isBuild = false): [] => {
  const vitePlugins: any = []

  // Vue 官方插件
  vitePlugins.push(vue())

  // UnoCSS 原子化 CSS 引擎
  vitePlugins.push(createUnoCss())

  // 自动导入插件
  vitePlugins.push(createAutoImport(path))

  // 组件自动导入插件
  vitePlugins.push(createComponents(path))

  // 图标自动导入插件(iconify)
  vitePlugins.push(createIcons())

  // ⭐ iconfont 图标类型生成插件
  vitePlugins.push(createIconfontTypes())

  return vitePlugins
}

图标插件 (icons.ts)

配合 unplugin-icons 使用,支持图标自动安装:

typescript
// vite/plugins/icons.ts
import Icons from 'unplugin-icons/vite'

// 图标使用方法:https://icones.js.org/ 前往搜索图标
// 点击 unocss,得到样式类
// <div class="i-material-symbols-home-rounded"/>

export default () => {
  return Icons({
    // 自动安装图标库
    // 当使用未安装的图标集合时,将自动下载并安装相应的图标依赖
    autoInstall: true
  })
}

vite.config.ts 配置

typescript
// vite.config.ts
import { defineConfig } from 'vite'
import createPlugins from './vite/plugins'

export default defineConfig(({ mode }) => {
  const isBuild = mode === 'production'
  const viteEnv = loadEnv(mode, process.cwd())

  return {
    plugins: createPlugins(viteEnv, isBuild),
    // 其他配置...
  }
})

开发模式

热更新支持

插件在开发模式下监听图标 JSON 文件变化:

typescript
handleHotUpdate({ file }) {
  const iconsPath = resolve(projectRoot, iconsDir)

  // 只处理图标目录下的 JSON 文件
  if (file.startsWith(iconsPath) && file.endsWith('.json')) {
    console.log(`检测到图标文件变化: ${relative(projectRoot, file)}`)
    writeTypeFile()
  }
}

触发条件

  1. 文件必须在 src/assets/icons/ 目录下
  2. 文件扩展名必须是 .json
  3. 文件内容发生变化

输出示例

text
检测到图标文件变化: src/assets/icons/iconify/preset.json
发现 iconify 图标文件: D:\project\plus-ui\src\assets\icons\iconify\preset.json, 图标数量: 173
图标扫描完成 - iconfont: 644, iconify: 173, 总计: 817
图标类型已生成: src/types/icons.d.ts (817 个图标)

开发工作流

text
1. 编辑 preset.json 添加新图标

2. Vite 检测到文件变化

3. handleHotUpdate 触发

4. 重新扫描所有图标 JSON

5. 重新生成 icons.d.ts

6. TypeScript 检测到类型文件变化

7. IDE 更新智能提示

使用类型

类型安全

typescript
import type { IconCode } from '@/types/icons'

// ✅ 类型检查通过
const validIcon: IconCode = 'dashboard'

// ❌ 编译错误: Type '"not-exist"' is not assignable to type 'IconCode'
const invalidIcon: IconCode = 'not-exist'

组件 Props

vue
<script setup lang="ts">
import type { IconCode } from '@/types/icons'

interface Props {
  icon: IconCode
  size?: number
}

const props = withDefaults(defineProps<Props>(), {
  size: 16
})
</script>

<template>
  <div :class="getIconClass(props.icon)" :style="{ fontSize: props.size + 'px' }"></div>
</template>

智能提示

vue
<script setup lang="ts">
import type { IconCode } from '@/types/icons'

// 输入时自动提示所有 817 个可用图标
const icon = ref<IconCode>('dash') // IDE 会提示 'dashboard'
</script>

表单验证

typescript
import { isValidIconCode } from '@/types/icons'

// 验证用户输入的图标代码
function validateIcon(value: string): boolean {
  if (!isValidIconCode(value)) {
    console.error(`无效的图标代码: ${value}`)
    return false
  }
  return true
}

// 在表单校验中使用
const rules = {
  icon: [
    { required: true, message: '请选择图标' },
    { validator: (rule, value, callback) => {
      if (!isValidIconCode(value)) {
        callback(new Error('无效的图标代码'))
      } else {
        callback()
      }
    }}
  ]
}

图标选择器

vue
<script setup lang="ts">
import { ALL_ICONS, searchIcons, getIconName } from '@/types/icons'
import type { IconCode } from '@/types/icons'

const keyword = ref('')
const selectedIcon = ref<IconCode>('dashboard')

// 过滤后的图标列表
const filteredIcons = computed(() => {
  if (!keyword.value) return ALL_ICONS
  return searchIcons(keyword.value)
})

// 选择图标
const selectIcon = (code: IconCode) => {
  selectedIcon.value = code
}
</script>

<template>
  <div class="icon-picker">
    <input v-model="keyword" placeholder="搜索图标..." />

    <div class="icon-grid">
      <div
        v-for="icon in filteredIcons"
        :key="icon.code"
        :class="{ selected: icon.code === selectedIcon }"
        @click="selectIcon(icon.code as IconCode)"
      >
        <Icon :icon="icon.code" />
        <span>{{ icon.name }}</span>
      </div>
    </div>
  </div>
</template>

高级功能

自定义输出路径

修改插件配置以自定义输出路径:

typescript
// 插件内修改
const iconsDir = 'src/assets/icons'           // 图标源目录
const outputPath = 'src/types/custom-icons.d.ts' // 自定义输出路径

多图标源配置

支持扫描多个目录:

typescript
const iconsDirs = [
  'src/assets/icons',      // 主图标目录
  'src/modules/*/icons'    // 模块级图标
]

const getAllIcons = () => {
  const allIconfontIcons: IconItem[] = []
  const allIconifyIcons: IconifyIconItem[] = []

  iconsDirs.forEach(dir => {
    const { iconfontIcons, iconifyIcons } = scanDirectory(dir)
    allIconfontIcons.push(...iconfontIcons)
    allIconifyIcons.push(...iconifyIcons)
  })

  return mergeAndDedupe(allIconfontIcons, allIconifyIcons)
}

生成分组信息

扩展生成的类型文件,包含分组信息:

typescript
// 按前缀分组
function groupIcons(icons: IconItem[]): Record<string, IconItem[]> {
  return icons.reduce((groups, icon) => {
    const prefix = icon.code.split('-')[0] || 'other'
    if (!groups[prefix]) groups[prefix] = []
    groups[prefix].push(icon)
    return groups
  }, {} as Record<string, IconItem[]>)
}

// 生成分组导出
export const ICON_GROUPS: Record<string, IconItem[]> = {
  arrow: [
    { code: 'arrow-up', name: '向上箭头' },
    { code: 'arrow-down', name: '向下箭头' }
  ],
  chart: [
    { code: 'chart', name: '图表' },
    { code: 'pie-chart', name: '饼图' },
    { code: 'bar-chart', name: '柱状图' },
    { code: 'line-chart', name: '折线图' }
  ]
  // ...
}

生成统计信息

扩展类型文件,包含统计信息:

typescript
// 生成统计常量
export const ICON_STATS = {
  total: 817,
  iconfont: 644,
  iconify: 173,
  groups: 45,
  lastGenerated: '2025-12-25T10:30:00Z'
} as const

// 使用统计信息
console.log(`共有 ${ICON_STATS.total} 个图标`)

自定义验证规则

添加更严格的验证规则:

typescript
/** 验证图标代码格式 */
export const validateIconCodeFormat = (code: string): { valid: boolean; error?: string } => {
  // 检查空值
  if (!code || typeof code !== 'string') {
    return { valid: false, error: '图标代码不能为空' }
  }

  // 检查格式
  if (!/^[a-z][a-z0-9-]*$/.test(code)) {
    return { valid: false, error: '图标代码只能包含小写字母、数字和连字符' }
  }

  // 检查是否存在
  if (!isValidIconCode(code)) {
    return { valid: false, error: `未知的图标代码: ${code}` }
  }

  return { valid: true }
}

手动生成

命令行脚本

创建独立脚本用于手动生成类型:

typescript
// scripts/generate-icon-types.ts
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'

const __dirname = dirname(fileURLToPath(import.meta.url))
const projectRoot = resolve(__dirname, '..')

// 复用插件逻辑
import { generateIconTypes } from '../vite/plugins/iconfont-types'

console.log('🚀 开始生成图标类型...')

try {
  generateIconTypes(projectRoot)
  console.log('✅ 图标类型生成完成!')
} catch (error) {
  console.error('❌ 生成失败:', error)
  process.exit(1)
}

package.json 配置

json
{
  "scripts": {
    "generate:icons": "tsx scripts/generate-icon-types.ts",
    "icons": "npm run generate:icons"
  }
}

执行生成

bash
# 使用 npm
npm run generate:icons

# 使用 pnpm
pnpm generate:icons

# 直接执行
npx tsx scripts/generate-icon-types.ts

CI/CD 集成

在 CI 流程中确保类型是最新的:

yaml
# .github/workflows/build.yml
jobs:
  build:
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: pnpm install

      - name: Generate icon types
        run: pnpm generate:icons

      - name: Type check
        run: pnpm type-check

      - name: Build
        run: pnpm build

与 UnoCSS 集成

safelist 配置

生成的类型文件与 UnoCSS safelist 配合使用:

typescript
// uno.config.ts
import { ICONIFY_ICONS } from './src/types/icons.d'

export default defineConfig({
  safelist: [
    // 预加载所有 Iconify 图标
    ...ICONIFY_ICONS.map((icon) => icon.value)
  ],

  presets: [
    presetIcons({
      scale: 1.2,
      cdn: 'https://esm.sh/'
    })
  ]
})

动态图标支持

typescript
// 获取动态图标的 CSS 类名
const getIconClass = (code: string): string => {
  // 先检查是否是 Iconify 图标
  const iconifyValue = getIconifyValue(code)
  if (iconifyValue) {
    return iconifyValue
  }

  // 否则按 Iconfont 处理
  if (isIconfontIcon(code)) {
    return `icon-${code}`
  }

  // 回退
  return code
}

故障排查

类型未生成

症状src/types/icons.d.ts 文件不存在或为空

排查步骤

bash
# 1. 检查插件是否注册
grep -r "iconfontTypes" vite.config.ts

# 2. 检查图标目录
ls -la src/assets/icons/

# 3. 检查 JSON 文件格式
cat src/assets/icons/iconify/preset.json | jq .

# 4. 手动触发生成
pnpm generate:icons

类型不更新

症状:添加新图标后类型没有更新

解决方案

bash
# 1. 清除 Vite 缓存
rm -rf node_modules/.vite

# 2. 删除旧类型文件
rm src/types/icons.d.ts

# 3. 重启开发服务器
pnpm dev

图标重复

症状:控制台警告图标代码重复

解决方案

  1. 检查多个 JSON 文件中是否有相同的 code
  2. 使用 seenCodes Set 确保去重正确
  3. 明确 Iconfont 和 Iconify 的优先级

JSON 解析失败

症状:控制台输出 "解析 xxx 文件失败"

解决方案

bash
# 验证 JSON 格式
cat src/assets/icons/iconify/preset.json | python -m json.tool

# 常见问题:
# - 尾随逗号
# - 缺少引号
# - 特殊字符未转义

热更新不触发

症状:修改 JSON 后类型没有重新生成

排查

  1. 确认文件路径在 src/assets/icons/
  2. 确认文件扩展名是 .json
  3. 检查 Vite 热更新是否正常工作
  4. 查看控制台是否有 "检测到图标文件变化" 日志

性能优化

缓存机制

添加生成结果缓存,避免重复计算:

typescript
let cachedContent: string | null = null
let cachedHash: string | null = null

const generateWithCache = (iconsPath: string) => {
  const currentHash = computeDirectoryHash(iconsPath)

  if (cachedHash === currentHash && cachedContent) {
    return cachedContent
  }

  const content = generateTypeContent(getAllIcons(iconsPath))
  cachedContent = content
  cachedHash = currentHash

  return content
}

增量更新

只更新变化的部分:

typescript
const updateIncrementally = (changedFile: string) => {
  // 只重新解析变化的文件
  const newIcons = parseJsonFile(changedFile)

  // 合并到现有数据
  mergeIcons(existingIcons, newIcons)

  // 只重新生成类型
  regenerateTypes()
}

并行处理

大量文件时使用并行处理:

typescript
import { Worker } from 'worker_threads'

const parseFilesInParallel = async (files: string[]) => {
  const workers = files.map(file =>
    new Promise((resolve) => {
      const worker = new Worker('./parse-worker.js', { workerData: file })
      worker.on('message', resolve)
    })
  )

  return Promise.all(workers)
}

最佳实践

1. 保持代码唯一

json
// ✅ 好的实践:使用唯一、有意义的代码
{
  "code": "user-management",
  "name": "用户管理"
}

// ❌ 不推荐:使用模糊或重复的代码
{
  "code": "icon1",
  "name": "图标1"
}

2. 使用语义化名称

json
// ✅ 好的实践
{
  "code": "arrow-left",
  "name": "向左箭头",
  "value": "i-mdi:arrow-left"
}

// ❌ 不推荐
{
  "code": "a1",
  "name": "箭头",
  "value": "i-mdi:arrow-left"
}

3. 组织目录结构

text
src/assets/icons/
├── system/              # 系统级图标
│   └── iconfont.json    # Iconfont 图标
├── iconify/             # Iconify 图标
│   └── preset.json      # 预设图标
├── business/            # 业务图标
│   ├── shop.json        # 商城模块
│   └── workflow.json    # 工作流模块
└── custom/              # 自定义图标
    └── brand.json       # 品牌图标

4. 版本控制

bash
# 将生成的类型文件加入版本控制
git add src/types/icons.d.ts

# 在 .gitignore 中不要忽略它
# 这确保团队成员都有一致的类型

5. 类型文件检查

在 CI 中验证类型文件是否最新:

bash
#!/bin/bash
# scripts/check-icon-types.sh

# 生成新的类型文件
pnpm generate:icons --output=.tmp-icons.d.ts

# 比较差异
if ! diff -q src/types/icons.d.ts .tmp-icons.d.ts > /dev/null; then
  echo "❌ 图标类型文件不是最新的,请运行 pnpm generate:icons"
  rm .tmp-icons.d.ts
  exit 1
fi

echo "✅ 图标类型文件是最新的"
rm .tmp-icons.d.ts

6. 文档化图标

为重要图标添加注释说明:

json
{
  "icons": [
    {
      "code": "workflow-approve",
      "name": "审批通过",
      "value": "i-mdi:check-circle",
      "_description": "用于工作流审批通过状态图标"
    },
    {
      "code": "workflow-reject",
      "name": "审批拒绝",
      "value": "i-mdi:close-circle",
      "_description": "用于工作流审批拒绝状态图标"
    }
  ]
}

配置示例

小型项目配置

typescript
// 简单配置,单一图标源
const iconsDir = 'src/assets/icons'
const outputPath = 'src/types/icons.d.ts'

// 最小化输出
const generateTypeContent = (iconData) => {
  const codes = iconData.allIcons.map(i => `'${i.code}'`).join(' | ')
  return `export type IconCode = ${codes}`
}

中型项目配置

typescript
// 标准配置,多图标源
const iconsDirs = [
  'src/assets/icons/system',
  'src/assets/icons/business'
]

// 完整输出,包含工具函数
// 使用默认的 generateTypeContent

大型项目配置

typescript
// 企业级配置
const config = {
  // 多目录源
  iconsDirs: [
    'src/assets/icons/system',
    'src/assets/icons/business',
    'packages/*/src/assets/icons'
  ],

  // 多输出文件
  outputs: {
    types: 'src/types/icons.d.ts',
    json: 'public/icons.json',
    stats: 'docs/icons-stats.json'
  },

  // 验证规则
  validation: {
    requireUniqueCode: true,
    requireChineseName: true,
    maxIcons: 2000
  },

  // 性能配置
  performance: {
    parallel: true,
    cache: true,
    incremental: true
  }
}

常见问题

1. 为什么使用 code 而不是直接用 value?

原因

  • code 是业务层面的抽象,不依赖具体图标实现
  • 更换图标库时只需修改 JSON,不需要改代码
  • 更短更易记忆
  • 支持 Iconfont 和 Iconify 统一处理

2. 如何添加新的图标类型?

步骤

  1. parseXxxJson 函数中添加解析逻辑
  2. 定义新的接口类型
  3. scanDirectory 中添加识别逻辑
  4. 更新 generateTypeContent 的输出模板

3. 图标数量太多影响性能吗?

回答

  • 类型生成只在构建时执行,不影响运行时
  • 类型文件会被 TypeScript 缓存
  • 搜索函数使用线性查找,1000+ 图标仍然很快
  • 如需优化,可以考虑索引或分组

4. 如何支持多语言图标名称?

方案

json
{
  "code": "dashboard",
  "name": {
    "zh": "仪表盘",
    "en": "Dashboard",
    "ja": "ダッシュボード"
  },
  "value": "i-tabler:layout-dashboard"
}
typescript
export const getIconName = (code: IconCode, locale = 'zh'): string => {
  const icon = ALL_ICONS.find(i => i.code === code)
  if (typeof icon?.name === 'object') {
    return icon.name[locale] || icon.name['zh'] || code
  }
  return icon?.name || code
}

5. 如何处理图标版本更新?

建议

  1. 使用 Git 跟踪 JSON 文件变化
  2. 在提交信息中说明图标变更
  3. 考虑添加 _version 字段
  4. 使用 CHANGELOG 记录重大变更

6. 生成的类型文件很大怎么办?

优化建议

  1. 移除未使用的图标
  2. 按模块分割类型文件
  3. 使用 TypeScript 项目引用
  4. 考虑按需加载图标数据

自动类型生成机制确保了图标使用的类型安全,提高了开发效率和代码质量。通过 Vite 插件的热更新支持,开发者可以即时看到新图标的类型提示,无需手动维护类型定义。