
1. 从一次“图标交接翻车”说起为什么我会盯上 iconify事情是这样的上个月我接手一个 vue3 vite 的中后台项目原开发者在组件里直接塞了几十张本地 svg命名从icon-1.svg排到icon-38.svg没有目录说明没有预览页设计迭代后部分图标换了风格我一时间根本分不清哪个是哪个、谁在引用谁。更头疼的是有些 svg 是从 iconfont 下载的文件里还带着源站的style标签打包后体积蹭蹭涨渲染时偶尔还会和全局样式打架。那段时间我满脑子就一个问题在 vue3 vite 的生态里到底有没有一种方式让图标这件事既不用本地维护文件又能按需加载、实时同步设计稿还能在代码里非常直观地引用然后我就认真研究了 iconify。先说结论iconify 不是一个“又一个图标库”而是一个图标聚合框架。它把 Material Design、Font Awesome、Jam Icons、Tabler 等上百个开源图标集统一收编提供了统一的命名规则和调用 API。在 vue3 vite 的项目里用上它之后我本地不再需要存放任何 svg 文件组件里写一句Icon iconmdi:home /就能出图打包时只打包实际用到的图标体积控制得相当漂亮。这篇文章我会从零开始把我在 vue3 vite 项目中“优雅”接入 iconify 的完整路径拆开来讲包括依赖选择、组件封装、动态图标处理、离线部署方案以及我实际踩过的几个坑。如果你正被本地图标管理折磨或者只是想在下一个项目里用更现代的方式处理图标这篇文章应该能给你一个可以直接落地的参考答案。2. 传统图标方案在 vue3 vite 里的三个“不优雅”在正式动手之前我想先花点篇幅说清楚为什么在 vue3 vite 这个组合下传统的那几种图标方案越来越让人难受。2.1 本地 svg 文件维护成本会失控这是最原始的做法把设计给的 svg 放进src/assets/icons然后封装一个SvgIcon组件通过symbol结合use去渲染。刚起步的时候没问题但项目一大就麻烦了命名全靠自觉icon-home、home-icon、ic_home会同时出现没有人记得哪个图标被哪个页面引用删文件全靠赌运气部分 svg 本身制作不规范带内联样式、带g层的 transform放进项目后表现和设计稿不一致你还得手动改文件。这些问题的本质是本地 svg 把“图标的元信息”和“图标的渲染细节”强行耦合在一起而这两者其实应该分离。2.2 CSS 类字体图标样式侵占和扩展困难以 iconfont 为代表的字体图标方案在 vue2 时代非常流行。引入一个字体文件和一份 css然后写i classiconfont icon-home就能出图。但在 vue3 vite 时代它的短板也很明显。首先是样式污染。字体图标通常会给所有[class*icon-]加公共样式这些全局规则很容易和组件库比如 Element Plus的图标体系产生意外交集。其次是新增图标不便捷每次想要加一个不在字体包里的图标你得重新上传、下载、替换字体文件完全没法走自动化流程。而且字体渲染依赖font-family的加载顺序网络不稳定时会出现“方块闪一下再变图标”的丑陋体验。2.3 组件库自带图标边界锁死如果你用的是 Element Plus它自带的element-plus/icons-vue确实顺手但它的范围只覆盖 Element Plus 体系。一旦你需要在项目中混入品牌 logo、自定义业务图标、或某个开源图标集里特有的图就得再引入其他方案最终项目里会同时并存“组件库图标 本地 svg iconfont”三套体系风格很难统一。我见过不少项目的图标代码是长这样的el-iconHomeFilled //el-icon svg classicon aria-hiddentrueuse xlink:href#icon-user/use/svg i classiconfont icon-settings/i同一个页面里三种图标写法渲染出来的视觉风格还各不相同看得人头大。2.4 iconify 恰好补上了这几个洞统一命名一套规则上百个图标集。按需取用代码里写到的图标才会被请求或打包不产生全量资源。不锁定 UI 框架vue、react、svelte 都有对应封装你在 vue3 里用它就是一套 vue 组件。支持在线实时访问开发时从 CDN 拉取改个图标名立刻生效不需要刷新构建。下面我直接演示如何在 vue3 vite 中真正把它跑起来。3. 选对依赖iconify/vue和unplugin-icons怎么选在你的 vue3 vite 项目里使用 iconify有两条主流路线我建议你先理解差异再决定用哪条。3.1 方案一iconify/vue——开箱即用的 Vue 组件这是最直白的方式直接用官方提供的 Vue 组件。npm install --save-dev iconify/vue然后你就可以在任意组件里这么写template Icon iconmdi:home width24 height24 / /template script setup langts import { Icon } from iconify/vue /script这种方式的优点使用成本极低几乎和img一样默认走 iconify 的公共 API开发时实时拉取不需要重启项目就能预览新图标支持颜色、旋转、翻转等属性直接写在组件上。缺点也很明显运行时会通过 API 从 CDN 加载图标数据要求构建环境或用户环境能访问外网如果项目最终要内网离线部署需要额外配置离线模式。3.2 方案二unplugin-icons——构建期按需打包这个方案本质上是在 Vite 构建阶段把~icons/xx/xx这种路径解析成真实的 svg 数据再交给 Vue 组件渲染。它的特点是最终产物完全不依赖 CDN真正实现按需打包。npm install --save-dev unplugin-icons在vite.config.ts里这样配置import Icons from unplugin-icons/vite export default defineConfig({ plugins: [ Vue(), Icons({ compiler: vue3 }) ] })使用时有几种写法最常见的是直接引入import IconHome from ~icons/mdi/home也可以配合unplugin-vue-components实现自动按需注册连 import 都不用写template i-mdi-home / /template这个方案的好处在生产构建上特别明显——用多少打包多少不用的图标不会出现在产物里。缺点是需要一点配置门槛模板里使用自动组件时需要对命名规则有了解。3.3 我的建议如果你做的是需要长期维护、可能要部署到内网或弱网环境的中后台系统我更推荐unplugin-icons。如果你只是写个 demo、个人项目或纯前端演示页用iconify/vue会更快更省心。我实际在做中后台时用的是两者结合的姿势开发环境用iconify/vue的在线 API 快速调试图标样式生产构建切到unplugin-icons让图标进入本地包。这种做法把“开发体验”和“生产体积”同时照顾到了后面我会给出具体的切换配置。4. 实战接入从安装到第一个图标显示我们直接进代码这一节我以unplugin-iconsunplugin-vue-components为例讲完整的接入链路。为什么拿这个组合讲因为它最能体现“优雅”——你不需要手动 import 任何一个图标组件模板里写了就生效而且构建后产物完全本地化。4.1 初始化项目并安装依赖假设你已经有了一个 vue3 vite 项目如果还没有最快的创建方式是npm create vitelatest iconify-demo -- --template vue-ts cd iconify-demo npm install接着安装两个关键插件npm install --save-dev unplugin-icons unplugin-vue-components这里加一个unplugin-vue-components的原因是为了实现“模板中直接写图标组件名无需手动 import”的效果。它支持很多 UI 库的自动引入但我们现在只需要它协助处理~icons/前缀的图标组件。4.2 配置 vite 插件在vite.config.ts中写入import { defineConfig } from vite import vue from vitejs/plugin-vue import Icons from unplugin-icons/vite import Components from unplugin-vue-components/vite export default defineConfig({ plugins: [ vue(), Icons({ compiler: vue3 }), Components({ dts: true }) ] })这段配置做了三件事Icons({ compiler: vue3 })告诉 unplugin-icons 生成的是 Vue3 组件形式Components({ dts: true })自动生成components.d.ts类型声明文件确保 TypeScript 不报错插件顺序上Icons需要在Components之前这样后续自动解析时才能识别到~icons/这个虚拟模块。4.3 写一个页面验证在src/App.vue里写template div h1iconify 接入测试/h1 i-mdi-home stylefont-size: 24px; color: #409eff / i-mdi-account stylefont-size: 24px; color: #67c23a / i-mdi-cog-outline stylefont-size: 24px; color: #e6a23c / /div /template然后npm run dev页面应该能直接看到三个图标。你可能会问这个i-mdi-home是哪来的这就是 unplugin-icons 的魔法所在。它实际上把i 图标集简称 图标名称转换成了一个虚拟组件背后等价于import IconHome from ~icons/mdi/home如果你不想用自动组件形式也可以手动 importtemplate IconHome stylefont-size: 24px / /template script setup langts import IconHome from ~icons/mdi/home /script两种方式各有适用场景自动组件适合绝大多数常规页面手动 import 适合你需要在script里动态拼装图标的场景。4.4 修改图标的颜色和尺寸在 unplugin-icons 中图标本质上是被转化成内联 svg 的所以设置尺寸和颜色有两种方式通过 CSS直接用font-size控制尺寸用color控制颜色通过 Vue props在组件上写width、height同时显式传入color。例如i-mdi-home width32 height32 color#f56c6c /这和我之前用本地 svg 时的体验完全不同不用打开文件去改 fill 属性直接在模板里控制即可。5. 进阶用法动态图标、批量注册与离线部署基础接入跑通之后你会发现 iconify 真正的大头还在后面。我整理了三个在真实项目中一定会碰到的场景。5.1 动态图标后端返回图标名前端怎么渲染中后台项目里有个常见需求菜单、按钮、甚至页面配置来自后端图标字段是一个字符串比如mdi:user或ep:setting。这时候你不可能预先在模板里写死所有图标需要动态渲染。在 unplugin-icons 体系下可以借助一个辅助函数import { icons } from unplugin-icons // 不推荐全部引入更合理的做法是用iconify/vue里的Icon组件做动态渲染。因为它是运行时从 API/本地缓存取数据渲染的template Icon :icondynamicIconName / /template script setup langts import { Icon } from iconify/vue import { ref } from vue const dynamicIconName ref(mdi:home) /script如果你前一步已经用 unplugin-icons 做静态打包这里额外引入iconify/vue并不会造成太大体积负担因为动态图标本身就意味着“运行时兜底”无法完全避免运行时库的体积。实际项目中这种“静态 动态”混合使用非常常见。如果你想尽量让动态图标也走本地打包可以在构建时扫出所有后端可能返回的图标名生成一个映射表然后用动态组件方案去匹配。具体做法是写一个脚本扫描后端图标配置产出export const iconMap: Recordstring, string { home: ~icons/mdi/home, user: ~icons/mdi/account, setting: ~icons/mdi/cog-outline }然后在 Vue 里用defineAsyncComponent异步加载const iconMap: Recordstring, Component { home: defineAsyncComponent(() import(~icons/mdi/home)), user: defineAsyncComponent(() import(~icons/mdi/account)), } component :isiconMap[iconName] /这种方案在笔者实际项目中跑下来效果很稳既保留了按需打包又解决了后端动态下发需求。缺点就是你需要维护一份“图标名到模块路径”的映射但这份映射可以由脚本从后端配置自动生成人工不需要介入太多。5.2 unplugin-icons 配合 Element Plus 使用如果你用 Element Plus会发现它也自带一套图标但风格和 mdi 等开源集并不统一。我通常的做法是Element Plus 自己的组件内部图标保持原样但业务图标全部走 iconify。不过 implementation 细节上要注意Element Plus 中有一些组件的icon插槽需要接收组件类型例如el-button的:icon属性。你没法直接把i-mdi-home /传进去需要传入的是一个组件定义template el-button :iconHomeIcon返回首页/el-button /template script setup langts import HomeIcon from ~icons/mdi/home /script在这里HomeIcon是一个 Vue 组件对象el-button内部会用component :isicon /把它渲染出来。这是很多人刚上手时容易踩的坑我记得第一次直接把i-mdi-home /写成字符串传进去结果自然是渲染失败。5.3 离线部署彻底切断 CDN 依赖我之前提到iconify/vue默认会请求api.iconify.design获取图标数据。在内网环境中这个请求会被拦截图标显示不出来。要解决这个问题有三个方向完全离线打包直接使用 unplugin-icons不引入iconify/vue手动下载图标 JSON使用iconify/json包将用到的图标 JSON 放在本地再通过iconify/vue的addCollection注册自建 iconify API 代理在自己的服务器上部署 iconify 的 API 服务前端继续使用iconify/vue。从简化运维角度我推荐第一个方向既然已经用 unplugin-icons 了就尽量把所有图标都变成构建时产物不要留运行时依赖。但如果你的动态图标很多完全预打包不可行那就得考虑iconify/json方式npm install --save-dev iconify/json然后在项目入口处import { addCollection } from iconify/vue import mdiHome from iconify/json/json/mdi/home.json addCollection(mdiHome)之后Icon组件在渲染mdi:home时就会优先使用本地注册的集合不再发请求。缺点是要手动维护注册清单图标量大的话比较烦。我在实际中后台工程里采用的最优解是一个脚本去扫描所有后端菜单配置把可能用到的 iconify 图标 JSON 拉下来构建时自动生成注册文件这样既兼顾动态渲染又保证内网可用。流程大概是后端配置里收集所有图标名如mdi:home、fa:user脚本用iconify/json的离线数据提取对应集合/图标的 JSON自动生成src/iconify-offline.ts并在入口import每次构建前跑一次脚本保证新配置的图标不会漏。这个流程跑起来之后开发体验和生产部署都挺舒服的。6. 三种接入方案的对比怎么选最稳这一节我把当前 vue3 vite 项目里接入 iconify 的常见路径放在一起做个对比方便你在立项时快速决策。表格如下方案使用成本构建产物运行时网络依赖动态图标支持推荐场景iconify/vue极低import 即可用仅打入组件代码图标数据运行时拉取需要默认 CDN强传入字符串即可渲染个人项目、原型、外网部署unplugin-icons静态引入中需要写 import 或配置自动注册按需打包每个图标变为 svg/vue 组件无弱需要做映射表中后台、强内网环境、注重体积unplugin-iconsiconify/json离线注册高需要脚本维护打包体积可控运行时加载本地 JSON无强配合 runtime 动态组件可 render大型中后台、菜单/图标动态下发的场景有一点我要特别提醒如果你选择了iconify/vue且项目部署在海外 CDN 访问较慢的地域用户打开页面时图标可能会有短暂延迟。那种情况我建议给Icon组件设置一个初始width/height避免布局抖动。7. 我在实际项目里踩过的坑含完整排查思路这里写几个我在 vue3 vite 项目中接入 iconify 时实际遇到过的问题每一个都是我花了不少时间才排查干净的希望对你有用。7.1 自动生成的图标组件不生效页面没报错现象配置了Components({ dts: true })后模板里写了i-mdi-home /页面空白也没有任何报错。我的排查过程第一步确认vite.config.ts中插件顺序是否把Icons()放在了Components()前面。如果顺序反了有可能虚拟模块解析出问题。第二步查看node_modules/.vite/deps下是否生成了_unplugin-icons的依赖记录如果有但页面还是空白多半是自动刷新没有触发。第三步删掉components.d.ts和node_modules/.vite缓存重新npm run dev问题解决。这个问题的根源是 Vite 的依赖预构建缓存没有及时更新尤其是在你最近改动了vite.config.ts中的插件配置时。后来我给自己定了一个习惯修改 vite 插件配置后先重启 dev server而不是让它热更新能省去很多莫名的排查时间。7.2~icons/路径在 TypeScript 中报错如果项目使用的是vue-tsc做类型检查直接写import IconHome from ~icons/mdi/homeTypeScript 会提示找不到模块~icons/mdi/home。原因是没有类型声明。解决方式有两种手动在env.d.ts中声明declare module ~icons/* { import { Component } from vue const component: Component export default component }让unplugin-icons自动生成类型文件。实际上 unplugin-icons 自带一个icons.d.ts生成机制在Icons({ compiler: vue3, dts: src/types/icons.d.ts })配置后它会自动写入类型声明。我在用的是后者因为省心而且每次新增图标后不需要手动维护类型。7.3 项目部署到服务器后图标全部消失这个坑我印象很深刻。当时开发环境一切正常构建后丢到服务器上页面上的图标全部不显示控制台有大量 404 请求。排查过程先是打开浏览器 devtools发现请求的路径都是https://api.iconify.design/...我第一反应是这是 CDN 请求不能被内网访问。再查构建产物发现产物里确实只有iconify/vue的运行时代码图标数据并没有被打包进去。原因是我当时图省事直接用iconify/vue写动态图标而iconify/vue默认在线请求生产环境又封锁了外网。解决方式就是我在 5.3 节写到的离线注册方案。现在我的项目入口处会做一次本地 JSON 注册并把所有用到的图标数据预取下来。这样图标就是本地资源不仅不会 404加载速度也快了很多。8. 效能提升封装一个自己的 Icon 组件在实际项目里直接使用i-mdi-home这种标签虽然方便但统一封装一个业务组件会让后续维护更轻松。我通常在src/components/AppIcon.vue里这样做template component :iscomp v-bind$attrs / /template script setup langts import { computed } from vue import type { Component } from vue const props defineProps{ name: string size?: string | number color?: string }() const icons import.meta.glob(~icons/*/*, { eager: true }) as Recordstring, Component const comp computed(() { const [prefix, icon] props.name.split(:) const key ~icons/${prefix}/${icon} const comp icons[key] if (!comp) { console.warn([AppIcon] 未找到图标: ${props.name}) return null } return comp }) /script这段代码的核心是import.meta.glob。它会在构建时扫描~icons/目录下所有图标组件并生成一个映射表。你想用mdi:home图标时组件内部把它转成~icons/mdi/home并查表。使用方式就变得非常统一AppIcon namemdi:home :size20 color#409eff /这样做有几个明显的好处业务代码里图标名统一为字符串后端配置的mdi:home可以直接传进来不用写一堆component :is的胶水代码即使有人误传了不存在的图标名控制台也会给出明确的 warning而不是页面某个角落默默缺个图标后续如果要从 mdi 换成其他图标集只需要全局替换name字符串不用改几十个页面里的标签。不过要注意import.meta.glob的eager: true会让所有图标组件都进入主包。如果你项目里使用的图标数量特别多不想一次性全打进来可以改成import { defineAsyncComponent } from vue const icons import.meta.glob(~icons/*/*) // 拿到模块路径后再用 defineAsyncComponent(() icons[key]())这样图标会按需异步加载代价是首次渲染时有个别图标的加载时间。这种改动通常只有图标量上百个的项目才需要考虑几十个图标的情况下eager: true完全没问题。9. 最后分享几个我一直在用的“优雅点”到这里核心内容已经讲得比较完整了。最后我想分享几个我实际使用过程中的细节这些细节让 iconify 的体验从“还不错”提升到“真香”。第一个是图标命名和语义化。我用 iconify 的一个习惯是在业务代码里不直接写死原始图标名而是小组件封装后再引入。比如“用户管理”页面用到的头像占位图标我会在UserAvatarPlaceholder.vue里定义一次之后所有地方都用这个小组件。这样如果后来想要更换图标集只需要改动一个文件而不是全项目搜索替换。第二个是颜色继承。很多时候图标需要在不同场景下显示不同颜色比如菜单展开态和收起态。如果你直接给AppIcon传color那就写死了不如让color留空让图标继承外部 CSS 的颜色设置style scoped .menu-item { color: var(--menu-text-color); } .menu-item:hover .app-icon { color: var(--menu-active-color); } /style因为 unplugin-icons 生成的 svg 默认会继承currentColor所以只要外层设了颜色里面图标的填充色就会跟着变非常方便。第三个是渐变动画的实现。iconify 图标是 svg 内联天然支持 CSS 过渡与动画。我曾经在一个侧边栏里做过一个“hover 图标旋转 90 度”的效果直接加transition: transform .2s就实现了不需要额外处理图标文件。如果将来你在实践里遇到其他的 iconify 集成问题欢迎回来交流我也很乐意把新踩过的坑继续沉淀下来。