一、背景与问题
在微前端架构下,主应用长期积累了 5 套图标方案并存 的混乱局面:
| # | 方案 | 位置 | 使用方式 | 核心问题 |
|---|---|---|---|---|
| 1 | iconfont JS | assets/icon.min.js(115KB) |
<SvgIcon name="xxx"> → #icon-xxx |
全量加载无 Tree-shaking,iconfont 平台维护成本高 |
| 2 | 本地 SVG | assets/svgs/(30 个文件) |
vite-plugin-svg-icons → SVG Sprite |
仅主应用可用,子应用无法共享 |
| 3 | @purge-icons + Iconify | Icon.vue |
<Icon icon="ep:edit"> |
运行时渲染,依赖 @purge-icons/generated |
| 4 | IconJson 硬编码 | Icon/src/data.ts(1962 行) |
IconSelect 组件消费 |
手动维护 EP / FA 图标名列表,极易过时 |
| 5 | CmcIcon | @cmclink/ui |
<CmcIcon name="xxx"> |
已有基础但只支持 SVG Sprite,未与其他方案打通 |
核心痛点:
- 子应用无法共享主应用图标,每个应用各自维护
- 同一个图标可能通过 3 种不同方式引用
- iconfont JS 全量加载 115KB,无法按需
- 1962 行硬编码图标列表,维护成本极高
- 中后台系统 90% 以上使用通用图标,不需要每个应用单独管理
二、治理目标
统一入口 + 集中管理 + 零配置共享
- 一个组件 :
<CmcIcon>统一消费所有图标 - 一个图标包 :
@cmclink/icons集中管理 SVG 资源 - 零配置:子应用迁入 Monorepo 后自动获得所有共享图标
- 按需加载:Element Plus 图标异步 import,不影响首屏
三、方案架构
bash
┌──────────────────────────────────────────────────────┐
│ 使用层(所有子应用) │
│ │
│ <CmcIcon name="Home" /> --- SVG Sprite 图标 │
│ <CmcIcon name="ep:Edit" /> --- Element Plus 图标 │
│ <CmcIcon name="Star" size="lg" color="primary" /> │
├──────────────────────────────────────────────────────┤
│ @cmclink/ui --- CmcIcon 组件 │
│ │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ SVG Sprite │ │ Element Plus │ │
│ │ <svg><use> │ │ 动态 import │ │
│ │ 无前缀 │ │ ep: 前缀 │ │
│ └─────────────┘ └──────────────────┘ │
├──────────────────────────────────────────────────────┤
│ @cmclink/icons --- 共享图标资源包 │
│ │
│ packages/icons/src/svg/ │
│ ├── Home.svg │
│ ├── Star.svg │
│ ├── Logo.svg │
│ └── ... (30+ 通用图标) │
├──────────────────────────────────────────────────────┤
│ @cmclink/vite-config --- 构建自动集成 │
│ │
│ vite-plugin-svg-icons 自动扫描: │
│ 1. packages/icons/src/svg/ (共享图标,优先) │
│ 2. apps/{app}/src/assets/svgs/ (本地图标,可覆盖) │
└──────────────────────────────────────────────────────┘
四、CmcIcon 组件设计
4.1 Props 接口
typescript
interface CmcIconProps {
/**
* 图标名称
* - 无前缀: SVG Sprite 图标(如 "Home"、"Star")
* - "ep:" 前缀: Element Plus 图标(如 "ep:Edit"、"ep:Delete")
*/
name: string
/** 尺寸:数字(px) | 预设('xs'|'sm'|'md'|'lg'|'xl') | CSS 字符串 */
size?: number | string | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
/** 颜色:CSS 值 | 主题色('primary'|'success'|'warning'|'danger'|'info') */
color?: string
/** 旋转角度 */
rotate?: number
/** 旋转动画 */
spin?: boolean
/** 禁用状态 */
disabled?: boolean
/** 可点击 */
clickable?: boolean
}
4.2 预设尺寸
| 尺寸 | 像素 | 场景 |
|---|---|---|
xs |
12px | 辅助文字旁小图标 |
sm |
14px | 表单项内图标 |
md |
16px | 默认,正文行内图标 |
lg |
20px | 按钮内图标 |
xl |
24px | 标题旁图标 |
4.3 主题色
使用 CSS 变量自动跟随 Element Plus 主题:
typescript
const colorMap = {
primary: 'var(--el-color-primary, #004889)',
success: 'var(--el-color-success, #10b981)',
warning: 'var(--el-color-warning, #f59e0b)',
danger: 'var(--el-color-danger, #ef4444)',
info: 'var(--el-color-info, #3b82f6)',
}
4.4 Element Plus 图标异步加载
typescript
// ep: 前缀触发异步加载,不影响首屏 bundle
watch(
() => props.name,
async (name) => {
if (!name.startsWith('ep:')) return
const iconName = name.slice(3) // "ep:Edit" → "Edit"
const icons = await import('@element-plus/icons-vue')
elIconComponent.value = icons[iconName] ?? null
},
{ immediate: true }
)
五、Vite 构建集成
5.1 主应用配置(main-app.ts)
typescript
createSvgIconsPlugin({
iconDirs: [
// 共享图标库(@cmclink/icons)--- 所有子应用共享
resolve(root, '../../packages/icons/src/svg'),
// 应用本地图标(可覆盖共享图标,或放置业务特有图标)
resolve(root, 'src/assets/svgs'),
],
symbolId: 'icon-[dir]-[name]',
svgoOptions: true,
})
5.2 子应用配置(child-app.ts)
typescript
// svgIcons 选项默认 true,子应用零配置即可共享图标
export interface ChildAppOptions {
svgIcons?: boolean // 默认 true
// ...
}
关键设计 :iconDirs 数组中共享图标在前、本地图标在后,本地同名 SVG 可覆盖共享图标,实现灵活的图标定制能力。
六、迁移实施
6.1 迁移映射表
| 旧用法 | 新用法 | 说明 |
|---|---|---|
<SvgIcon name="Home" :size="20" /> |
<CmcIcon name="Home" :size="20" /> |
仅改标签名 |
<Icon icon="ep:edit" /> |
<CmcIcon name="ep:Edit" /> |
icon → name,PascalCase |
<Icon icon="ep:user-filled" /> |
<CmcIcon name="ep:UserFilled" /> |
kebab → PascalCase |
<Icon icon="fontisto:email" /> |
<CmcIcon name="ep:Message" /> |
替换为 EP 等效图标 |
<svg><use href="#icon-xxx" /></svg> |
<CmcIcon name="xxx" /> |
直接使用组件 |
6.2 实施清单
已完成 ✅
| 步骤 | 变更 | 影响文件数 |
|---|---|---|
创建 @cmclink/icons 共享图标包 |
packages/icons/ |
新建 |
| 迁移 SVG 到共享包 | assets/svgs/ → packages/icons/src/svg/ |
30 个 SVG |
重写 CmcIcon 组件 |
支持 SVG Sprite + ep: 前缀 |
1 个文件 |
main-app.ts 配置共享图标扫描 |
iconDirs 新增共享目录 |
1 个文件 |
child-app.ts 同步配置 |
新增 svgIcons 选项 |
1 个文件 |
替换 <SvgIcon> → <CmcIcon> |
删除 import + 替换标签 | 10 个文件 |
替换 <Icon> → <CmcIcon> |
icon → name,PascalCase |
9 个文件 |
删除 icon.min.js |
移除 iconfont 全量加载 | -115KB |
删除 Icon/ 目录 |
Icon.vue + IconSelect.vue + data.ts |
-1962 行 |
删除 SvgIcon.vue |
旧 SVG 图标组件 | 1 个文件 |
清理 setupGlobCom |
移除旧 Icon 全局注册 |
1 个文件 |
清理 Form.vue |
<Icon> → <CmcIcon> (JSX) |
1 个文件 |
6.3 收益量化
| 指标 | 治理前 | 治理后 | 收益 |
|---|---|---|---|
| 图标方案数量 | 5 套 | 1 套 | 维护成本降低 80% |
| 首屏资源 | +115KB (iconfont JS) | 0KB (按需加载) | -115KB |
| 硬编码图标列表 | 1962 行 | 0 行 | 消除过时风险 |
| 子应用图标配置 | 每个应用单独维护 | 零配置 | 开发效率提升 |
| 图标使用入口 | 3 个组件 | 1 个组件 | 心智负担降低 |
七、使用指南
7.1 SVG Sprite 图标(推荐)
vue
<!-- 基础用法 -->
<CmcIcon name="Home" />
<!-- 预设尺寸 -->
<CmcIcon name="Star" size="lg" />
<!-- 自定义像素 -->
<CmcIcon name="Document" :size="32" />
<!-- 主题色 -->
<CmcIcon name="Warning" color="danger" />
<!-- 旋转动画 -->
<CmcIcon name="Loading" spin />
<!-- 可点击 -->
<CmcIcon name="Close" clickable @click="handleClose" />
7.2 Element Plus 图标
vue
<!-- ep: 前缀,异步加载 -->
<CmcIcon name="ep:Edit" />
<CmcIcon name="ep:Delete" color="danger" />
<CmcIcon name="ep:Search" :size="18" />
<CmcIcon name="ep:Loading" spin />
7.3 添加新图标
- 将 SVG 文件放入
packages/icons/src/svg/ - 文件名即图标名(如
MyIcon.svg→<CmcIcon name="MyIcon" />) - 无需任何额外配置,Vite HMR 自动生效
- 所有子应用自动可用
7.4 应用级图标覆盖
如果某个子应用需要定制某个图标的样式:
- 在
apps/{app}/src/assets/svgs/放入同名 SVG - 本地版本自动覆盖共享版本
- 其他子应用不受影响
八、目录结构
bash
packages/
├── icons/ # 共享图标包
│ ├── package.json # @cmclink/icons
│ ├── README.md # 使用文档
│ └── src/
│ ├── index.ts # 导出图标目录路径常量
│ └── svg/ # 所有共享 SVG 图标
│ ├── Home.svg
│ ├── Star.svg
│ ├── UnStar.svg
│ ├── Logo.svg
│ ├── TopMenu.svg
│ └── ...
├── ui/
│ └── src/base/CmcIcon/
│ ├── index.ts
│ └── src/CmcIcon.vue # 统一图标组件
└── vite-config/
└── src/
├── main-app.ts # iconDirs: [共享, 本地]
└── child-app.ts # svgIcons 选项
九、FAQ
Q: IconSelect 组件删除后,图标选择功能怎么办?
A: IconSelect 依赖已删除的 data.ts(1962 行硬编码列表)。如果业务确实需要图标选择器,建议基于 @element-plus/icons-vue 的导出列表动态生成,而非硬编码。后续可在 @cmclink/ui 中实现新版 CmcIconPicker。
Q: 子应用还在外部独立仓库,如何使用共享图标?
A: 当前 child-app.ts 的 iconDirs 使用相对路径 ../../packages/icons/src/svg,仅适用于 Monorepo 内的子应用。外部子应用迁入 Monorepo 后自动生效。迁入前可通过 extraPlugins 自行配置 vite-plugin-svg-icons。
Q: 第三方图标库(如 Font Awesome)怎么处理?
A: 当前 CmcIcon 支持 SVG Sprite 和 Element Plus 两种源。如需扩展第三方图标库,可在 CmcIcon 中增加新的前缀识别(如 fa: → Font Awesome),通过异步 import 按需加载。但中后台系统建议优先使用 Element Plus 图标,保持设计一致性。