基於webpack的css sprites實現方案

才子鍋鍋發表於2017-09-20

本文首發於部落格園

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

1. 功能需求

css sprites的功能需求簡單說就是將style中引用的雜湊小圖示合併成一張sprites圖片。從功能角度來講比較單一,從實現角度來講需要具備以下幾點:

  • 對style檔案進行資源依賴分析,能夠得出style中引用的圖片資源;
  • style檔案引用的圖片並非都是圖示,其他的比如背景圖等資源不應該被sprites合併。所以必須有明確的標識可以區分圖示與非圖示資源。

對於第一點,webpack本身就具備依賴分析的功能,所以無需自行實現。那麼如何設計明確的標識以便區分資源型別呢?

2. 使用者至上的設計原則

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

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通過識別這個標識來區分資源型別。這種模式的優點是可以精確地進行定位,而且對圖示檔案的路徑沒有強制要求,可以將圖示檔案與其他資原始檔混合存放。但是,在程式碼中書寫標識,首先需要具體的業務開發人員時刻注意不要遺漏;其次,這種模式實質上是對程式碼的一種“綁架”,程式碼中存在與業務無關的內容並且可移植性不高。

作為框架,所有方案都應該遵循使用者至上的設計原則

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

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

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

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

  • 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檔案中對兩張圖示都有引用,如果配置retina:true,boi將把兩種解析度的圖片分別合併為一張sprites圖片,否則會編譯到同一張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鉤子函式進行合法性驗證: javascript 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理解本文的內容。

相關文章