
做 Vue3 Vite 项目也有一年多了后台管理系统、官网、中台页面都折腾过图标这块从最开始的 iconfont、Element Plus 自带图标到后来换成 Iconify算是踩了不少坑才找到一套比较顺手的方案。这篇东西就把我自己的实践过程整理出来包括为什么换、怎么接入、离线环境怎么处理、动态图标怎么做以及几个非常容易踩的坑。如果你也在用 Vue3 Vite 开发正纠结图标方案或者已经被 Iconify 的“不显示”问题折磨过那这篇应该能帮上忙。1. 为什么在 Vue3 Vite 里我选择 Iconify1.1 它到底解决了什么问题先说说我之前用的方案。早期项目用 iconfont那时候工作流是这样的去 iconfont 网站选图标加购物车下载字体文件放到项目里然后在 CSS 里定义一堆.icon-xxx::before或者.iconfont类名。听起来还行但真用起来问题很大。一个是图标管理特别散。一个项目几个页面可能用了几十个图标每个人都往同一个 iconfont 项目里加时间久了根本分不清哪些在用、哪些是废弃的。另一个是字体图标本身的渲染问题在部分 Windows 浏览器上会出现图标被裁切、模糊的情况尤其是在不同 dpr 的屏幕上边缘不够锐利。还有多项目复用的场景不同项目的 iconfont 字体相互覆盖样式错乱那叫一个头疼。后来 Vue3 Vite 起来之后项目里流行用 SVG 组件方式比如把每个图标封装成一个 Vue SFCimport IconHome from ./icons/Home.vue然后当组件用。这种方式渲染效果确实好SVG 矢量化、清晰、想怎么改颜色都行。但是维护成本也不低每新增一个图标就要手动建文件、处理命名、统一路径。我需要的其实是一个“图标库”但不是一个“手工维护的图标库”。Iconify 恰恰把这件事做透了。它本质上是一个聚合框架把 Material Design Icons、Phosphor、Tabler、Simple Icons 等等几十个主流开源图标集统一成了一个调用入口。你不需要去某个网站下载、复制文件只需要知道图标的名称比如mdi:home、ph:user-duotone然后组件会帮你把对应的 SVG 拉下来渲染。1.2 加载原理先从 API 说起Iconify 在 Vue3 里最常用的方式是安装iconify/vue然后使用Icon iconmdi:home /这种写法。这个过程中很多人以为图标是本地打包进去的其实不对。默认情况下iconify/vue组件第一次渲染一个图标时会向 Iconify 的公共 API 发起请求把对应的 SVG 数据拉回来后渲染。组件内部有缓存机制同一个图标第二次用就不会再请求了。这个设计的好处是永远按需不管你项目里写了多少图标实际加载的只有页面里真实出现的那些坏处也很明显——如果用户访问环境没有外网图标就白屏了。所以这里会有一个“运行时按需加载”和“离线可用”的矛盾点。后面我会详细讲怎么解决。这里先给大家留个印象Iconify 的默认工作方式是在线按需不是本地打包。理解了这一点后面很多坑就知道缘由了。2. 基础接入在 Vue3 Vite 项目里快速用起来2.1 安装与全局注册接入方式非常简单先把依赖装上npm install iconify/vue然后在main.ts里全局注册import { createApp } from vue import { Icon } from iconify/vue import App from ./App.vue const app createApp(App) app.component(Icon, Icon) app.mount(#app)当然也可以不注册在需要的组件里直接import { Icon } from iconify/vue局部使用。我个人的习惯是全局注册因为图标在后台管理项目里使用频率实在太高几乎每个页面都会用全局注册省得每次 import。2.2 五个基础用法示例注册成功之后使用就是一个组件的事template div !-- 最基本的用法集合名:图标名 -- Icon iconmdi:home / !-- 设置尺寸和颜色 -- Icon iconmdi:account width24 height24 color#409EFF / !-- 多色图标会自动保留原始颜色 -- Icon iconph:user-duotone / !-- 通过 class 控制样式 -- Icon icontabler:settings classicon-btn / !-- inline 模式让图标跟文字基线对齐 -- Icon iconmdi:heart inline / /div /template这里几个属性说明一下width和height默认单位是 px也可以传1.5em、2rem这类 CSS 长度值用 em 单位做响应式图标很方便。color直接设置图标颜色但要注意只对单色图标有效多色图标如ph:user-duotone本身带调色板color不会覆盖所有颜色。inline加上这个属性后图标会被设置为display: inline-block并且垂直对齐方式会调优适合嵌在文字中间。class可以直接用 CSS 覆盖 SVG 的样式颜色用currentColor时也能通过外部color控制。2.3 如何找到想要的图标这是很多人第一次接触 Iconify 时最懵的地方我怎么知道某个图标叫什么名字我的建议是直接去 Iconify 官网搜索搜索框支持中文关键词。比如搜“删除”出来的结果会告诉你mdi:delete、fluent:delete-24-regular、ph:trash等等。你挑一个风格匹配的直接复制字符串就行。但在实际开发中“找到一个图标”只是第一步更关键的是“图标风格统一”。同一个项目里如果又用 Material 风格、又用 Phosphor 风格视觉上会很乱。建议整个项目统一用一套集合比如全用mdi:或全用tabler:。我一般是在项目初期就定下通常是mdi或者tabler因为这两个集合覆盖度广、风格现代。2.4 为什么直接写字符串而不是引入文件很多从 iconfont 切过来的朋友会问直接写iconmdi:home万一写错名了怎么办构建时不会报错吗答案是运行时才知道对错。iconify/vue组件在渲染时会异步拉取数据拉不到就显示一个空节点或默认占位。所以这个方案有一定的“字符串魔法”风险。对于这个问题我的处理是配合编辑器的智能提示来兜底。推荐安装 VSCode 插件Iconify IntelliSense在代码里写iconmdi:ho的时候会自动联想出补全项错误率能降很多。这一点团队协作时尤其有用。3. 解决离线与包体积从运行时加载到构建期按需3.1 一个让人头大的场景build 之后图标不显示先还原一下我遇到过的场景项目开发阶段一切正常vite dev 跑起来图标全部正常显示。然后vite build、部署到测试环境打开页面发现图标全没了有的地方还报 404 请求错误Network 面板里能看到一堆对 Iconify API 的请求全部失败。这个问题的原因就是前面说的——默认方案是运行时在线加载而测试环境没有外网访问权限图标自然是加载不出来的。生产环境更不用说很多企业的后台系统都是内网部署根本不可能让用户浏览器去请求 Iconify 的公共 API。所以如果你用 Iconify第一步就要想清楚你的项目会不会运行在离线/内网环境会的那必须走本地打包方案不要把图标加载这件事留在运行时。3.2 离线加载的核心思路本地注册图标数据要理解解决方案得先明白一个关键概念Iconify 组件本身只负责渲染真正起作用的是“图标数据”。Vue 组件在运行时拿到iconmdi:home它要先去找到一份包含了mdi:home的 SVG path 数据的 JavaScript 对象然后才渲染。所以只要能在运行时把“图标数据”提前放到本地组件就不会去请求网络了。Iconify 生态提供了一系列工具包其中就包括iconify/json它把全量图标数据放在一个 JSON 文件里。而这个数据可以通过addCollection注册到运行时。看一段代码就明白了import { addCollection } from iconify/vue import mdiIcons from iconify/json/json/mdi.json addCollection(mdiIcons)执行完addCollection(mdiIcons)后项目中所有mdi:开头的图标就都有了本地数据组件渲染时直接读缓存不再请求网络。这是离线场景下的一个有效方案。但这样做有一个很明显的问题iconify/json里的mdi.json非常大我印象里动不动就是几 MB 甚至更大。如果直接 import 进来打包首屏 JS 会暴涨Vite 构建产物会明显变大。别笑这是真有人干过的事上生产之后发现首屏加载慢到怀疑人生。3.3 优雅解法一unplugin-icons 构建期按需转换既然全量 JSON 太大那就回到“按需”这个关键词上。不过不是运行时按需而是“构建期按需”用到的图标才是最终产物的一部分。社区里最成熟的方案是unplugin-icons它的用法是配合 Vite 插件把图标变成可以直接 import 的组件。先安装npm install -D unplugin-icons然后在vite.config.ts里配置import { defineConfig } from vite import vue from vitejs/plugin-vue import Icons from unplugin-icons/vite export default defineConfig({ plugins: [ vue(), Icons({ compiler: vue3, autoInstall: true }) ] })有了这个插件之后你在组件里可以直接这样写template IconHome / IconAccount / /template script setup langts import IconHome from ~icons/mdi/home import IconAccount from ~icons/mdi/account /script~icons/mdi/home会被插件在构建期转换为一个内联 SVG 组件输出到产物里就是实实在在的 SVG 字符串完全不需要运行时请求也没有寻找 icon 数据的过程。产物体积可控构建时可以通过 tree-shaking 和按需 import 做到“只打包用到的”。这个方案我用了很久最大的感受就是“确定性强”本地能显示build 之后一定也能显示离线环境也稳得很。唯一的缺点是它要求你在代码里把一个一个图标都 import 进来写起来比Icon iconmdi:home /繁琐一些。对于代码里图标名固定不变的场景这是目前最优解。3.4 优雅解法二从 API 模式切到本地模式的前置检查如果你不想用 unplugin-icons还是想用Icon iconmdi:home /这种字符串风格又必须要离线那就必须解决“只注册用到的图标而不是全量”的问题。一个可落地的思路是自己写一个脚本扫描项目代码里出现的所有iconxxx:yyy字符串去iconify/json里提取对应的数据生成一个精简的 JSON 文件然后在项目入口通过addCollection加载。大概流程是这样在项目里安装iconify/json作为 devDependency。写一个 Node 脚本用正则扫描src目录下所有.vue和.ts文件找出iconmdi:home这类字符串。根据集合名读取iconify/json/json/集合名.json只保留需要的那几个图标数据。把结果写成一个src/assets/icons/local-icons.ts之类的文件里面统一执行addCollection。这个方案的好处是保留了Icon组件的动态性也解决了离线问题坏处是脚本要自己维护而且如果图标名是通过变量拼接出来的比如iconmdi: name正则就扫不到了需要额外处理。我个人建议图标名都是硬编码场景优先用 unplugin-icons如果确实需要动态字符串再用脚本提取方案。4. 动态图标场景的优雅处理4.1 后台管理系统最常见的需求菜单图标由后端返回这是做后台管理系统逃不开的需求菜单表在数据库里图标字段是字符串接口返回[mdi:home, mdi:user, ph:gear]这种数据前端拿到之后直接把字符串丢给Icon :iconmenu.icon /渲染。如果是本地 dev 环境默认在线加载模式下一切都很顺利因为组件会自动去请求 API 拿数据。但这个模式一旦上了内网部署整个菜单的图标就全空了这在 3.1 已经说过了。这种“动态字符串 离线可运行”的需求我就没法用 unplugin-icons 了因为插件是编译期展开的不可能知道运行时的menu.icon值是什么。这个时候我的方案是在入口处注册项目实际会用到的那批图标数据配合后端约定限制可选的图标集合。实操上我现在的做法是这样的在后端配置菜单图标时限定了只能从我们维护的“白名单”里选。前端同步维护一个白名单的 icon 数据文件大概长这样import { addCollection } from iconify/vue import { icons as mdiIcons } from iconify-json/mdi // 这里也可以自己构造一个精简对象只保留用到的图标 addCollection(mdiIcons)iconify-json/mdi是mdi集合的独立 npm 包比直接引全量iconify/json小很多但这仍然是整套 mdi 集合加载多少取决于集合大小。如果你们项目只用了 20 个图标全量注册还是有点浪费。更极致一点我会让后端在接口里直接返回“图标 key”而不是完整的 iconify 字符串比如返回home、user前端维护一个映射表const iconMap: Recordstring, string { home: mdi:home, user: mdi:account, gear: mdi:cog }这样一方面保证后端配置不会被乱填另一方面配合提取脚本把映射表里出现的这几个图标单独提取成精简 JSON运行时只注册这几个。有人可能会说这不是把动态图标变成静态了吗其实不是。映射表本身还是反射业务动态性的只是把“有限集合”这件事管住了。做中后台系统的人应该都懂菜单图标如果允许随意填最后页面上一定会出现各种风格迥异的图标视觉质量很难保证。从产品角度讲限制图标集合反而是一种规范。4.2 通过 Icon 组件的动态能力实现运行时切换有些场景需要做主题切换或者用户自定义图标比如用户在设置页选一个头像图标、选一个侧边栏图标这种需求其实用Icon :iconcurrentIcon /就能跑通。前提是所有可选图标的数据都已经在本地注册过了或者运行环境允许在线加载。如果不是离线环境那直接在线加载反而简单因为可选图标是有限的在线请求也只会发起那几次之后的缓存命中会很稳定。如果是离线环境建议在图标选择页面单独把可选列表用addCollection注册到缓存。比如 200 个候选图标全量注册虽然比 20 个要大但比起整套mdi.json已经小很多了而且只在需要时注册不会影响首屏。4.3 动态图标 组件库的联动如果你用的是 Element Plus导航菜单的图标插槽是支持自定义内容的。可以这样玩el-menu-item :indexmenu.path el-icon Icon :iconmenu.icon width18 height18 / /el-icon span{{ menu.title }}/span /el-menu-item不过这里有个小细节需要注意el-icon内部默认继承父级字号和颜色Iconify 的Icon组件渲染出来是svg它的width和height如果你传了固定数字就会覆盖掉el-icon设定的尺寸。想要跟随菜单的尺寸变化最好别传死宽度改用 CSS 类去控制或者用width1em height1em让它跟随字号。5. 常见问题排查速查表5.1 图标不显示页面空白遇到这个问题先按下面的顺序排查看 Network 面板有没有请求api.iconify.design之类的地址如果有且失败了大概率是网络原因离线环境或域名被限制。解决办法就是走本地注册或 unplugin-icons 构建打包。看图标名是否写对mdi:home和mdi:Home是两个不同的图标前者存在后者可能不存在。复制图标名时注意大小写。看是不是集合名不存在Iconify 的集合名是确定的mdi、ph、tabler都可以但你不能凭空造一个比如myicons:home如果没注册数据肯定渲染不出来。看是不是注册时机不对如果是addCollection方案一定要保证注册代码在组件渲染之前执行。比如在main.ts入口文件顶部执行不要在某个页面里注册完立刻渲染异步顺序容易出问题。5.2 颜色改了不生效Iconify 图标分两种单色和多色。单色图标的 SVG 路径用currentColor所以外部控制color就有效。多色图标自带多个色板比如ph:user-duotone有主色和辅色color属性只能作用于currentColor部分未必能把所有颜色都覆盖掉。如果你必须改多色图标的颜色一个思路是用 CSSmask方式把 SVG 当遮罩用。.icon-mask { background-color: #409EFF; -webkit-mask: url(data:image/svgxml;utf8,svg .../svg) no-repeat center / contain; mask: url(data:image/svgxml;utf8,svg .../svg) no-repeat center / contain; }这样整个图标都只显示背景色相当于强制单色化。但我其实不太推荐在项目里用多色图标除非你确认不想统一控制颜色。日常后台管理项目统一用单色图标通过color控制交互态hover 变蓝、禁用变灰要方便得多。5.3 图标模糊或有锯齿Iconify 走的是 SVG 渲染正常来说不存在位图那种模糊问题。如果你觉得图标模糊大概率是给了非整数尺寸比如width23.5px浏览器在缩放时做了插值。建议把宽度高度设置成整数。另外某些集合的原始图标自带小尺寸 path放大到 64px 以上时边缘曲线会显得不够细腻这是图标设计精度的问题不是渲染问题。这种时候换一个同含义但更精细的集合比如mdi通常比某些极简集合更适合大尺寸展示。5.4 和 UnoCSS/Tailwind 的冲突如果项目里配置了 UnoCSS 或 Tailwind图标类名冲突也是一个常见问题。比如你在 class 里写了hover:scale-110预期图标放大但因为 Icon 组件内部结构或 CSS 优先级问题没生效。这种时候建议用调试工具查看 SVG 元素实际被哪些样式覆盖了。如果要用 Tailwind 控制 Icon 颜色且图标是单色的直接在父级元素定义文本颜色就好了因为currentColor会顺着样式继承。不需要显式给 Icon 传color。span classtext-gray-400 hover:text-blue-500 Icon iconmdi:settings / /span5.5 SSR / 构建时 preload 相关问题如果你不是纯 Vite SPA而是用 Nuxt 或 VitePress 这类 SSR 框架用iconify/vue可能需要额外处理。SSR 环境下组件会尝试在服务端预取图标数据这时如果网络不可达服务端渲染出来的 HTML 里就没有图标内容等客户端 hydration 后再补渲染视觉上会有闪烁或者布局偏移。Nuxt 生态的话可以直接用iconify/vue配合ssr: false强制只在客户端渲染或者用官方针对 Nuxt 的iconify/nuxt模块它做了预取处理。如果你只是做简单静态站建议直接用 unplugin-icons 把图标编译成静态 SVG没有运行时请求也就不用关心 SSR 了。6. 我现在的最优实践组合最后分享一个我目前推荐给团队的组合方式。静态图标场景侧边栏 logo、按钮图标、页面装饰图标优先用 unplugin-icons图标通过~icons/xxx直接 import构建产物里就是内联 SVG速度最快、最稳定、无任何网络依赖还能拿到自动补全。动态图标场景菜单项、用户可配置图标用iconify/vue的Icon组件 本地注册。前端维护一个图标映射表同时保证所有可能出现的图标名在入口处已经通过addCollection注册到缓存。这样既保住了动态性又不需要请求外部 API。这套组合运行下来实际开发体验是写代码时不需要去找文件、不需要担心路径问题构建产物里只有用到的东西部署到内网环境也不慌因为没有运行时请求。踩过几次坑之后我现在的结论是Iconify 最大的价值不在于它的某个 API而在于它把所有图标统一定义成了一个“名称 数据”的表达方式让本地存储、网络缓存、构建期编译这些都是围绕同一个抽象展开的。理解了这个抽象关系你在 Vue3 Vite 里用什么方式接入其实就是组合题了。