06月17, 2016

【译】SVG图标好用后备难做

原文:http://www.zcfy.cc/article/123

最近,图标字体用得越来越少了,好像有很多不错的理由都建议不再用图标字体,而改用SVG图形。对于《金融时报》(Financial Times)而言,我们大致同意这个观点, 是该考虑一下过渡方案了。

浏览器对SVG的支持非常好,94%以上都完全支持SVG 1.1,如果你不用遮罩,这个比例会提高到96%。

视情况不同,有人可能不准备为剩余的4%准备后备方案。而实际上,很多用户都是依赖图标的。不管你觉得用户体验好不好,很多网站确实在使用图标实现导航、切换菜单、关闭提示或者让用户执行登录/退出等重要操作。

下面就是Facebook、Twitter和Google Drive使用图标的情况。

Examples of Facebook, Twitter and Google Drive icon usage

一般来说,在界面空间有限的时候,用图标可以省掉文字。这时候,用户就更依赖图标了。

在FT(金融时报),我们的图标是由金融时报统一平台和Web开发工具包Origami提供的,这个平台必须适应全公司的各种应用场景,要好用、可靠、稳定。考虑到FT的用户都是一些大型企业和老板1,这就意味着必须给那4%看不到SVG的用户提供后备。

于是,难题来了。

标记

实现SVG图标的方法很多,比如行内元素、图标或背景。说到提供后备,其实结果显而易见。

  1. 使用行内<svg>元素加后备太过复杂。<img>元素依赖的srcset属性没有被IE、Opera Mini以及Android系支持。后备图片会导致每个图片都发送一次请求。✘
  2. 背景图片早在2000年初就用来实现雪碧图,而从图标字体升级不会改动太多代码。✓

过去,基于图标的Web字体实现都是添加一个基类,然后再为必要的符号添加特殊类名。字体可以继承颜色和大小,而基于雪碧图的背景不能继承颜色和大小,必须使用额外的类来指定颜色和大小:

<!-- 当前基于字体的实现 -->
<span class="icon icon--{symbol}"></span>

<!-- 建议未来的实现 -->
<span class="icon icon--{size} icon--{colour} icon--{symbol}"></span>

雪碧图

既然后备图片这么重要,使用SVG片段标识符也不行,那我必须按照2004年的做法,将图标按常规、不相互重叠的形式布局。

我试过的所有SVG雪碧图工具都可以输出一张一种颜色的雪碧图,要么照搬来源,要么重写。我真不想为我们20多种颜色搭配分别生成不同的文件,因此必须改进我们自己的系统。

FT有很多符号以及前面提到的大调色板,如果每个网站的用户都要下载一个巨大的雪碧图,结果却只用到其中几个图标,那对这些用户肯定不公平。我曾经做过一个小型应用的原型,用于只生成用到的符号及颜色的雪碧图。

这个应用从SVG源文件中找到路径,经过优化按定义把结果包含在输出中。每个定义都会计算viewbox,因此它们占的地方一样大。定义没有可见的界面,因此程序会循环一遍请求的颜色,引用相应的路径,再为它们应用相应的填充。

Screenshot of SVG sprite generator output

这个SVG雪碧图生成器抓到每一个SVG源文件,优化并计算一个viewbox,以便它们占的地方一样大。程序循环所有请求的颜色并引用相应的路径,然后进行填充。

目前我们只使用系统来生成静态文件,包括SVG和PNG。不过,今后如果有需求,我们还可以再实现一个类似的工具,为我们实时生成雪碧图。

样式

以下是最初的Sass代码。别急着嗤之以鼻,容我稍后解释。首先定义了图标大小、颜色和符号。

使用Sass写会更清晰地展示计算过程,为了清晰起见,稍有改动。

$icon-sizes: (16, 24, 36, 48);
$icon-colors: ('black', 'white', 'blue', 'red');
$icon-symbols: ( 'chronometer', 'cog', 'hearts', 'rocket', 'sign', 'speech', 'user');

.icon {
  position: relative;
  display: inline-block;
  overflow: hidden;

  &:after {
    content: '';
    position: absolute;
    width: percentage(length($icon-colors));
    height: percentage(length($icon-symbols));
    background-image: url('sprite-sheet.svg');
    -webkit-background-size: 100%;
    background-size: 100%;
  }
}

这里没有给.icon元素应用雪碧图作为背景,反倒引入了一个伪元素,觉得奇怪吗?原因是雪碧图的横轴从左到右依次是各种颜色,纵轴从上到下是相应的符号,而background-position属性无法一个符号一个符号地指定坐标。

只使用背景定位,需要为每个一个图标和颜色的可能组合定义样式,对FT的50个符号、20种颜色来说,那就是1000个声明

而使用绝对定位的伪元素不仅所有浏览器都支持,而且可以使用单独的类分别指定leftright属性:.icon--{colour}用于横轴,.icon--{symbol}用于纵轴。

相对来说,background-position-xbackground-position-y属性是有某些浏览器支持,但它们并不属于CSS规范,而且没有得到所有浏览器支持。已经有人建议将这两个属性已放到背景与边框模块4级了。

如果应用背景图片的话,还是需要进行绽放才能让每个图片按照包含元素的大小填充。因为雪碧图由普通的大小一样的正方形组成,颜色和符号数量已知,因此可以计算。只要用100%(元素宽度)乘以颜色/符号的数量即可。不过,不是使用这些值去设置background-size属性,而是用它们去设置伪元素以及雪碧图的大小。

Diagram of icon element and pseudo-element layout

雪碧图由普通的大小一样的正方形组成,颜色和符号数量已知,因此可以计算。

初始的设置就是这样,但光有这些还不行。需要不同的类分别指定大小、颜色和符号。在通过列表定义这些参数后,可以迭代它们分别生成不同的声明:

@each $size in $icon-sizes {
  .icon--#{$size} {
    width: $size + px;
    height: $size + px;
  }
}

@each $color in $icon-colors {
  .icon--#{$color} {
    &:after {
      $color-index: index($icon-colors, $color);
      left: percentage($color-index - 1) * -1;
    }
  }
}

@each $symbol in $icon-symbols {
  .icon--#{$symbol} {
    &:after {
      $symbol-index: index($icon-symbols, $symbol);
      top: percentage($symbol-index - 1) * -1;
    }
  }
}

在支持SVG的浏览器中,这样就行了!看这个示例

现在解决难题……

后备

不支持SVG的浏览器大致可分为两类:老版本IE(< IE9)和Android Froyo、Gingerbread等版本中的浏览器。这些浏览器都需要栅格图作为后备,而且不是指定两张背景图那么简单,还必须阻止它们下载和应用SVG文件。

可以使用不可见的渐变技巧作为SVG支持的代理。不完美(IE9及Android 3中确实支持SVG的浏览器会被排除在外),但还是可以接受的:

.icon:after {
  background-image: url('sprite-sheet.png');
  background-image: -webkit-linear-gradient(transparent, transparent),  
    url('sprite-sheet.svg');
  background-image: linear-gradient(transparent, transparent),
    url('sprite-sheet.svg');
}

解决了老版本Android中的浏览器问题之后,接下来是老版本IE及其他不支持CSS3背景属性的浏览器。

一个方案,就是像颜色一样,为每一种大小都提供一个单独的雪碧图。的确可以通过图片服务预生成并提供雪碧图,但这就意味着会有额外的请求,而且只针对需要它们的浏览器也会导致问题。除非你真的能容忍这些问题,否则这个方案还是不行。

老版本IE中有ActiveX滤镜,其中AlphaImageLoader有一个sizingMethod选项,可以模拟background-size: 100% 100%。遗憾的是滤镜不能应用给伪元素,于是这个奇妙的方案也不能用。

最终解决问题,用的是另一个非标准属性:zoom。因为必须给图标元素一个尺寸,而且雪碧图已经按已知大小生成了,这里的比例是可以计算的。虽然不能给图标元素指定相对大小,但相信这个问题很好解决。

让人生气的是,元素的绝对位置也必须根据雪碧图的原始大小重新计算,因为这些属性也是放大的。

zoom虽然也不属于CSS规范,但它已经得到了相当广泛的支持。我们的后备方案在最新版Chrome中的效果和在IE8中一样。

我们把每个要重写的后备属性都包装在了相应的媒体查询中。媒体查询与CSS3背景选项放在一块完全没有问题。

$icon-intrinsic-size: 50;

@each $size in $icon-sizes {
  .icon--#{$size} {
    width: $size + px;
    height: $size + px;

    &:after {
      zoom: 1 / $icon-intrinsic-size * $size;

      @media all and (min-width: 0) {
        zoom: 1;
      }
    }
  }
}

@each $color in $icon-colors {
  .icon--#{$color} {
    &:after {
      $color-index: index($icon-colors, $color);
      left: $icon-intrinsic-size * ($color-index - 1) * -1 + px;

      @media all and (min-width: 0) {
        left: percentage($color-index - 1) * -1;
      }
    }
  }
}

@each $symbol in $icon-symbols {
  .icon--#{$symbol} {
    &:after {
      $symbol-index: index($icon-symbols, $symbol);
      top: $icon-intrinsic-size * ($symbol-index - 1) * -1 + px;

      @media all and (min-width: 0) {
        top: percentage($symbol-index - 1) * -1;
      }
    }
  }
}


最后的话

想必大多数看到这篇文章的朋友都不会碰上与FT Origami团队面临的同样的问题。如果你不需要非SVG的后备,或者你的图标只需要显示为一种大小,那么实现SVG雪碧图跟实现图标字体一样很简单。但我们由于面临如此严苛的要求……目前已经发布了一个beta版本的实现,希望得到大家尽量多的反馈,我会及时更新。我愿意回答如下一些问题:

  • 提供一张高分辨率的PNG是不是更好?
  • 在一个页面中使用多个SVG图标会不会影响性能?
  • 克服图标字体的不足比实现SVG图标(以及适当后备)还要难吗?

在CodePen上查看源代码


  1. 大公司IT设备的升级是一件很复杂的事,因为要花不少钱,所以流程很慢。这不是用户的问题!

英文原文:http://maketea.co.uk/2015/12/14/svg-icons-are-easy-but-the-fallbacks-arent.html

本文链接:http://lisongfeng.cn/post/svg-icons-are-easy-but-the-fallbacks-aren-t.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。