广告:SF 里弄了个 CSS 小圈子,欢迎一块儿来讨论问题css
前端小图标处理方案众多,本文主要介绍基于雪碧图的处理方案,分析雪碧图的预处理和后处理模式的得与失,以及在项目中一般会遇到的问题以及解决方案。其余小图标处理方案未在此文探讨之列。前端
此外,本文更多的是理论性分析,具体技术实现不做深刻解释。若有疑问,欢迎到上述小圈子一块儿讨论!node
讨论的起点是工程工具 Gulp/Webpack 上的集成方案(手动拼合雪碧图的作法已经做古了)。webpack
表明:gulp.spritesmith 和 webpack-spritesmithgit
预处理方案是预先指定须要生成的雪碧图切片元素,由工具合成后,获得相应的雪碧图和数据 (S)CSS 文件,开发中将两者投入使用。github
数据文件内容一般是可定义的,咱们能够自定义模板或者内容生成函数(怎么写这里就不探讨了)。借助 SCSS 等 CSS 预编译语言的威力,如何使用雪碧图数据就极具便利性。web
注意,下面的 SCSS 代码都是写的模板函数生成的内容,模板函数具体怎么写不在本文探讨范围内。gulp
最简单的,咱们能够直接生成类(如),如:segmentfault
.icon-home { width: 12px; height: 12px; background: url(sprite.png) -56px -48px no-repeat; }
这样在 HTML 结构中能够直接使用。但若是切片元素众多,每一条规则都要有单独的 background
属性的话,未免太冗长,稍微改进下,把公共的 background-image
属性提取出来(高清模式下还有 background-size
,如下从简讨论),能够生成这样的内容:sass
.icon-home { width: 12px; height: 12px; background-position:-56px -48px; } .icon-back { width: 18px; height: 20px; background-position: -10px 0; } .icon-home, .icon-back { background-image: url(sprite.png); }
可是,若是有的类没用到,白白生成一个类岂不是很不必?或者咱们不想使用 .icon-home
这样子的类名,那咱们用 SCSS 占位符能够解决这个问题:
%-icon-home { width: 12px; height: 12px; background-position:-56px -48px; } %-icon-home, %-icon-back { background-image: url(sprite.png); background-repeat: no-repeat; }
而后须要用到的时候,再继承这个占位符:
.head-home { @extend %-icon-home; }
这样,咱们就能用上自定义的类名,并节省 CSS。
但若是遇到状态性变化怎么办?好比 :hover
时小图标变红,这时候其实除了 background-position
改变外,其余数据是没有变化的。
按上述方案,咱们的写法会是:
.head-home { @extend %-icon-home; &:hover { @extend %-icon-home_hover; } }
生成的最终 CSS 是:
.head-home { width: 12px; height: 12px; background-position:-56px -48px; } .head-home:hover { width: 12px; height: 12px; background-position: 0 0; } .head-home, .head-home:hover { background-image: url(sprite.png); }
思索发现,本质上占位符只是包含了图片信息,那咱们换一种更加高端点的写法:
// 把雪碧图数据保存为一个 SCSS Map 数据 $__sprite__: ( 'home': ( 'width': 12, 'height': 12, 'x': 56, 'y': 48, 'url': 'sprite.png' ), 'home_hover': ( 'width': 12, 'height': 12, 'x': 0, 'y': 0, 'url': 'sprite.png' ) ); // 公共部分依然占位符 %-sprite-common { background-image: url(sprite.png); } // 输出 background-position @mixin sprite-position($name) { $data: map-get($__sprite__, $name); $x: map-get($data, 'x'); $y: map-get($data, 'y'); background-position: -#{$x}px -#{$y}px; } // 输出其余切片元素私有数据 @mixin sprite-item($name) { // 继承公共样式 @extend %-sprite-common; // 设置独有样式 @include sprite-position($name); $data: map-get($__sprite__, $name); $width: map-get($data, 'width'); width: unquote($width + 'px'); height: unquote(map-get($data, 'height') + 'px'); }
生成出以上 SCSS 模板后,咱们就能够这样使用雪碧图了:
.head-home { @include sprite-item('home'); &:hover { @include sprite-position('home_hover'); } }
生成出来的效果相似于:
.head-home { background-image: url(sprite.png); } .head-home { background-position: -56px -48px; width: 12px; height: 12px; } .head-home:hover { background-position: -0px -0px; }
这样,在 :hover
态下生成的 CSS 规则只会包含必要的变更。
总结起来,预处理模式的优势在于,经过自定义 CSS 数据文件,能够为所欲为地使用已经生成出来的雪碧图信息。
然而,在预处理模式下,开发的页面依赖的是生成后的雪碧图,而不是合并前的雪碧图切片元素,随之带来的问题是:没办法实现雪碧图的按需合并。
预处理方案通常以页面为单元组织雪碧图。思考这样的问题:若是一张雪碧图对应一个页面,那各页面的公共组件使用的雪碧图,是每一个页面各一份副本,仍是只保存一次切片元素,把通用的抽成一张公共雪碧图呢?
若是各存一份,公共组件的切片元素就得保存到多个文件夹,每次更新、删除、添加的时候得同步多处。若是管理不当,就会致使页面元素不一样、废弃的切片仍被合并以及遗漏等问题。(以 CSS 文件为单元组织雪碧也会遇到相似的状况。)
若是抽成一张公共雪碧图,假设一个最简单场景:有 ABC 三个页面,其中 AB 页面有一个共同的切片元素 ab.png
,BC 页面也有一个共同的切片元素 bc.png
。这两张切片元素都放进了 spr_common.png
中。因为静态资源管理须要,咱们在打包的时候统一给资源加上签名,也就成了 spr_common.de353d.png
。如今,须要更新 ab.png
这张切片,进而变成了 spr_common.5ef25d.png
。而 C 页面的样式里包含了这张雪碧图,尽管自身没有任何变更,但因为公共雪碧图变了,C 页面的 CSS 也必须跟着变化。即,公共雪碧图会带来耦合问题,局部页面更新会形成其余页面没必要要的跟随变化。
后处理方案则解决了这些问题。
后处理方案经过对已经生成的 CSS 文件进行分析,将其中包含切片元素的 background
或 background-image
做为依赖收集,合并成雪碧图后再将相关参数替换。
如上述典型工具,生成前是:
.comment { background: url(images/sprite/ico-comment.png) no-repeat 0 0; } .bubble { background: url(images/sprite/ico-bubble.png) no-repeat 0 0; }
生成后是:
.comment { background-image: url(images/sprite.png); background-position: 0 0; } .bubble { background-image: url(images/sprite.png); background-position: 0 -50px; }
如此一来,CSS 中有哪些切片元素就合并哪些,不会把没有用到的切片也合并进去。即一张 CSS 样式表有一张专门的雪碧图。
不过,正如上面所看到的,后处理模式解决了按需合并的问题,也不会形成页面/组件间雪碧图的耦合,但却丧失了预处理方案中直接利用数据的便捷性。在预处理方案中,咱们不用人工地去衡量切片元素的宽高,而是让 SCSS 自动输出,后处理方案却作不到这一点。
既要能像预处理那样不用人工的地去量切片,又要像后处理那样实现按需的合并,这就是我理想的开发模式。
基于以上探索,我写了个工具:postcss-sprite-property 来实现两者的平衡。作法是:
node-sass
所支持的自定义函数功能,为 SCSS 注入 image-width
和 image-height
两个自定义函数,用来查询图片宽高数据@sprite-item
和 @sprite-position
两个混合,优雅地定义一张雪碧图元素来看一个实例:
咱们把以下样式放进咱们的公共样式库中:
// 使用雪碧图 // 以图片名做为参数 @mixin sprite-item($name) { // 统一一个路径存放切片元素 // 这样就能获得图片的实际地址了 $url: '../asset/sprite/#{$name}.png'; // 注入的 `image-width` 函数能够帮咱们查询图片宽度 $width: image-width($url); @if (null == $width) { @warn 'Sprite element `#{$name}` not found!'; } @else { // 获取图片高度 $height: image-height($url); // 自动书写 `width` 和 `height` width: $width; height: $height; // 定义背景 // 开发模式下直接按输出预览 // 生产环境下将其替换为雪碧图的数据 background: url($url) 0 0 / #{$width} #{$height} no-repeat; } } // 改变雪碧图 `background-position` // 和上面的 `sprite-item` 区别仅在于 // 这个混合不输出 `width` 和 `height` @mixin sprite-position($name) { $url: '../asset/sprite/#{$name}.png'; $width: image-width($url); @if (null == $width) { @warn 'Sprite element `#{$name}` not found!'; } @else { $height: image-height($url); background: url($url) 0 0 / #{$width} #{$height} no-repeat; } }
如今,具体项目里就能这样使用了:
.home-head { @include sprite-item('pageA/home'); &:hover { @include sprite-position('pageA/home_hover'); } } .home-back { @include sprite-item('pageB/back'); }
开发阶段,没有合成雪碧图,直接使用切片预览,因此效果是这样的:
.home-head { width: 12px; height: 12px; background: url("../asset/sprite/pageA/home.png") 0 0 / 12px 12px no-repeat; } .home-head:hover { background: url("../asset/sprite/pageA/home_hover.png") 0 0 / 12px 12px no-repeat; } .home-back { width: 18px; height: 20px; background: url("../asset/sprite/pageB/back.png") 0 0 / 18px 20px no-repeat; }
最后发布生成的样式则是这样的:
.home-head { width: 12px; height: 12px; background-position: -56px -48px; } .home-head:hover { background-position: 0 0; } .home-back { width: 18px; height: 20px; background-position: -10px 0; } .home-head, .home-head:hover, .home-back { background-image: url(sprite.png); }
固然,如上所示,.home-head
和 .home-head:hover
都在最后的公共规则中。最理想的固然是没有更好,但做为一个平衡方案,这还是可接受的。
更具体的用法和更多的功能请参看 Github:https://github.com/HaoyCn/pos...
这个算是雪碧图的硬伤。其余小图标方案不会有这个问题。REM 布局里雪碧图错位几乎是不可避免的,在安卓下错位甚至能够达到 2px 左右。我也总结出一个 CSS 兜错方案:
background-position
使用百分比单位spritesmith
工具设置 padding: 8
增长 1-2px 的容错区域,概要代码以下:
%-sprite { // 错位时的容错区域 padding: 1px; // 从内容区开始绘制背景 background-origin: content-box; // 在内边距盒外裁切背景 background-clip: padding-box; }
利用以上三个属性的组合来腾出容错的空间。
关于雪碧图的处理方案的讨论就到此结束了。待 HTTP2 普及以后,也就没有雪碧图什么事了,而如今仍有其存在的必要性。
处理小图标还有其余的方案,如 Iconfont 和 Svg-Sprite。总之没有最好的,只有最适合的。
感谢阅读。欢迎来文章顶部的小圈子一块儿讨论!