做 Vue 项目,尤其是管理后台或者中后台系统,图标是天天都要打交道的东西。选图标库这件事,看着不起眼,实际上非常影响开发效率和最终效果。我在实际项目里见过不少团队,一开始随便找了一套图标就开干,等到后面需要统一风格、按需加载,或者让后端下发图标名时,才发现当初的选择有多被动。
这篇文章把我这几年在 Vue 项目里实际用过的免费图标库做一个系统梳理,同时把选型思路、接入方式、避坑点都放进来。适合正在搭新项目、或者想把手头项目的图标方案换掉的开发者。免费方案不等于凑合,但要用对地方,才能既省钱又不给自己挖坑。
1. 为什么"免费"会成为第一优先级
1.1 免费图标库的授权边界
很多人理解的"免费",就是不要钱随便用。但在图标库这件事上,"免费"有两个完全不同的层次:一种是开源协议授权,源码和资源可以自由使用、修改甚至商用;另一种是平台性质的"免费下载",但版权归属并不清晰,或者只允许个人使用,禁止商用。
拿 Font Awesome 举例子,它有一个非常大的免费图标集,覆盖了日常开发 90% 以上的场景,使用协议对一般项目也算友好。但 Pro 版本那部分图标,比如一些特殊符号、带 Solid 风格的高级图标,免费版里根本没有。如果你在项目里看到某个图标特别好看,一查发现是 Pro 专属,那就只能要么放弃,要么换方案。更麻烦的是,有些模块的授权条款会注明"如果你在出版物或成品中使用了本图标,需要标注出处",这在给客户交付的商业项目里,就是一个容易被合规部门盯上的点。
我自己的原则是:商业项目优先选 MIT、ISC、Apache 这类宽松开源协议的图标库,授权清晰,没有历史包袱。个人项目或者内部工具,自由度可以大一点,但也要把免责条款看清楚,尤其是从图标分享平台上下载的素材,出处和版权归属往往是一笔糊涂账。
1.2 免费方案的隐性成本
免费的另一层隐性成本是"维护成本"。很多漂亮的图标库火一阵子就不更新了,或者作者转型去做了其他产品,GitHub 仓库停更两年,新出的图标风格完全没人维护。你在项目里引入了这个库,刚开始觉得挺好,第二年想补几个新图标,发现要么没有,要么风格不搭,要么版本兼容性出了幺蛾子。这种隐形成本比花钱买授权还恶心。
所以我选免费图标库,不只是看图标好不好看,还看三个点:社区活跃度、更新频率、周边生态。社区活跃意味着你踩的坑大概率有人踩过了,搜索一下就能找到解决方案。更新频率决定了这个库能不能跟上系统版本的迭代。周边生态则影响它在 Vue 里的接入方式——是官方提供 Vue 组件,还是需要自己封装一层。
2. 主流免费Vue图标库横向对比
在正式给出建议之前,先把市面上能用的免费方案拉出来做个对比,这样后面再讲细节的时候,大家心里有个谱。
| 方案 | 授权类型 | Vue接入方式 | 按需加载 | 适用场景 |
|---|---|---|---|---|
| Iconify | 各图标集跟随原开源协议 | unplugin-icons 编译为组件 | 支持且推荐 | 需要跨图标集、按需引用 |
| Tabler Icons | MIT | @tabler/icons-vue 组件 | 支持 | 风格统一的中后台项目 |
| Heroicons | MIT | @heroicons/vue 组件 | 支持 | Tailwind CSS 项目 |
| Lucide | ISC | lucide-vue-next 组件 | 支持 | 轻量、简洁风格 |
| Font Awesome | CC BY 4.0 / Pro 部分需授权 | @fortawesome/vue-fontawesome | 需手动配置 | 老项目迁移、图标种类多 |
| Element Plus 自带 | MIT | @element-plus/icons-vue 组件 | 支持,但需要全局注册 | 基于 Element Plus 的后台 |
| Ant Design Vue 自带 | MIT | @ant-design/icons-vue 组件 | 支持 | 使用 Ant Design Vue 的项目 |
| iconfont | 图标版权易模糊 | symbol / fontface | 无法按需,全量加载 | 团队自建图标库、定制需求 |
2.1 图标聚合平台型:Iconify
Iconify 大概是我目前最推荐的方向。它本身不是一个图标库,而是一个图标聚合平台,把几十个开源图标集的图标统一起来,你用一套接口去调用。更重要的是,配合 unplugin-icons 这个构建工具,它可以在编译阶段把用到的图标"扣出来",生成独立的 Vue 组件,想用几个就打包几个,Tree-Shaking 直接拉满。
Iconify 的使用体验有一点像"图标界的 npm":你不需要关心图标从哪个源来,只需要知道图标的名称规范。比如tabler:home表示 Tabler 图集的 home 图标,heroicons:home表示 Heroicons 的 home 图标。命名空间前缀一隔,多套图标风格可以共存,这在项目里非常实用——后台管理界面用 Tabler 的统一风格,某些特殊场景想用一个别的图集的图标,也不需要再引入一个完整的库。
唯一的注意点是,它本身是一个巨大的数据生态,直接在线引入会有网络依赖。我的建议是把它和 unplugin-icons 结合,在构建时把图标打包成本地组件,这样发布后的项目是完全离线的,不依赖任何 CDN,内网部署也不怕。
2.2 独立设计型:Tabler、Heroicons、Lucide
这三家是我个人在各项目里翻牌率最高的几个独立图标库。
Tabler Icons 走的是简洁、中性、线框风格,线条粗细统一,视觉上非常耐看。它对中后台系统极其友好,所有图标大小、比例、对齐都做得非常规整,放在表格、菜单、按钮里特别协调。它的 Vue 组件包 @tabler/icons-vue 支持按需引入,不用的图标不会进入打包结果,体积控制得非常好。我自己在大部分后台项目里,首选方案就是 Tabler。
Heroicons 是 Tailwind CSS 团队维护的图标库,设计风格偏向圆润、柔和,同时提供 outline(线框)和 solid(实心)两套风格。如果你项目里用了 Tailwind,选 Heroicons 会特别顺手,因为图标尺寸、颜色都和 Tailwind 的实用类配合得很好。比如h-5 w-5 text-gray-500这样写,图标颜色和大小就直接被控制了,完全不需要额外写 CSS。
Lucide 是原 Feather Icons 的继承者,同样走轻量、简洁的路线,但比 Feather 多维护了大量新图标。它和 Tabler 的风格有一定相似,但线条稍微细一点,适合追求精致感的小型项目或 C 端页面。Vue 3 对应的是 lucide-vue-next 这个包,同样支持按需引入。
2.3 组件库自带图标
如果你项目里已经引入了 Element Plus 或者 Ant Design Vue,那么我建议你先认真看看组件库自带的那套图标。很多人一上来就去找第三方图标库,忽略了组件库自带的图标其实已经够用、而且和组件库设计语言完全统一。
Element Plus 自带的是 @element-plus/icons-vue,全部都是 SVG 组件,MIT 授权,官方把所有图标都导出来了。用法上可以全局注册,也可以按需引入。关键点在于:组件库自带的图标在视觉上和老牌的组件库交互模式是配套的,比如菜单的展开收起、表格里的排序箭头、弹窗的关闭按钮,这些都非常契合后台管理系统最常见的交互习惯。我在实际项目里,习惯先把自带图标用上,只有遇到确实没有的图标,才去查第三方图标库。
Ant Design Vue 也是一样,@ant-design/icons-vue 提供了大量风格统一的图标。这一点在团队协作里其实很被低估:图标风格和组件库一致,界面看起来就是一个整体,而不是七拼八凑的感觉。
2.4 国内团队常用的 iconfont 方案
iconfont 是阿里出品的一个图标管理平台,在国内团队里使用率非常高。它的核心优点有两个:一是可以在线管理自己的图标库,设计同学把切图上传后,前端可以自己选择图标打包成字体文件或者 SVG symbol,整个流程非常方便;二是 png、svg、字体、base64 多种格式都能输出,不管老项目还是新项目都能找到对应的接入方式。
但 iconfont 也有让我比较警惕的地方。平台上大量的图标是用户主动上传的,版权归属并不总是清晰。如果你只是做内部系统,问题不大;但如果项目要交付给客户,我建议只使用 iconfont 上官方出品的图标库,或者自己团队上传的原创图标。另外,iconfont 的字体方案在跨平台渲染时会有细微差异,偶尔会出现图标在部分浏览器里位置偏了几个像素的情况。
如果你已经在用 iconfont 管理自己团队的图标,那我的建议是:优先用它的 SVG symbol 方案,别用字体。symbol 方案本质上是把 SVG 图标放到一个<symbol>集合里,然后用<use>标签引用,渲染效果好、不存在字体渲染的兼容问题,而且可以在运行时动态改颜色。
3. 选型思路与落地适配
3.1 按项目类型选
没有"最好的图标库",只有"最合适的图标库"。根据项目类型做一个初步筛选,能帮你省掉很多纠结时间。
管理后台、中后台系统,我推荐先看 Element Plus 或 Ant Design Vue 自带图标,配合 Tabler Icons 补缺口。这类项目界面密度高、表格多、操作按钮多,图标的语义要非常明确,不能太花哨。自带的图标配上组件库正好,第三方补缺口时选 Tabler 这样的中性线框风格,视觉能统一。
C 端产品、营销页面,可以优先考虑 Heroicons 或 Lucide。这类页面对于图标的情感和调性要求更高,Heroicons 圆润的风格更容易传达友好、亲切的感觉,Lucide 则更轻快。如果设计团队已经有完整的视觉规范,那就让设计导出 SVG,再统一封装成 Vue 组件。
低代码平台、通用组件库类项目,建议直接使用 Iconify 体系。因为这类项目需要应对各种不可预知的使用场景,你不能替用户决定只能用什么图标,而是应该通过 unplugin-icons 这样的方式,让使用方能够在配置层选择不同的图标集和图标名。
3.2 字体图标还是 SVG
这里想把一个非常关键的技术决策讲透:选字体图标(font-face)还是 SVG 图标。
老牌项目里常看到用字体图标的方式,就是先引入一个 iconfont.css,然后在 DOM 上写<i class="iconfont icon-home"></i>。这种方式实现简单,只需要一个字体文件和一个 CSS 文件,图标就能用。但它的缺点也很明显:字体图标在渲染时是文本,默认受 CSS 的font-size、color控制,想要多色图标基本不可能,而且小字号下锯齿和模糊问题时有发生,不同操作系统对字体的抗锯齿处理也不一致。
SVG 图标则完全没有这些顾虑。第一,SVG 是矢量图形,任意尺寸下都清晰;第二,多色表现和渐变支持都比字体图标好太多;第三,SVG 可以通过 viewBox 灵活控制比例,不依赖 font-size。现代前端工程里,SVG 已经成为事实标准。所以我的建议是:新项目一律用 SVG 方案,老项目能迁移就尽量迁移,不用犹豫。
3.3 图标风格和技术栈的匹配
还有一个容易被忽视的点,是图标风格和技术栈的匹配。Vue 本身没有立场,但你选用的 UI 组件库、CSS 方案会影响图标的观感。
比如你的项目用了 Naive UI,它的设计语言偏现代、简洁,你再搭配一个非常厚重、描边粗的图标,界面会显得特别重。反过来,如果项目用的是偏硬朗的设计风格,再用一堆圆润图标,又会有种不协调感。一般来说,线框风格、细线条的图标(Tabler、Lucide)适配面最广;圆润风格(Heroicons)适合偏柔和的产品;实心风格(Font Awesome Solid)视觉冲击力强,但大面积使用容易显得拥挤。
另外,使用 Vue 组件形式的图标时,最好在项目里封装一层通用图标组件,统一入口。比如我自己都会在项目里封装一个<BaseIcon>组件,屏蔽掉底层图标库的差异。这样以后要换图标库,只需要改这一个组件,而不是全项目搜索替换。
4. 实操:把 Vue3 项目中的图标跑起来
4.1 使用 unplugin-icons 按需加载
先说我自己最常用的一套方案:Vue 3 + Vite + unplugin-icons,配合 Iconify 图标集。
安装依赖:
npm install -D unplugin-icons npm install -D @iconify/json然后在 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, }), ], })配置好之后,在组件里直接引入图标:
<script setup> import IconHome from '~icons/tabler/home' import IconSetting from '~icons/tabler/settings' </script> <template> <div> <IconHome class="h-5 w-5 text-gray-500" /> <IconSetting /> </div> </template>~icons/tabler/home这个路径,就是告诉插件"从 Tabler 图集里找 home 图标"。插件会在编译阶段把这个图标转成一个 Vue 组件,没有用到的图标完全不会进打包结果。这种方式非常透明,IDE 跳转也方便,因为它直接可以生成源码。
如果想在模板里直接用<i-tabler-home />这样的标签,可以配合 unplugin-vue-components 使用:
import Components from 'unplugin-vue-components/vite' import IconsResolver from 'unplugin-icons/resolver' export default defineConfig({ plugins: [ vue(), Components({ resolvers: [ IconsResolver({ prefix: 'i', }), ], }), Icons({ compiler: 'vue3', autoInstall: true, }), ], })这样模板里写<i-tabler-home />就行,不需要在 script 里手动 import 了。需要注意的是,autoInstall: true会在第一次编译时自动帮你安装对应的图标集依赖,比如 @iconify-json/tabler。如果你在离线环境构建,这步会卡住,所以最好先在联网环境把依赖安装好,再提交构建。
4.2 组件库自带图标的注册
以 Element Plus 为例,全量注册图标最省事:
import { createApp } from 'vue' import App from './App.vue' import * as ElementPlusIconsVue from '@element-plus/icons-vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' const app = createApp(App) for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } app.use(ElementPlus) app.mount('#app')注册之后,在模板里配合 el-icon 使用:
<template> <el-icon :size="20" color="#409efc"> <Home /> </el-icon> </template>在实际项目中,我不太建议无脑全局注册所有图标。全量注册会把所有图标都打进包里,哪怕你只用了其中 20 个,Tree-Shaking 也救不回来。更好的做法是只注册常用的几个,或者干脆按需引入:
<script setup> import { Home, Setting } from '@element-plus/icons-vue' </script> <template> <Home /> </template>这样打包时只会保留用到的图标组件,体积更可控。全局注册适合项目里图标用得很散、数量很多,而且团队图省事的场景;如果你在意包体大小,就按需引入。
4.3 iconfont symbol 方式的接入
如果团队已经在 iconfont 上维护了自己的图标项目,我建议走 SVG symbol 方式接入,而不是下载字体文件。
在 iconfont 网站勾选需要的图标,点击"下载至本地",选择 Symbol 方式。下载后会得到一个 iconfont.js 文件,里面是所有图标的<symbol>定义。把它放到项目的 public 目录,然后在 index.html 里引入:
<script src="/iconfont.js"></script>接着在任意组件里使用:
<template> <svg class="icon" aria-hidden="true"> <use xlink:href="#icon-home"></use> </svg> </template>这里#icon-home是 symbol 的 id,要和你下载的图标名称保持一致。使用这种方式,svg 的宽高默认是 1em,所以它的大小会跟随font-size,也可以通过 CSS 覆盖width和height。颜色方面,单色图标默认使用currentColor,所以color: red就能改变图标颜色,非常方便。
我一般会在项目里封装一个SvgIcon.vue组件:
<script setup> defineProps({ name: { type: String, required: true, }, }) </script> <template> <svg class="svg-icon" aria-hidden="true"> <use :xlink:href="`#icon-${name}`"></use> </svg> </template> <style scoped> .svg-icon { width: 1em; height: 1em; vertical-align: -0.15em; fill: currentColor; overflow: hidden; } </style>这样项目里需要动态显示不同图标的时候,只需要传 name 变量过去就行,不用写一长串条件判断。
4.4 动态图标的处理
真实项目里经常遇到一种需求:菜单数据放在数据库里,后端给前端一个图标名字,前端要渲染出对应图标。这时候直接用字符串字符串拼组件名是不行的,Vue 里不能直接把字符串当作组件渲染。
我自己常用的一种做法,是维护一个图标名到组件的映射表:
// icons/index.ts import { Home, Setting, User, ShoppingCart } from '@element-plus/icons-vue' export const iconMap = { home: Home, setting: Setting, user: User, cart: ShoppingCart, }然后在模板里用<component :is="iconMap[iconName]" />渲染。注意,如果后端下发的名字不在映射表里,要做兜底处理,我习惯用一个默认图标,避免页面直接白屏报错。
对于 unplugin-icons 接入的场景,可以用类似的方式维护:
import IconHome from '~icons/tabler/home' export const iconMap = { 'tabler:home': IconHome, }如果你觉得手写映射表太麻烦,也可以考虑用 Vite 的import.meta.glob自动加载图标文件,但我不太推荐为了"自动"牺牲可读性和体积控制。映射表虽然朴素,但它最稳、最直观,而且做权限管理时还能控制哪些图标允许被后端下发,避免恶意字符串造成问题。
5. 常见问题与排查技巧实录
5.1 图标不显示
图标不显示是出现频率最高的问题,原因也很多,我把典型的几种整理了一下:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 所有图标都不显示 | 字体文件路径错误,或者 CSS 未加载 | 确认 public 路径,检查 network 面板 |
| 个别图标不显示 | 图标 id 写错,或该图标未添加到图标库 | 检查 symbol 的 id 和实际名称 |
| 图标显示为方块 | 字体文件加载失败,或者字体格式不被浏览器支持 | 换用 SVG symbol 方案,或补全字体格式 |
| 打包后图标丢失 | base 路径配置问题,字体资源被处理到错误目录 | 检查 vite 的 base 和 assetsDir 配置 |
| 动态图标不显示 | 映射表里没有对应的键 | 在映射表增加兜底逻辑,或检查后端返回值 |
其中字体文件路径问题,在 Vue 项目里有几种情况。如果你用了vue-router的 history 模式,并且部署在子路径下,字体文件的绝对路径就需要加上 base 前缀,否则打包后查不到资源。我一般会建议把字体文件放到 public 目录下,用绝对路径引用,同时检查部署后的实际 URL,确保资源能被正确拿到。
5.2 动态组件图标不生效
用<component :is="xxx">渲染图标时,最容易忽略的一点是:xxx 必须是一个组件对象,不能是字符串,也不能是undefined。如果后端接口返回慢,初始数据是null,此时<component :is="null">会渲染成一个空注释节点,表现就是"图标位置什么都没有"。
遇到这种情况,一是要做数据的默认值处理,二是映射表取不到值时给一个默认组件。还有一个小细节:Vue 在动态组件切换时会对组件类型做缓存,如果动态返回的组件在多次切换中变化不大,代码上不需要额外处理;但如果需要监听图标变化做动画等操作,记得在<component>外层加key来强制重建。
5.3 颜色和大小不受控制
SVG 图标的颜色不受控制,通常问题出在 SVG 内部写死了fill属性,或者路径上自带颜色。很多图标库的源码里面是有fill="currentColor"的,但如果你从某个平台复制的 SVG 自带了fill="#333",那无论外面怎么设color都没用。
解决办法有两个:一个是选择提供 outline 风格、默认使用 currentColor 的图标库,接入时不需要做处理;另一个是自己在封装组件时强制设置fill: currentColor或者用 CSS 覆盖。
大小方面,SVG 图标如果使用width="1em" height="1em",就能跟随文字大小。某些图标库组件默认是 24px 或 1.5em 等固定尺寸,如果和你项目里的文字比例看着不协调,记得通过外层 CSS 显式覆盖尺寸。
5.4 打包体积暴涨
如果你用的图标库没有被按需加载,打包体积很容易直接多出几百 KB。尤其像 Font Awesome 这种,如果全量引用了 CSS 和字体文件,哪怕只用了 5 个图标,也会把所有字体格式的数据都打包进来。
针对这个问题,我总结了一套检查路径:
- 看打包产物里是否包含
woff2、ttf、eot等字体文件,如果有且页面里只用了少量图标,说明是字体方案全量引入。 - 看是否有很大的 JS chunk,包含很多 SVG 组件源码,说明组件库图标被全量注册或全量 import 了。
- 用
rollup-plugin-visualizer或 Vite 的build.rollupOptions输出分析报告,确认占据体积的是哪部分代码。
定位到原因之后,按需引入就能解决大部分问题。如果项目实在没法全面按需化,至少要在关键路径上做优化,比如首屏只加载必要的图标,次要页面图标通过懒加载方式引入。
5.5 多语言和 RTL 布局的坑
如果项目要支持多语言,特别是希伯来语、阿拉伯语这类从右往左布局的语言,图标的箭头、方向类图标需要特别注意。在 RTL 模式下,表示"前进"的右箭头可能实际应该指向左,表示"返回"的左箭头则相反。
最稳妥的做法是方向性图标不用箭头加文字的组合,而是用语言切换时动态渲染不同图标;如果条件不允许,也可以为图标做水平翻转,SVG 里用transform: scaleX(-1),Vue 里可以封装一个flip属性来支持。
这块很多人会忽视,但等到 UI 验收的时候才暴露,返工成本不小。
5.6 图标库版本升级带来的破坏
我不会轻易升级项目里的图标库依赖。图标库的版本升级,尤其是大版本升级,可能出现图标名变化、组件 API 调整、样式默认值改变等破坏性变更。比如某个图标改了个名字,你全项目搜不到,只有用户反馈某个页面图标不见了。
升级前,我会先看一眼 changelog,然后全局搜一下用到的图标名是否在变更列表里。如果升级涉及的改动面太大,宁可守着旧版本,也不要追新。毕竟图标库是一个"能稳定用就不折腾"的依赖。
6. 一些个人使用体会
做技术选型时,很多人会把注意力放在"这个库够不够好用"上,但忽略了"这个库能不能陪我走完整个项目周期"。图标库虽然只是项目中很小的一环,但它一旦选定,就会遍布你的模板、组件、文档和设计规范里,换起来非常痛苦。所以我的经验是:花一点时间做选型调研,远远比后期迁移划算得多。
我现在的新项目,默认组合是这样:如果项目用 Element Plus,图标先上@element-plus/icons-vue,遇到缺口再补unplugin-icons按需引入 Tabler;如果项目用 Tailwind,首选 Heroicons;如果甲方有完整的 VI 体系,那所有图标都由设计出 SVG,前端用 iconfont 的 symbol 或者直接封装SvgIcon组件去管理。至于字体图标方案,我基本只在老项目维护时才会碰。
最后再分享一个小技巧。不管是哪种图标库,我都建议在项目里约定好统一的图标命名规则。比如菜单图标全部用icon-前缀,操作按钮图标统一用动词开头,状态图标统一用状态名称。这样后面前端和后端的沟通成本会低很多,尤其是菜单图标要靠后端配置的时候,一套清晰的命名规范能让你少掉不少头发。