做跨端开发这些年,我见过太多次“图标”引起的线上事故。同一个页面,H5 里好好的,打包成小程序就变方框,真机再一测干脆空白。后来我在 uni-app 项目里统一用 iconfont 图标库来管理所有图标,靠一套字体文件加一套封装组件,把小程序、H5、App 三端的图标表现彻底拉齐了。这篇文章就是把我在多个项目里踩过坑、验证过的完整使用流程整理出来,从选型讲到落地,再附上一份排查清单,给正在被图标问题折腾的 uni-app 开发者一个能直接照着做的参考。
整套方案的核心思路并不复杂:把 iconfont 平台上的矢量图标以字体文件或 SVG 雪碧图的方式接入 uni-app,让业务代码不再关心图标资源在某个端具体怎么加载,只面对统一的“图标名 + 尺寸 + 颜色”接口。适合刚接触 uni-app 的新手,也适合正在做跨端项目重构、想统一图标方案的团队。下面我把每个选择背后的“为什么”也一起讲清楚,这样你拿到的不只是步骤,而是举一反三的判断力。
1. 为什么值得在 uni-app 里用 iconfont:选型思路与方案对比
1.1 常见图标方案的优劣对比
在决定用 iconfont 之前,先搞清楚市面上的方案各有什么“病”,你才能理解这个选择不是跟风,而是多次踩坑后的理性判断。
本地图片方案最直观,设计师切个图丢进static,用<image>标签一引就完事。但问题也很明显:首先是多端分辨率适配,iPhone 和安卓机的逻辑像素不同,需要准备@2x、@3x多套切图,或者用mode="widthFix"硬缩放,结果就是边缘发虚;其次是请求数量,页面图标一多,包体变大、首屏加载变慢,尤其是小程序里图片过多会明显卡顿。网络图片更省事,但依赖 CDN,图片服务一旦出问题或者域名没有配到合法列表里,整个页面直接崩。
SVG 文件方案比图片好一些,至少是矢量的,缩不糊。但在 uni-app 里管理一堆.svg文件,要么写个图片组件去逐个加载,本质上还是多一次网络请求;要么在页面里内联写<svg>标签,但微信小程序根本不支持这种行内 SVG 直接渲染。这个方案在小程序端基本走不通。
所以在跨端项目里,最终长期稳定服役的方案是 iconfont 的字体图标为主、SVG Symbol 为辅。字体图标的本质是一个字体文件,图标以字形的方式存在,用font-size控制尺寸、用color控制颜色。这句话说出来其实就已经赢了一大半:跨端渲染一致、体积小、改样式方便。而且字体文件是纯资源文件,不参与组件树渲染,性能上也有优势。
1.2 iconfont 真正解决的核心问题
为什么特别强调“在 uni-app 里”?因为 uni-app 的编译目标是多端,而不同端对资源处理策略完全不同。拿字体文件来说,H5 端就是常规 CSS,走网络加载没问题;微信小程序端本地字体文件路径不能直接引用,得 base64;App 端 Vue 页面可以用字体,nvue 页面又需要单独处理。如果不用一个统一的图标方案,等于每个端都要单独写一套加载逻辑,代码分支多到无法维护。
iconfont 平台把图标管理这件事简化成了三步:选图标、建项目、生成代码。整套流程属于典型的“把复杂度留在工具内部,让使用方只面对一个可预期接口”。字体文件本身只有一个,体积通常在几十 KB,图标多一些也就两三百 KB,相比整页图片方案优势非常明显,尤其适合对包体积敏感的小程序场景。
当然,并不是所有场景都适合 iconfont。极复杂的扁平插画、需要整组配色且动态变化的图形,还是交给 PNG 或 SVG 更直接。但在绝大多数业务图标场景里,iconfont 的性价比是最高的。把方案边界划清楚,再用它就踏实了。
2. 动手前的准备:图标挑选、项目创建与下载配置
2.1 从选图标到创建项目
第一件事当然是打开 iconfont 平台,搜索需要的图标。有设计师团队的可以直接把设计资源上传到平台统一管理,这算团队资产沉淀;个人开发就直接从公共图标库挑。这里有个容易被忽略的细节:挑选时尽量选线条简洁、单色居多的图标,这类图标用字体方式渲染最干净。那种本身就带多色的扁平插画图标,要留到后面用 Symbol 方式单独处理,不要混进 Font class 的字体文件里。
选完图标后,把所有想用的图标加入购物车,然后进入购物车选择“添加至项目”。这时候会提示创建新项目或加入已有项目。命名上我强烈建议不要用平台默认的“我的项目”,而是按业务来,比如“商城项目图标”“后台管理系统图标”。原因后面会讲:项目名会直接影响下载文件里的命名规则,项目名规范了,后续维护成本会低很多。
在项目详情页里,找到下载按钮,平台会弹出窗口让你选择引用模式:Unicode、Font class、Symbol。这里先不急着全选,Unicode 算是历史遗留方案,需要在页面里粘贴一串字符码,可维护性很差,我基本不用;字体图标这块就选 Font class,需要多色图标再勾 Symbol,核心选择逻辑我在第三章详细展开。如果只做 uni-app 项目,你勾选 Font class 和 Symbol 两个就够用了。
2.2 下载后到底拿到了什么
点击下载后,本地会解压出一个文件夹,里面大致包含:
demo_index.html:本地预览文件,双击能看见所有图标效果和对应类名iconfont.css:整个图标库的样式文件,封装了@font-face和所有图标类iconfont.ttf / woff / woff2 / eot / svg:字体文件的各种格式iconfont.js:Symbol 方式的 JS 文件,里面是 SVG symbol 的定义(如果勾选了)
一定要先打开demo_index.html看一眼,它不只是预览工具,还能帮你复制类名。我做项目时习惯把这个预览文件存到项目的docs目录里,后续找图标时直接翻本地文件,不用反复上平台搜索。每次从平台更新图标后,都要重新下载代码包并覆盖,这一步在多人协作里特别容易漏,所以我会在代码里写注释标记“更新日期和版本”,方便下一次覆盖时对齐。
2.3 下载前的参数选择逻辑
回到选项问题。Font class 模式生成的东西是一套 CSS:每个图标一个 class,比如.icon-search:before { content: "\e6e1"; }。使用方把对应 class 挂在元素上,父级已经声明了font-family: iconfont就能渲染。这个模式最大优点是使用简单,任意端都认识;缺点是字体图标本质是单色矢量,你只能通过 CSS 的color改颜色,没法像 SVG 那样让某个笔画单独变色。
Symbol 模式则是把每个图标包成<symbol>,整个文件就是一张大的 SVG 雪碧图,要用哪个就<use>相应 symbol 的 id。这个模式支持多色图标,因为 symbol 内部的 path 可以自带 fill。但使用门槛比 Font class 高,在 uni-app 小程序端尤其麻烦,需要封装组件才能方便使用。Symbol 更适合承载品牌 Logo、多色运营素材这类对视觉要求高的少量图标。
我个人在项目里的默认策略是:80% 的单色业务图标走 Font class,剩下 20% 需要特殊配色的图形走 Symbol。这样既保证开发效率,也不牺牲视觉表现。
3. 核心细节解析:Font Class 接入 uni-app 的完整配置
3.1 字体文件到底放哪、CSS 怎么写
先把字体文件放进项目的static/iconfont/目录,这是我在多个项目里固定下来的目录结构:
static/ iconfont/ iconfont.css iconfont.ttf iconfont.woff iconfont.woff2之所以放到static目录,是因为 uni-app 对static下的文件会原样拷贝到各端产物目录,不参与编译处理。这里有个很关键的点:如果你把iconfont.css放到common或components目录,编译时 CSS 会经过路径改写,字体文件的相对路径容易断,尤其是小程序端经常 404。放在static后,CSS 引用相对路径就以自身位置为准,字体文件和 css 放同一个文件夹,URL 直接写文件名就行,最省事。
打开iconfont.css,核心是两段内容。第一段是字体定义:
@font-face { font-family: "iconfont"; src: url('iconfont.woff2?t=123456') format('woff2'), url('iconfont.woff?t=123456') format('woff'), url('iconfont.ttf?t=123456') format('truetype'); }第二段是每个图标的类规则:
.icon-search:before { content: "\e6e1"; }在使用之前,我一定会做两步修改。第一步,把font-family的名字改成项目专用名,比如app-iconfont,别用通用的iconfont。原因很实际:一套代码里不可避免会引其他组件库的图标字体,如果大家都叫iconfont,渲染时谁能加载谁不能,一看 CSS 加载顺序,图标就乱。改名的范围包括@font-face里的font-family和下面每个类里的font-family声明。第二步,把 css 里?t=123456这种时间戳参数统一处理。平台每次生成都会带随机时间戳,如果你从某处复制了半个 CSS、又从另一处复制了半个字体文件,版本对不上,图标就会一部分能显示、一部分变方框。
改完后把这份 CSS 放进全局。在 uni-app 里统一在App.vue的style中引,不要放到每个页面单独引。字体定义全局只需要一份,页面单引既重复又容易造成样式覆盖不一致。代码也很简单:
<style lang="scss"> @import "./static/iconfont/iconfont.css"; </style>如果你用了 scss,也可以把这个 css 内容统一收进一个iconfont.scss管理。注意路径是相对App.vue的位置写的。
3.2 Font class 在页面里的具体用法
全局注册完之后,页面上使用就很随意了,任何text元素挂上“基础图标类 + 具体图标类”:
<text class="app-iconfont app-icon-search"></text>关键点在于:第一个类定义了font-family: app-iconfont,第二个类通过::before伪元素写入对应的 Unicode 码点。字体图标的尺寸跟着font-size走,颜色跟着color走。
<text class="app-iconfont app-icon-search" style="font-size: 28rpx; color: #333;"></text>注意,这里不要试图用width和height去控制图标大小,因为伪元素生成的“内容”本质是文本字符,不是块级盒子,设置宽高无效。想调整图标和文字的对齐关系,用line-height和vertical-align。我的经验值是vertical-align: -0.15em左右,不同图标基线略有差异,微调即可。这个坑我在初学时期踩过:图标中间莫名偏上,同事看了半天才发现是line-height没设置。
关于动态切换图标,我见过不少人写:class="'app-iconfont app-icon-' + iconName"。在小程序端,动态拼接出来的类名如果从未在某个模板里以静态字符串形式出现过,编译器对样式做精简时可能不保留对应规则,结果真机上明明引入了所有图标类,页面还是空白。我给团队的规范是:要么把业务里可能用到的图标收集成一个常量数组,在页面里用v-for一次性挂到一个隐藏节点;要么在class里预置所有会用到的图标,再通过v-if控制显示。不是所有编译器都会出这个问题,但团队协作时要保证产物可预期,所以我干脆统一用最稳的方式。
3.3 微信小程序真机的字体文件特殊处理
前面反复提到一个坑,这里单独展开:微信小程序本地字体文件不能直接在 CSS 里用相对路径加载。开发者工具里一切正常,但真机上url('iconfont.ttf')会直接失败,图标全部变方框。原因在于小程序运行时环境不会像浏览器那样发起相对路径网络请求去加载本地资源。
解决方案有两种。第一种是 base64 内嵌,把 ttf 文件转成 base64 字符串直接写到 CSS 的@font-face里。这会让 CSS 体积增大不少,一个几十 KB 的 ttf 转 base64 后大约增加三成体积,但换来的是小程序、H5、App 都能用,而且不会再出现字体加载闪烁。具体做法是把url('iconfont.ttf')替换成url(data:font/truetype;charset=utf-8;base64,xxx)。第二种是换用 woff 格式做 base64,压缩率更高,但部分低版本安卓兼容性不如 ttf。我的经验是业务小程序以 ttf 为准,面向用户的 H5 端保留 woff 在线加载。
还有一种远程字体方案是wx.loadFontFace动态加载,不过这个接口依赖网络,还要在代码里异步处理回调,对一个高频使用的图标库来说太重了,我基本不用。base64 处理直接决定了“真机不显示”这个高频问题的解决率。我建议把这个处理动作固化成一个脚本,每次更新图标库后跑一遍,避免团队里有人忘记做。
4. 进阶:SVG Symbol 方案与自定义图标组件
4.1 Symbol 方案的运行机制
SVG Symbol 方案很多人听过但没真正玩明白。iconfont 平台下载的iconfont.js长这样(简化后):
var svgSymbols = `<svg style="position:absolute;width:0;height:0;overflow:hidden;"> <symbol id="icon-search" viewBox="0 0 1024 1024"> <path d="M..." fill="#333333"></path> </symbol> <symbol id="icon-close" viewBox="0 0 1024 1024"> <path d="M..." fill="#333333"></path> </symbol> </svg>`;原理是先把所有图标放进一个不可见的 svg 里,每个图标是一个<symbol>,相当于模板。使用时放一个<svg><use xlink:href="#icon-search"/></svg>,运行时就把 symbol 内容渲染到use所在位置。好处是符号内部的 path 可以自带多个颜色;坏处是不能像字体图标那样简单用color整体改色,要改色要么直接修改 symbol 里的 fill 值,要么用 CSS 的fill: currentColor让它跟随文字颜色。
在 uni-app 各端里,微信小程序对原生<svg>行内渲染支持得很差,直接在 wxml 里写一个带<use>的 svg 标签渲染不出来。正规做法是把 symbol 定义抽出来做成自定义组件,组件内部用条件编译处理不同端。如果目标只有 H5,方案非常轻量;如果目标是全端,复杂的多色图标我建议干脆单独导出成 PNG 或 SVG 文件,用 image 组件加载,简单直接,不要硬上 Symbol。
4.2 图标组件的设计思路
不管用哪种加载模式,在 uni-app 项目里我都建议封装一个AppIcon组件,把“图标名、尺寸、颜色”这几个对外参数收口,内部处理加载细节。这样页面代码非常干净:
<app-icon name="search" size="28" color="#333" />组件内部的思路大致如下:
<template> <!-- H5 端用 SVG Symbol 渲染,小程序端用字体图标 --> <!-- #ifdef H5 --> <svg :width="size" :height="size" :fill="color"> <use :xlink:href="'#icon-' + name"></use> </svg> <!-- #endif --> <!-- #ifndef H5 --> <text class="app-iconfont" :class="'app-icon-' + name" :style="{ fontSize: size + 'rpx', color }" ></text> <!-- #endif --> </template>这段代码只是说明组件的形态,不同项目条件编译的粒度可以更细。封装组件最大的收益是后期换方案时,改动只发生在一个组件文件里,而不是全局几十个页面到处找图标类名。我之前重构过一个老项目,当时图标分散在十几个页面里,因为早期直接裸用 class 名称,重构时差点改崩;后来新项目一开始就强制走组件,再也没遇到这类问题。
4.3 Font class 与 SVG Symbol 的全面对比
放一张对比表,方便不同团队做取舍:
| 对比维度 | Font class(字体图标) | SVG Symbol(雪碧图) |
|---|---|---|
| 加载原理 | 字体文件按字符渲染 | SVG 内联 symbol + use |
| 图标颜色 | 单色,靠 CSS color 控制 | 本身可多色,也可 currentColor |
| 使用复杂度 | 低,类名直接挂 | 中等,需封装组件和处理兼容 |
| 微信小程序兼容 | 高(推荐 base64 字体) | 低,需条件编译或转图片 |
| 动态切换 | 注意类名需静态声明 | 绑定 name 即可 |
| 打包体积 | 字体文件可控,几十 KB | SVG 字符串内嵌,图标多会膨胀 |
| 适合场景 | 绝大多数业务图标 | 品牌 Logo、多色插画、少量图形 |
如果你刚开始,我的建议是别一上来就搞太复杂,先用 Font class 跑通主流程,等真遇到需要多色图标的那一天,再按我的方案把对应图标挑出来单独处理。把简单方案先用顺,再按需引入复杂度,这套节奏在工程里最靠谱。
5. 实操过程:在 uni-app 项目里完整落地一套 iconfont
5.1 目录初始化和文件复制
现在开始完整走一遍落地流程,假设项目是用 CLI 创建的标准 uni-app 脚手架,所有路径基于项目根目录。
第一步,在static下建static/iconfont目录,把刚下载的iconfont.css、iconfont.ttf、iconfont.woff、iconfont.woff2复制进去。iconfont.js先不复制,后面如果走 Symbol 方案再拿。注意不要直接把整个 demo 文件夹拖进来,里面有一堆重复字体文件副本,会让小程序包凭空多出几百 KB。
第二步,打开iconfont.css,做三件事:改font-family为app-iconfont,确认 src 里引用的字体文件名和实际文件名一致,把时间戳参数统一。改完保存。
第三步,在App.vue的style里 import 这个 css 文件。如果项目里有uni.scss做全局变量,字体文件的引入放在App.vue的style里即可,不需要放进uni.scss。
5.2 微信小程序端的 base64 处理
如果目标平台包含微信小程序,需要把iconfont.css中对 ttf 或 woff 的本地引用替换成 base64 内嵌。具体做法是写一个本地 Node 脚本,把static/iconfont/iconfont.ttf读成 base64,再在 CSS 里替换url('iconfont.ttf?...')为url(data:font/truetype;charset=utf-8;base64,xxxxxx)。
我在项目里用了一个简化脚本,放在scripts/build-icon-font.js。
const fs = require('fs'); const path = require('path'); const ttfPath = path.join(__dirname, '../static/iconfont/iconfont.ttf'); const cssPath = path.join(__dirname, '../static/iconfont/iconfont.css'); const base64 = fs.readFileSync(ttfPath).toString('base64'); let css = fs.readFileSync(cssPath, 'utf8'); css = css.replace(/url\('?iconfont\.ttf.*?'?\)/g, `url(data:font/truetype;charset=utf-8;base64,${base64})`); fs.writeFileSync(cssPath, css);这个脚本刻意做了简化,实际项目里还要考虑重复执行时的幂等性、woff 格式是否也要内嵌等问题。每次从平台更新图标包后,我会先跑一遍这个脚本,防止再次把相对路径覆盖回去。
5.3 页面内使用与自定义组件收口
页面里最直接的使用方式前面已经说过:<text class="app-iconfont app-icon-search"></text>。但如果团队有几十个页面都在用图标,我会在components下建一个AppIcon.vue,把字体图标的用法收口。组件要做两件事,一是校验传入的 name 是否在白名单内,防止图标名称打错时静默变成方框;二是对外暴露size和color。
一个可用的基础版本是这样的:
<template> <text class="app-iconfont" :class="`app-icon-${name}`" :style="{ fontSize: size + 'rpx', color }" ></text> </template> <script> export default { name: 'AppIcon', props: { name: { type: String, required: true }, size: { type: [Number, String], default: 32 }, color: { type: String, default: '#333' } } } </script> <style lang="scss"> // 全局已引入 iconfont 字体和样式,这里不需要重复 </style>这个组件代码量不大,使用价值却很高。注意rpx单位在小程序端是自适配的,在 H5 端会被转换成 px,App 端也类似。如果设计稿是 750 宽,组件里传的size直接按设计稿的 px 写就行。
5.4 多端统一和版本管理
一次完整的落地还要考虑后续维护。团队协作时,我会在仓库里维护一个ICON_VERSION.md,记录每次更新 iconfont 的日期、图标数量、变更说明。做法很简单,但非常有用。之前有个项目,某位同事直接复制了另一个分支里的iconfont.css到自己分支,字体文件更新了一半没更新另一半,线上图标版面全乱,排查了两天才发现是资源版本不一致。有版本记录文件至少能快速定位是不是“某次覆盖遗漏”。
图标更新流程建议固定成:iconfont 平台修改项目 -> 重新下载代码包 -> 覆盖static/iconfont下的对应文件 -> 跑 base64 转换脚本 -> 提交文档说明 -> 发版验证。全部命令化、文档化之后,新同事也能一次做对。
6. 常见问题与排查技巧实录
6.1 常见问题速查表
每天收到最多的技术咨询基本都是那几类,我整理了一张速查表。你在项目里遇到对应症状,可以直接查:
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 所有图标显示成方框 | 字体文件未加载,或@font-face路径错误 | 检查 CSS 是否被全局引入、字体文件是否在产物目录 |
| 只有部分图标是方框 | 字体文件版本和 CSS 版本不一致(覆盖遗漏) | 重新从平台整体下载并覆盖,统一 font-family |
| 开发者工具正常,真机空白 | 小程序本地字体未被引用 | 用 base64 内嵌字体 |
| 图标大小不受 width/height 控制 | 字体图标是字符,不是图片 | 用 font-size 设置大小,line-height 微调 |
| 图标颜色改不了 | Font class 是单色字体 | 用 CSS color 改,需要多色则切 Symbol 或图片 |
| 动态拼接 class 不显示 | 小程序端样式精简了未静态出现的类 | 将可能用到的图标类统一静态挂载或收口到组件 |
| 图标和文字有偏移 | 基线对齐问题 | 设置 line-height 与 vertical-align 组合值 |
对照这张表,90% 的图标异常能在十分钟内定位。
6.2 几个隐藏的坑和我的处理思路
第一个坑是“字体文件被自动清掉”。小程序端为了控制包体积会尽量压缩资源,如果你把iconfont.css放在非 static 目录且经过编译器加工,字体文件的相对路径可能被改写,最终产物里路径失效,但本地开发时又正常,因为开发服务器刚好命中了一个同名资源。处理办法前面说了,资源统一放static,发版前再看一眼产物。
第二个坑是“不同页面字体加载时序不一致”。H5 端字体文件通过网络加载,文件较大时图标会先空白或闪烁一下再出现。这个问题可以用font-display: swap缓解,但在某些浏览器下还是会有短暂 fallback。更稳的做法是把字体文件 base64 进 CSS,虽然体积增大,但字体随 CSS 一起到,彻底没有闪断问题。我在对体验要求高的 H5 页面里也这么干。
第三个坑是“图标库动不动就上百 KB”。很多人把整个公共图标库全加到项目里,一个图标 2 KB,一千个就是 2 MB,在小程序端非常不划算。我的做法是严格按照项目维度维护 iconfont 项目,只加业务真正用到的图标,定期删掉已下线的图标,并在版本记录里同步。把“每个图标都是有成本的”这个观念传导给团队,比任何工具都管用。
6.3 排查流程的“三板斧”
如果你已经在这个坑里了,我建议按顺序走一遍:
第一步,开开发者工具看 console,有没有字体文件 404 的报错。有报错就直接进入路径和加载方式排查。第二步,在App.vue里临时写一行<text class="app-iconfont app-icon-search">测试</text>。如果它也不显示,说明资源或全局样式本身有问题;如果它显示而某个页面不显示,那就是页面内部样式被局部覆盖,或者类名使用方式不对。第三步,检查渲染出来的元素计算样式里font-family到底是什么。如果某个页面里因为第三方 UI 库的权重更高导致font-family被覆盖,图标同样会变方框。
三句话讲完,但每一次都真实救过急。排查图标问题时别盯着“为什么是方块”想太久,直接把加载链路拆开,问题很快浮出水面。
最后分享一个我个人的习惯:iconfont 上线后,我不会只在小程序开发者工具里点两下就完事,一定会在两个真机上各看一遍,一台最新 iOS,一台安卓机。因为字体图标的渲染在不同内核上多少有差异,尤其安卓低版本字体 baseline 经常有偏差。多花这十分钟,能省掉上线后用户反复反馈“你这图标怎么是歪的”的无数解释。
这套方案我从 v2 项目用到 v3 项目,踩过的坑基本就是上面列的这些。只要按文章里的主流程走一遍,后续新增图标也只是重复同样的步骤。真正值得尽早建起来的,是那个“更新脚本 + 版本记录”的流程。它不复杂,但能在关键时候帮你省下大半天排查时间。