网页图标糊成狗,怎么用Sass把SVG塞进CSS变量里潇洒展示?

3,406字
14–22 分钟
in

高清屏下图标边缘像狗啃,hover换个颜色还得重新切图,更别提动画效果了。SVG格式虽然公认是图标界的天花板,但每次往HTML里塞一堆<svg>代码,看着就头大。有一种操作:把SVG转成数据URI,再用Sass函数批量编码,最后扔进CSS自定义属性里。这样CSS文件就能直接调用图标,HTML保持干净,还能随便改大小和滤镜颜色。下面直接上硬核流程,手把手教搭建这套图标库。

目录

准备图标库

先建一个Sass地图(map),把项目里所有图标的原始SVG代码都塞进去。每个图标给个外号,比如burger代表汉堡菜单图标。

// 图标仓库,键名随便起,值必须是完整的SVG字符串
$icon-library: (
  burger: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24.8 18.92" width="24.8" height="18.92"><path d="M23.8,9.46H1m22.8,8.46H1M23.8,1H1" fill="none" stroke="#000" stroke-linecap="round" stroke-width="2"/></svg>',
  downArrow: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 16L6 10h12z" fill="currentColor"/></svg>'
);

代码里的fill="currentColor"是个神技,这样图标颜色就能继承父元素的文字颜色,后面不用额外写滤镜。但注意,如果SVG里写死了stroke="#000",那就只能用其他黑科技改色了。另外,SVG代码越精简越好,别留什么编辑器自带的注释或多余分组,否则编码后CSS文件会膨胀得像猪肚。

转义字符表

数据URI要求特殊字符必须转义,否则浏览器解析会崩。空格、双引号、井号、斜杠这些全得换成百分号编码。搞一张转义对照表,Sass函数会遍历每个字符并替换。

$escape-char-map: (
  ' ': '%20',
  '"': '%22',
  '#': '%23',
  '/': '%2F',
  ':': '%3A',
  '(': '%28',
  ')': '%29',
  '%': '%25',
  '<': '%3C',
  '>': '%3E',
  '\': '%5C',
  '^': '%5E',
  '{': '%7B',
  '|': '%7C',
  '}': '%7D',
);

这里有个坑:如果SVG代码里本来就用了%号(比如某些滤镜),必须把它也转成%25,不然双重编码会乱套。另外,换行符和制表符最好提前手动删掉,虽然不转义也能用,但会增加体积。建议把整个SVG字符串压缩成一行,连回车都不要留。

编写编码函数

写一个Sass函数,传入图标名,自动从库中取出代码,逐个字符查表转义,最后拼成url('data:image/svg+xml, ...')格式。

@function encode-svg($icon-name) {
  // 检查图标名是否存在
  @if not map-has-key($icon-library, $icon-name) {
    @error '图标 “#{$icon-name}” 不在库中,检查一下键名';
    @return false;
  }

  $raw-svg: map-get($icon-library, $icon-name);
  $unquoted-svg: unquote($raw-svg);
  $escaped: '';

  // 逐字符处理
  @for $i from 1 through str-length($unquoted-svg) {
    $char: str-slice($unquoted-svg, $i, $i);
    $replacement: map-get($escape-char-map, $char);
    @if $replacement != null {
      $char: $replacement;
    }
    $escaped: $escaped + $char;
  }

  @return url('data:image/svg+xml, #{$escaped}');
}

这个函数相当于给SVG穿了一层防护服,特殊符号全被屏蔽。不过注意,如果SVG代码里包含<style>标签内的CSS,里面的花括号{}也要被转义,幸好转义表已经覆盖了。但有一个副作用:转义后的字符串长度暴涨,一个小图标可能膨胀两倍。所以只适合图标数量少(比如10个以内)的项目,要是塞上百个复杂图标,CSS文件会变得比砖头还重。

存入CSS变量

把所有图标丢进:root,每个图标对应一个自定义属性(CSS变量)。变量名统一用--svg-加图标名,值为编码后的数据URI。

:root {
  @each $name, $code in $icon-library {
    --svg-#{$name}: #{encode-svg($name)};
  }
}

编译后,浏览器里会生成一堆类似--svg-burger: url("data:image/svg+xml, %3Csvg...");的变量。这些变量挂在全局,任何选择器都能用var(--svg-burger)直接召唤图标。这样做的好处是,同一个图标在多处使用时,编译出来的CSS只有一行变量引用,而不是重复输出几十KB的编码字符串。举个例子:十个地方调用汉堡图标,传统函数方式会生成十份相同的url(...),而变量方式只生成一份定义,CSS体积瞬间瘦身。

调用图标

伪元素的content属性可以接收CSS变量,直接把图标当成文字内容插进去。比如给按钮的:after加个菜单图标:

.menu-btn::after {
  content: var(--svg-burger);
  display: inline-block;
  width: 24px;
  height: 24px;
  // 调整大小可以通过font-size或宽高,但content中的SVG默认按原始尺寸渲染
}

但这里有个大坑:content里的SVG默认不会继承宽高,需要给伪元素设置display: inline-block和具体的宽高。而且content生成的图片无法直接改颜色(除非SVG里用了currentColor)。想换颜色?用滤镜!比如hover时变成白色:

.menu-btn:hover::after {
  filter: invert(1);  // 黑变白,简单粗暴
}

或者用brightness(0) invert(1)组合拳。不过滤镜会连阴影、边框一起反色,复杂图标可能会翻车。

备用遮罩法

如果嫌弃滤镜不够精准,可以换mask(蒙版)方案。把图标作为蒙版图片,背景色随便换。

.icon-mask {
  display: inline-block;
  width: 32px;
  height: 32px;
  background-color: #ff6600;  // 想要什么颜色就设什么色
  mask-image: var(--svg-burger);
  mask-repeat: no-repeat;
  mask-size: contain;
  mask-position: center;
  -webkit-mask-image: var(--svg-burger);  // 老浏览器需要前缀
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-size: contain;
  -webkit-mask-position: center;
}

这个骚操作相当于用SVG的形状在彩色矩形上挖了个洞,透出背景色。可以随心所欲换颜色,甚至加渐变色背景。但注意,mask属性在某些安卓老机型上翻车,而且需要双写带-webkit-前缀的版本(2024年依然如此)。另外,mask方式不支持多色图标,只能显示单色,因为背景色是统一的。

方案优点缺点
滤镜代码少全图变色
遮罩颜色精准仅单色

优化SVG源码

编码前一定先把SVG丢进优化工具(比如SVGOMG)里榨干水分。不必要的元数据、注释、空分组、默认数值全删掉。一个未优化的SVG可能几百字节,优化后直接腰斩。举个例子:
原始<path d="..." fill="#000000"/>可以改成fill="#000"stroke-width="2.0000"改成stroke-width="2"。还有<g>标签如果能合并就合并,能省不少字符。这些优化动作必须在编码前完成,否则转义后的小垃圾也会原封不动留在CSS里。