boi博文系列 - 基於webpack的css sprites實現方案
本文是58到家前端工程化集成解決方案boi的博文系列之一。boi是基於webpack打造的一站式前端工程化解決方案,現已開源Github。
作為前端構建工具不可或缺的一個環節,自動生成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。只有在sprites為true時,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.png和sprite.admin.png;如果配置split:false,boi只會編譯輸出一個sprites圖片文件sprite.icons.png。
- retina - Boolean,是否識別解析度標識,默認為true。解析度標識指的是類似@2x的文件名標識,比如存在兩個圖標文件logo.png和logo@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。
- postcssSpritesOpts - Object,默認為null。boi使用postcss-sprites作為實現css sprites的技術選型。postcssSpritesOpts是提供給用戶自定義postcss-sprites相關功能的,這個配置項一般情況下是不需要用戶操作的。如果遇到上文提到的配置項不能滿足的應用場景,用戶可以通過此API直接對postcss-sprites進行配置。
3. 技術選型
boi實現css sprites功能的技術選型如下:
- 構建內核: webpack;
- 資源編譯loader:postcss-loader
- sprites功能實現: postcss-sprites
4. 實現方案
上文第二節中提到了boi實現sprites功能的設計原則和工作模式。用戶在配置API中指定圖標文件的路徑
,boi以此路徑作為區分圖標與非圖標文件的標識;並且支持識別解析度標識進行單獨編譯。在配置postcss時,要注意以下幾點:
- 使用less/sass等css預編譯器時postcss的執行時機問題;
- 通過路徑進行圖標文件合法性過濾;
- 以子目錄名稱和解析度標識為基礎的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 分組規則
分組的依據有兩個:目錄名稱和解析度標識。首先需要根據用戶的配置創建目錄名稱驗證和解析度標識驗證的正則:
// 合法的散列圖pathconst 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理解本文的內容。
推薦閱讀:
※知乎的密碼輸入框的明文可以在控制台看到?
※極樂技術周報(第二十一期)
※為什麼沒有《前端面試寶典》?
※前端,準備年後跳槽,從現在開始準備,該制定怎樣的計劃?
※前端有哪些好的學習網站?