CSS > 關於雪碧圖預處理和後處理方案的討論

廣告:SF 里弄了個 CSS 小圈子,歡迎一塊兒來討論問題css

前端小圖標處理方案衆多,本文主要介紹基於雪碧圖的處理方案,分析雪碧圖的預處理和後處理模式的得與失,以及在項目中一般會遇到的問題以及解決方案。其餘小圖標處理方案未在此文探討之列。前端

此外,本文更多的是理論性分析,具體技術實現不做深刻解釋。若有疑問,歡迎到上述小圈子一塊兒討論!node

討論的起點是工程工具 Gulp/Webpack 上的集成方案(手動拼合雪碧圖的作法已經做古了)。webpack

1. 預處理方案

表明:gulp.spritesmithwebpack-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 也必須跟着變化。即,公共雪碧圖會帶來耦合問題,局部頁面更新會形成其餘頁面沒必要要的跟隨變化。

後處理方案則解決了這些問題。

2. 後處理方案

典型:postcss-sprites

後處理方案經過對已經生成的 CSS 文件進行分析,將其中包含切片元素的 backgroundbackground-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 自動輸出,後處理方案卻作不到這一點。

3. 預處理和後處理相結合

既要能像預處理那樣不用人工的地去量切片,又要像後處理那樣實現按需的合併,這就是我理想的開發模式。

基於以上探索,我寫了個工具:postcss-sprite-property 來實現兩者的平衡。作法是:

  1. 區分開發環境和生產環境。在開發環境中,不合並雪碧圖,直接使用切片元素預覽;生產環境中,爲每一張樣式表內的切片合併雪碧圖
  2. 利用 node-sass 所支持的自定義函數功能,爲 SCSS 注入 image-widthimage-height 兩個自定義函數,用來查詢圖片寬高數據
  3. 使用 @sprite-item@sprite-position 兩個混合,優雅地定義一張雪碧圖元素
  4. 儘可能爲公共屬性生成一個公共的規則
  5. Webpack 中,開發時不拼合雪碧圖,讓切片自己做爲組件依賴;打包時合併雪碧圖並剔除切片元素

來看一個實例:

咱們把以下樣式放進咱們的公共樣式庫中:

// 使用雪碧圖
// 以圖片名做爲參數
@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...

4. REM 佈局中雪碧圖的錯位問題

這個算是雪碧圖的硬傷。其餘小圖標方案不會有這個問題。REM 佈局裏雪碧圖錯位幾乎是不可避免的,在安卓下錯位甚至能夠達到 2px 左右。我也總結出一個 CSS 兜錯方案:

  • background-position 使用百分比單位
  • 給雪碧圖增長透明內邊距,如 spritesmith 工具設置 padding: 8
  • 增長 1-2px 的容錯區域,概要代碼以下:

    %-sprite {
        // 錯位時的容錯區域
        padding: 1px;
        // 從內容區開始繪製背景
        background-origin: content-box;
        // 在內邊距盒外裁切背景
        background-clip: padding-box;
    }

    利用以上三個屬性的組合來騰出容錯的空間。

4. 結語

關於雪碧圖的處理方案的討論就到此結束了。待 HTTP2 普及以後,也就沒有雪碧圖什麼事了,而如今仍有其存在的必要性。

處理小圖標還有其餘的方案,如 Iconfont 和 Svg-Sprite。總之沒有最好的,只有最適合的。

感謝閱讀。歡迎來文章頂部的小圈子一塊兒討論!

相關文章
相關標籤/搜索