boi剖析 - 基於webpack的css sprites實現方案

本文是58到家前端工程化集成解決方案boi的博文系列之一。boi是基於webpack打造的一站式前端工程化解決方案,現已開源Githubjavascript

做爲前端構建工具不可或缺的一個環節,自動生成css sprites圖片不只僅可以減小頻繁的人工操做,還可以避免多人協做時對同一個sprites圖片維護過程當中因我的緣由引發的圖片不規範問題。58到家前端工程化解決方案boi的自動css sprites功能基於webpack實現,本文記錄一下實現方案的各個細節以及須要注意的地方。css

1. 功能需求

css sprites的功能需求簡單說就是將style中引用的散列小圖標合併成一張sprites圖片。從功能角度來說比較單一,從實現角度來說須要具有如下幾點:html

  • 對style文件進行資源依賴分析,可以得出style中引用的圖片資源;
  • style文件引用的圖片並不是都是圖標,其餘的好比背景圖等資源不該該被sprites合併。因此必須有明確的標識能夠區分圖標與非圖標資源。

對於第一點,webpack自己就具有依賴分析的功能,因此無需自行實現。那麼如何設計明確的標識以便區分資源類型呢?前端

2. 用戶至上的設計原則

上文提到的資源標識,咱們首先看一下業內的同類產品是如何實現的。以fis爲例,請看如下代碼:java

li.list-1::before {
  background-image: url('./img/list-1.png?__sprite');
}
li.list-2::before {
  background-image: url('./img/list-2.png?__sprite');
}

fis的css sprites功能要求開發者在style代碼中添加__sprite標識,fis經過識別這個標識來區分資源類型。這種模式的優勢是能夠精確地進行定位,並且對圖標文件的路徑沒有強制要求,能夠將圖標文件與其餘資源文件混合存放。可是,在代碼中書寫標識,首先須要具體的業務開發人員時刻注意不要遺漏;其次,這種模式實質上是對代碼的一種「綁架」,代碼中存在與業務無關的內容而且可移植性不高。webpack

做爲框架,全部方案都應該遵循用戶至上的設計原則git

  • 配置API語義化,一目瞭然;
  • 減小代碼綁架,減小代碼中存在與業務無關的內容,以便代碼的高可移植性;
  • 提供高級配置API,方便用戶進行自定義。

基於以上原則,boi在設計配置API時儘可能作到了語義化,而且style代碼中不存在任何與業務無關的內容。如下代碼是boi配置css sprites功能的demo:github

boi.spec('style',{
    sprites: true,
    spritesConfig: {
        dir: 'assets/image/icons',
        split: true,
        retina: true,
        postcssSpritesOpts: null
    }
});

與sprites功能相關的配置項細節以下:web

  • sprites - Boolean,是否開啓自動sprites功能,默認false。只有在spritestrue時,spritesConfig纔會生效;
  • spritesConfig - Object,功能配置細節:
    • dir - String,圖標文件的目錄路徑,默認爲undefined。boi以路徑做爲區分圖標與非圖標資源的標識,也就是說參與自動sprites的圖標文件必須存放於獨立的目錄下,好比'assets/image/icons'
    • split - Boolean,是否識別子目錄而且每一個子目錄分別編譯爲sprites圖片,默認爲true。好比上述代碼對應的項目中存在圖標目錄'assets/image/icons',在此目錄下又存在兩個子目錄'assets/image/icons/index''assets/image/icons/admin',分別存在index頁面和admin頁面的圖標文件。若是配置split:true,boi將會編譯輸出兩個sprites圖片sprite.index.pngsprite.admin.png;若是配置split:false,boi只會編譯輸出一個sprites圖片文件sprite.icons.png
    • retina - Boolean,是否識別分辨率標識,默認爲true。分辨率標識指的是相似@2x的文件名標識,好比存在兩個圖標文件logo.pnglogo@2x.png而且style文件中對兩張圖標都有引用,以下:
    @media screen and (max-width:780px){
        .logo{
            background-image: url(../assets/icons/logo.png)
        }
    }
    @media screen and (min-width:781px and max-width:900px){
        .logo{
            background-image: url(../assets/icons/logo@2x.png)
        }
    }
    若是配置`retina:true`,boi將把兩種分辨率的圖片分別合併爲一張sprites圖片,不然會編譯到同一張sprites圖片裏。詳細內容能夠參考[boi-example-css-sprites](https://github.com/boijs/boi-example-css-sprites)。
    • postcssSpritesOpts - Object,默認爲null。boi使用postcss-sprites做爲實現css sprites的技術選型。postcssSpritesOpts是提供給用戶自定義postcss-sprites相關功能的,這個配置項通常狀況下是不須要用戶操做的。若是遇到上文提到的配置項不能知足的應用場景,用戶能夠經過此API直接對postcss-sprites進行配置。

3. 技術選型

boi實現css sprites功能的技術選型以下:前端工程化

4. 實現方案

上文第二節中提到了boi實現sprites功能的設計原則和工做模式。用戶在配置API中指定圖標文件的路徑
,boi以此路徑做爲區分圖標與非圖標文件的標識;而且支持識別分辨率標識進行單獨編譯。

在配置postcss時,要注意如下幾點:

  1. 使用less/sass等css預編譯器時postcss的執行時機問題;
  2. 經過路徑進行圖標文件合法性過濾;
  3. 以子目錄名稱和分辨率標識爲基礎的sprites圖片命名規則。

下文將分別介紹boi針對上述問題的具體解決方案。

4.1 與css預編譯器綜合使用

postcss並不是只支持原始的css語法,同時也支持less和sass等預編譯語法。webpack根據loader的前後順序從右至左依次進行編譯,好比:

{
    test: /\.less$/,
    loader: 'css!less'
}

webpack對less文件的編譯順序爲:less->css->style。那麼在使用postcss時應該在哪一步執行呢?

雖然postcss支持less和sass,筆者也並不推薦直接使用postcss去編譯less和sass。一方面是由於postcss支持的預編譯器類型有限;另外一方面即便postcss支持全部預編譯語言,考慮到用戶配置預編譯器的多樣性,若是對不一樣編譯器分派不一樣的postcss插件勢必會形成boi框架體積的臃腫。

基於上述的考慮,postcss-loader的位置就已經肯定了:在預編譯loader以後,css-loader以前。以下:

{
    test: /\.less$/,
    loader: 'css!postcss!less'
}

之因此在css-loader以前還有另一個緣由, postcss-sprites將散列的圖標合併成sprites以後首先要將生成的sprites圖片存放於一個臨時目錄內,而後在經過css-loader進行資源依賴解析並編譯到統一的dest目錄中。因此中間有一個暫存的過程,必須經過css-loader進行依賴解析才能獲得最終的結果。

4.2 合法性過濾

boi經過路徑進行圖標合法性標識,首先根據用戶的配置建立驗證正則:

const REG_SPRITES_NAME = new RegExp([
    path.posix.normalize(spritesConfig.dir).replace(/^\.*/, '').replace(/\//, '\\/'),
    '\\/\.+\\.',
    _.isArray(config.image.extType) ? '(' + config.image.extType.join('|') +')' : config.image.extType,
    '\$'
].join(''), 'i');

而後配置postcss-sprites的filterBy鉤子函數進行合法性驗證:

filterBy: (image) => {
    if (!REG_SPRITES_NAME.test(image.url)) {
        return Promise.reject();
    }
    return Promise.resolve();
}

4.3 分組規則

分組的依據有兩個:目錄名稱和分辨率標識。首先須要根據用戶的配置建立目錄名稱驗證和分辨率標識驗證的正則:

// 合法的散列圖path
const REG_SPRITES_PATH = new RegExp([
    path.posix.normalize(spritesConfig.dir).replace(/^\.*/, '').replace(/\//, '\\/'),
    '\\/(.*?)\\/.*'
].join(''), 'i');
// 合法的retina標識
const REG_SPRITES_RETINA = new RegExp([
    '@(\\d+)x\\.',
    _.isArray(config.image.extType) ? '(' + config.image.extType.join('|') +')' : config.image.extType,
].join(''), 'i');

而後經過postcss-sprites的groupBy鉤子函數進行分組規則制定:

groupBy: (image) => {
    let groups = null;
    let groupName = undefined;

    if (spritesConfig && spritesConfig.split) {
        groups = REG_SPRITES_PATH.exec(image.url);
        groupName = groups ? groups[1] : 'icons';
    } else {
        groupName = 'icons';
    }
    if (spritesConfig && spritesConfig.retina) {
        image.retina = true;
        image.ratio = 1;
        let ratio = REG_SPRITES_RETINA.exec(image.url);
        if (ratio) {
            ratio = ratio[1];
            while (ratio > 10) {
                ratio = ratio / 10;
            }
            image.ratio = ratio;
            image.groups = image.groups.filter((group) => {
                return ('@' + ratio + 'x') !== group;
            });
            groupName += '@' + ratio + 'x';
        }
    }
    return Promise.resolve(groupName);
}

上述代碼包括如下邏輯:

  • 若是用戶配置split:true,boi會對子目錄進行正則驗證,若是存在子目錄將會單獨分組;若不存子目錄子默認分組名稱爲'icons'
  • 若是用戶配置retina:true,boi會驗證圖標文件名是否包含分辨率標識,若是存在則將groupName加上相似'@2x'的後綴。

各位可能注意到上述代碼中如下的部分比較怪異:

image.groups = image.groups.filter((group) => {
    return ('@' + ratio + 'x') !== group;
});

postcss-sprites識別到圖標存在分辨率標識會生成單獨的分組名稱,若是不進行上述過濾的話,最終生成的sprites圖片名稱相似sprites.@2x.icons.png。以上過濾是爲了將@2x分組刪除,以便編譯後的文件名更具語義化,好比sprites.icons@2x.png

5. 開源代碼

各位能夠結合源碼/lib/config/genConfig/mp/style.js理解本文的內容。

相關文章
相關標籤/搜索