场景:小程序主包/分包体积优化时,把项目自有的 iconfont 字体图标替换为图片(SVG/PNG)。
难点:
<image>加载的 SVG 无法用 CSScolor改色 。本文给出一个用filter: drop-shadow的"剪影替身"方案,让替换后的图标依然能通过 CSS 自由控色。说明:文中路径、域名均为占位示例,请替换为自己项目的实际值。
一、背景与问题
为什么要换掉 iconfont
字体图标(iconfont)会把一整个 .ttf/.woff 字体文件打进包里。为什么这很浪费体积,有三层原因(容易被忽略):
- 整套字形一起打包,用几个就得扛几百个 :一套 iconfont 通常就一个字体文件,但里面塞了这套图标的全部字形。哪怕这套有 200 个图标、你只用到 5 个,那 195 个用不到的字形照样全在文件里。
- 字体文件几乎无法「摇树(tree-shaking)」 :普通 JS 里没用到的代码能被构建工具删掉,但字体是个整体二进制,构建工具删不掉里面没用到的字形,所以没法像代码那样自动瘦身。
- 小程序里往往更严重------字体常被 base64 内联进 CSS :小程序对加载外部字体文件有限制,很多项目直接把整个字体文件转成 base64 字符串嵌在 CSS 里 。而 base64 比原二进制还大约 33% ;如果这段 CSS 被多个页面/多个分包各引一次,还可能重复打包,进一步放大体积。
结论 :iconfont 的浪费 =「全量字形无法裁剪 + 可能 base64 内联/重复」,用到几个都得扛全套。当只用到其中几个图标、或要做包体积优化时,把这些图标改成按需从 CDN/OSS 加载的图片(SVG/PNG) 往往更划算------图标资源移出了包,包里只剩一个 URL 字符串,运行时按需从网络取。
换成图片后遇到的坑:颜色控制不了
一个常见误区是「SVG 可以用 CSS 改色,PNG 不行」。这个说法只在「内联 SVG」下成立:
| 用法 | CSS color 能否给 SVG 上色 |
|---|---|
内联 SVG :<svg><path fill="currentColor"/></svg> 直接写进 DOM |
✅ 能,color 通过 currentColor 传进去 |
<image src="x.svg"> :把 SVG 当外部图片资源加载 |
❌ 不能,和 PNG 一样是「替换元素」,CSS 进不去内部 |
而在小程序里,<image> 是原生组件 ,它把 SVG(或 PNG)解码成一张位图来渲染,没有任何 CSS 级联进入图片内部。所以:
xml
<!-- 期望白色,但 color 无效,最终显示 SVG 文件里烘焙的颜色(比如黑色) -->
<image class="icon" src="/static/icon-arrow.svg"></image>
css
.icon { color: #fff; } /* 对 <image> 里的 SVG 完全无效 */
如果 SVG 文件本身是黑色、而设计稿要白色,color:#fff 救不回来。这时要么去改 SVG 文件本身的颜色(不灵活、每种颜色要一份文件),要么换个技术方案------就是下面的 drop-shadow 剪影法。
二、核心思路:用「投影」当替身
一句话:改不了图片真身的颜色,就用它的「投影」当替身放到该在的位置,再给投影上色。
原理基于 CSS filter: drop-shadow() 的一个特性:它会按图片的不透明形状(alpha 通道)生成一整片纯色剪影,剪影颜色可由 CSS 控制。
三步配合(假设图标尺寸为 H):
- 藏起真身 :给外层容器设
overflow: hidden,把原图用transform: translateY(-H)往上顶出容器,被裁掉、看不见。 - 投影补位 :给图片加
filter: drop-shadow(0 H 0 currentColor),投影往下偏H。真身上移H、投影下偏H,一上一下正好抵消,投影精准落回可见区域。 - 投影上色 :投影颜色用
currentColor,也就是 CSS 的color。于是「设置color→ 图标变色」重新成立。
最终你看到的图标,其实是那片可被 CSS 染色的纯色剪影。
三、代码实现
基础结构
arduino
<view class="svg-icon">
<image class="icon-svg" src="/static/icon-arrow.svg" mode="aspectFit"></image>
</view>
css
/* 外层:可见窗口 + 决定颜色 */
.svg-icon {
display: inline-flex;
width: 32rpx;
height: 32rpx;
overflow: hidden; /* 关键:裁掉被顶出去的原图 */
color: #1A4D85; /* 目标颜色,currentColor 会取它 */
}
/* 内层图片:上移 + 投影补位 */
.icon-svg {
width: 32rpx;
height: 32rpx;
transform: translateY(-32rpx); /* 顶出窗口 */
filter: drop-shadow(0 32rpx 0 currentColor); /* 投影下偏,落回窗口,取 color 颜色 */
}
要点:
translateY的偏移量、drop-shadow的 Y 偏移量,都必须等于图标高度,两者要一致,否则投影对不齐。- 想换颜色,只改外层的
color即可,一张 SVG 可复用成任意颜色。 mode="aspectFit"可选,用来防止非正方形图标被拉伸变形(不影响投影原理)。
变体一:图标带状态色(复用颜色类)
不同状态用不同颜色时,把颜色类挂在外层,currentColor 自动继承:
arduino
<view class="svg-icon done"><image class="icon-svg" src="/static/icon-check.svg"></image></view>
<view class="svg-icon warn"><image class="icon-svg" src="/static/icon-warn.svg"></image></view>
css
.svg-icon .icon-svg { /* 同上:宽高 + translateY + drop-shadow(... currentColor) */ }
.done { color: #26BF76; }
.warn { color: #E6A23C; }
变体二:图标需要旋转/动画(避免 transform 冲突)
如果图标本身还要旋转(比如下拉箭头展开时翻转 180°),注意 drop-shadow 依赖的 translateY 也占用了 transform ,两者不能放同一个元素。解决办法:旋转放外层,translateY 放内层图片 ,各用各的 transform:
arduino
<view class="svg-icon" :class="{ active: open }">
<image class="icon-svg" src="/static/icon-arrow-down.svg"></image>
</view>
css
.svg-icon {
display: inline-flex; width: 32rpx; height: 32rpx; overflow: hidden;
color: #fff;
transition: transform 0.2s ease;
}
.svg-icon.active { transform: rotateZ(180deg); } /* 旋转在外层 */
.icon-svg {
width: 32rpx; height: 32rpx;
transform: translateY(-32rpx); /* translateY 在内层 */
filter: drop-shadow(0 32rpx 0 currentColor);
}
外层旋转时,内层的投影会跟着一起转,箭头翻转正常,互不干扰。
四、一个必须理解的副作用:DOM 位置 ≠ 视觉位置
这是本方案的固有特性,调试时容易困惑:
- 在开发者工具里选中
<image>元素 ,高亮框会出现在视觉图标的上方 ------因为元素被translateY(-H)真的移上去了,工具显示的是元素的真实几何位置。 - 你实际看到的图标 ,是下方那片
drop-shadow投影 。投影只是绘制效果、不是 DOM 元素,没有盒子、没有点击热区、工具也不会高亮它。
结论:元素在上、影子在下,你看的是影子。
带来的实际影响:
- 点击热区要挂在外层容器上(外层容器在正确的视觉位置)。
- 不要把点击事件直接绑在
<image>上 ------它的热区被移到了视觉上方H像素处,点视觉位置会点不到。
五、适用边界与注意事项
- 只能染单一纯色 :投影是「整个形状填一种色」的剪影,多色图标会被压成一个颜色。适合单色图标(箭头、眼睛、对勾等),不适合多色 logo。
- 小程序
filter兼容性 :filter: drop-shadow在部分小程序端/基础库版本上支持有限。务必在真机 + 目标基础库上验证,开发者工具能过不代表真机能过。 - 尺寸/偏移要一致 :
translateY与drop-shadow的 Y 偏移都必须等于图标高度;换尺寸时三处(宽高、translateY、drop-shadow)要同步改。 - 极小尺寸慎用:图标很小(如 7px)时,投影偏移也很小,真机上可能出现亚像素模糊或轻微错位,需实测。
- 能内联 SVG 时优先内联 :如果环境允许直接内联
<svg fill="currentColor">(如 H5),那是更直接的控色方式,不需要这套技巧。本方案主要解决「只能用<image>加载外部 SVG」的场景(典型如小程序)。
六、决策速查
| 使用iconfont情况 | 建议方案 |
|---|---|
图标单色 + 要 CSS 控色 + 只能用 <image>(小程序) |
✅ 本文的 drop-shadow 剪影法 |
| 图标颜色固定、不需要 CSS 控色 | 直接 <image> 用 SVG/PNG,最简单 |
| 图标多色 | 直接用彩色 SVG/PNG(放弃 CSS 控色) |
| 环境支持内联 SVG(H5 等) | 内联 <svg> + fill="currentColor" |
七、原理小结
<image> 负责提供「形状」,CSS 通过投影负责提供「颜色和位置」------把图标的形状与颜色拆成两层处理。 这就是绕过「<image> 里的 SVG 吃不到 CSS 颜色」限制的本质。