Iconify 动态加载机制与 unplugin-icons 的区别
背景
在课程的前面章节中介绍过 unplugin-icons 插件,它与 @iconify/vue 都基于 Iconify 图标数据。本节深入分析两者的核心差异,帮助在实际项目中做出正确选择。
unplugin-icons 的工作原理
unplugin-icons 是一个构建时插件,核心机制:
- 在编译阶段扫描源代码中引用的图标
- 从
@iconify-json/*包中提取对应图标的 SVG 数据 - 将 SVG 编译为独立的 Vue 组件
- 通过 Tree-shaking 只保留实际使用的图标
使用方式
<script setup lang="ts">
// 必须静态 import 每个要使用的图标
import IconHome from '~icons/mdi/home'
import IconAccount from '~icons/mdi/account-circle'
import IconSetting from '~icons/carbon/settings'
</script>
<template>
<IconHome />
<IconAccount />
<IconSetting />
</template>
vue
关键限制:不支持动态导入
<script setup lang="ts">
// 不支持这样写 -- 编译时无法确定 icon 变量的值
const iconName = ref('mdi:home')
// import(`~icons/${iconName}`) // 编译错误!
</script>
vue
这是因为 unplugin-icons 在构建时通过静态分析确定要打包哪些图标。动态变量在运行时才能确定值,构建时无法解析。
该限制在 GitHub Issues 中有明确讨论(2021 年已关闭),作者表示 "此插件用于静态使用场景"。
@iconify/vue 的动态加载机制
@iconify/vue 采用运行时加载策略:
- 组件接收
icon字符串属性 - 运行时从 Iconify API 或本地缓存获取 SVG 数据
- 渲染为内联 SVG
支持完全动态使用
<script setup lang="ts">
import { Icon } from '@iconify/vue'
// 图标名可以是任意动态字符串
const iconName = ref('mdi:home')
// 定时切换(完全动态)
const icons = ['mdi:home', 'mdi:account', 'mdi:cog', 'mdi:heart']
setInterval(() => {
const random = Math.floor(Math.random() * icons.length)
iconName.value = icons[random]
}, 1000)
</script>
<template>
<Icon :icon="iconName" class="text-2xl" />
</template>
vue
核心差异对比
| 特性 | unplugin-icons | @iconify/vue |
|---|---|---|
| 加载时机 | 构建时 | 运行时 |
| 动态图标 | 不支持 | 完全支持 |
| 网络请求 | 无(已内联) | 需要(首次从 API 加载) |
| 包体积 | 只包含使用的图标 | 核心库极小(~31KB) |
| 离线可用 | 天然支持 | 需预加载或本地 Provider |
| 图标数量 | 仅打包引用的 | 可按需加载任意图标 |
| 开发体验 | 需逐个 import | 直接传字符串 |
| 适用场景 | 图标固定、少量 | 图标多、需动态切换 |
| SSR 兼容 | 构建时确定,SSR 友好 | 需注意 API 请求 |
体积问题分析
全量安装 @iconify/json 会导致包体积过大:
@iconify/json/json/mdi.json → GZIP 后仍有 817KB+
text
因为 SVG 图标本质是字符串数据,大量图标字符串会显著增加 bundle 体积。因此:
- 不要将全量 JSON 打包进生产 bundle
- 使用
@iconify/vue的运行时加载避免此问题 - 或安装特定图标集
@iconify-json/mdi(仅单个集合,体积可控)
正确的动态图标方案
方案一:直接使用 @iconify/vue Icon 组件
<script setup lang="ts">
import { Icon } from '@iconify/vue'
const icon = ref('material-symbols:account-circle')
</script>
<template>
<Icon :icon="icon" class="text-2xl text-blue-500" />
</template>
vue
图标首次渲染时从 api.iconify.design 加载 SVG 数据,后续由浏览器缓存。
方案二:封装 IconifyD 组件
基于 @iconify/vue 封装一个支持完整 Props 的动态图标组件:
// src/components/Icons/types.d.ts
// 从 @iconify/vue 导出原始 IconProps 类型
export type { IconProps } from '@iconify/vue/dist/offline'
typescript
<!-- src/components/Icons/IconifyD.vue -->
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import type { IconPropsForIconify } from '@iconify/vue'
// 使用 @iconify/vue 内部的完整 Props 类型
const props = defineProps<IconPropsForIconify>()
</script>
<template>
<Icon v-bind="props" />
</template>
vue
IconPropsForIconify 包含 icon, width, height, color, rotate, flip, inline 等完整属性定义。
方案三:预加载 + 运行时渲染
对于已知需要使用的图标列表,提前预加载:
// src/utils/preload-icons.ts
import { loadIcons } from '@iconify/vue'
export async function preloadIcons(icons: string[]) {
await loadIcons(icons)
}
typescript
<!-- 在首页或入口页面预加载 -->
<script setup lang="ts">
import { onBeforeMount } from 'vue'
import { preloadIcons } from '@/utils/preload-icons'
onBeforeMount(async () => {
// 预加载常用图标
await preloadIcons([
'mdi:home',
'mdi:account',
'mdi:cog',
'mdi:bell',
'mdi:menu',
])
})
</script>
vue
预加载策略:
| 预加载时机 | 实现方式 | 适用场景 |
|---|---|---|
| 首页加载 | onBeforeMount + loadIcons | 全局常用图标 |
| 路由守卫 | router.beforeEach | 特定页面需要的图标 |
| 组件挂载前 | onBeforeMount | 当前组件使用的图标 |
| 延迟加载 | setTimeout + loadIcons | 非关键路径图标 |
方案选择决策树
是否需要动态切换图标?
├── 是 → 使用 @iconify/vue
│ ├── 图标数量少 → 直接 <Icon :icon="name" />
│ ├── 图标数量多 → loadIcons 预加载 + <Icon />
│ └── 完全离线 → addAPIProvider + 本地 JSON
│
└── 否 → 可以使用 unplugin-icons
├── 图标固定且少 → unplugin-icons(零运行时开销)
├── 图标固定且多 → unplugin-icons + 按需安装 @iconify-json/*
└── 混合场景 → 两者组合使用
text
生产环境推荐
对于管理后台类项目(本课程场景),推荐组合方案:
@iconify/vue作为主要图标方案 -- 支持动态加载、图标选择器等交互- UnoCSS
presetIcons用于 CSS 类方式使用固定图标(如导航图标) vite-plugin-svg-icons用于设计师提供的自定义 SVG 图标loadIcons预加载确保首屏图标无闪烁
// nuxt.config.ts 或 vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
// 构建时 SVG 图标
createSvgIconsPlugin({
iconDirs: [resolve(__dirname, 'src/assets/icons')],
}),
],
})
typescript
<!-- 混合使用示例 -->
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import SvgIcon from '@/components/SvgIcon.vue'
</script>
<template>
<!-- 运行时动态图标 -->
<Icon :icon="dynamicIcon" />
<!-- CSS 类方式(UnoCSS presetIcons) -->
<div class="i-mdi:home text-xl" />
<!-- 自定义 SVG 图标 -->
<SvgIcon type="custom-logo" />
</template>
vue
↑