css模組化及CSS Modules使用詳解
什麼是css模組化?
為了理解css模組化思想,我們首先了解下,什麼是模組化,在百度百科上的解釋是,在系統的結構中,模組是可組合、分解和更換的單元。模組化是一種處理複雜系統分解成為更好的可管理模組的方式。它可以通過在不同元件設定不同的功能,把一個問題分解成多個小的獨立、互相作用的元件,來處理複雜、大型的軟體。看完模組化,是不是有種拼圖的即視感,可以把大圖分成各個小圖,然後把小圖拼成大圖,分與合的藝術感。那麼css模組化思想,也就是在css編寫環境中,用上模組化的思想,把一個大的專案,分解成獨立的元件,不同的元件負責不同的功能,最後把模組組裝,就成了我們要完成的專案了。css模組化有什麼好處?
當做一個大專案,幾個人團隊合作開發,結果看不懂彼此的程式碼,怎麼辦,當面對前人已經寫好程式碼,需要修改,可是無處下手,怎麼辦.當代碼耦合,修改費時費力,怎麼辦,當需要迭代,面對龐大的程式碼,牽一髮動全身的悲催時刻,怎麼辦,這個時候,模組化思想就是救星了。css寫法特別的靈活,也因為靈活,所以容易耦合在一起,這時候就需要進行模組化的分離。那麼css模組化的好處多多,列舉了一些如下:- 提高程式碼重用率
- 提高開發效率、減少溝通成本
- 提高頁面容錯
- 降低耦合
- 降低釋出風險
- 減少Bug定位時間和Fix成本
- 更好的實現快速迭代
- 便於程式碼維護
CSS 模組化的解決方案有很多,但主要有兩類。一類是徹底拋棄 CSS,使用 JS 或 JSON 來寫樣式。Radium,jsxstyle,react-style 屬於這一類。優點是能給 CSS 提供 JS 同樣強大的模組化能力;缺點是不能利用成熟的 CSS 前處理器(或後處理器) Sass/Less/PostCSS,:hover
和 :active
偽類處理起來複雜。另一類是依舊使用 CSS,但使用 JS 來管理樣式依賴,代表是
CSS 模組化遇到了哪些問題?
CSS 模組化重要的是要解決好兩個問題:CSS 樣式的匯入和匯出。靈活按需匯入以便複用程式碼;匯出時要能夠隱藏內部作用域,以免造成全域性汙染。Sass/Less/PostCSS 等前仆後繼試圖解決 CSS 程式設計能力弱的問題,結果它們做的也確實優秀,但這並沒有解決模組化最重要的問題。Facebook 工程師
全域性汙染
CSS 使用全域性選擇器機制來設定樣式,優點是方便重寫樣式。缺點是所有的樣式都是全域性生效,樣式可能被錯誤覆蓋,因此產生了非常醜陋的!important
,甚至 inline!important
和複雜的選擇器權重計數表,提高犯錯概率和使用成本。Web Components 標準中的 Shadow DOM 能徹底解決這個問題,但它的做法有點極端,樣式徹底區域性化,造成外部無法重寫樣式,損失了靈活性。命名混亂 由於全域性汙染的問題,多人協同開發時為了避免樣式衝突,選擇器越來越複雜,容易形成不同的命名風格,很難統一。樣式變多後,命名將更加混亂。
依賴管理不徹底
元件應該相互獨立,引入一個元件時,應該只引入它所需要的 CSS 樣式。但現在的做法是除了要引入 JS,還要再引入它的 CSS,而且 Saas/Less 很難實現對每個元件都編譯出單獨的 CSS,引入所有模組的 CSS 又造成浪費。JS 的模組化已經非常成熟,如果能讓 JS 來管理 CSS 依賴是很好的解決辦法。Webpack 的css-loader
提供了這種能力。無法共享變數 複雜元件要使用 JS 和 CSS 來共同處理樣式,就會造成有些變數在 JS 和 CSS 中冗餘,Sass/PostCSS/CSS 等都不提供跨 JS 和 CSS 共享變數這種能力。
程式碼壓縮不徹底 由於移動端網路的不確定性,現在對 CSS 壓縮已經到了變態的程度。很多壓縮工具為了節省一個位元組會把 '16px' 轉成 '1pc'。但對非常長的 class 名卻無能為力,力沒有用到刀刃上。
上面的問題如果只憑 CSS 自身是無法解決的,如果是通過 JS 來管理 CSS 就很好解決,因此 Vjuex 給出的解決方案是完全的 CSS in JS,但這相當於完全拋棄 CSS,在 JS 中以 Object 語法來寫 CSS,估計剛看到的小夥伴都受驚了。直到出現了 CSS Modules。
CSS Modules 模組化方案
CSS Modules 內部通過 ICSS 來解決樣式匯入和匯出這兩個問題。分別對應 :import
和 :export
兩個新增的偽類。
:import("path/to/dep.css") {
localAlias: keyFromDep;
/* ... */
}
:export {
exportedKey: exportedValue;
/* ... */
}
但直接使用這兩個關鍵字程式設計太麻煩,實際專案中很少會直接使用它們,我們需要的是用 JS 來管理 CSS 的能力。結合 Webpack 的 css-loader
後,就可以在 CSS 中定義樣式,在 JS 中匯入。
啟用 CSS Modules
// webpack.config.js
css?modules&localIdentName=[name]__[local]-[hash:base64:5]
加上 modules
即為啟用,localIdentName
是設定生成樣式的命名規則。
/* components/Button.css */
.normal { /* normal 相關的所有樣式 */ }
.disabled { /* disabled 相關的所有樣式 */ }
/* components/Button.js */
import styles from './Button.css';
console.log(styles);
buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`
生成的 HTML 是
<button class="button--normal-abc53"> Processing... </button>
注意到 button--normal-abc5436
是 CSS Modules 按照 localIdentName
自動生成的 class 名。其中的 abc5436
是按照給定演算法生成的序列碼。經過這樣混淆處理後,class 名基本就是唯一的,大大降低了專案中樣式覆蓋的機率。同時在生產環境下修改規則,生成更短的 class 名,可以提高 CSS 的壓縮率。
上例中 console 列印的結果是:
Object {
normal: 'button--normal-abc546',
disabled: 'button--disabled-def884',
}
CSS Modules 對 CSS 中的 class 名都做了處理,使用物件來儲存原 class 和混淆後 class 的對應關係。
通過這些簡單的處理,CSS Modules 實現了以下幾點:
所有樣式都是 local 的,解決了命名衝突和全域性汙染問題
class 名生成規則配置靈活,可以此來壓縮 class 名
只需引用元件的 JS 就能搞定元件所有的 JS 和 CSS
依然是 CSS,幾乎 0 學習成本
樣式預設區域性
使用了 CSS Modules 後,就相當於給每個 class 名外加加了一個 :local
,以此來實現樣式的區域性化,如果你想切換到全域性模式,使用對應的 :global
。
.normal {
color: green;
}
/* 以上與下面等價 */
:local(.normal) {
color: green;
}
/* 定義全域性樣式 */
:global(.btn) {
color: red;
}
/* 定義多個全域性樣式 */
:global {
.link {
color: green;
}
.box {
color: yellow;
}
}
Compose 來組合樣式
對於樣式複用,CSS Modules 只提供了唯一的方式來處理:composes
組合
/* components/Button.css */
.base { /* 所有通用的樣式 */ }
.normal {
composes: base;
/* normal 其它樣式 */
}
.disabled {
composes: base;
/* disabled 其它樣式 */
}
import styles from './Button.css';
buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`
生成的 HTML 變為
<button class="button--base-abc53 button--normal-abc53"> Processing... </button>
由於在 .normal
中 composes 了 .base
,編譯後會 normal 會變成兩個 class。
composes 還可以組合外部檔案中的樣式。
/* settings.css */
.primary-color {
color: #f40;
}
/* components/Button.css */
.base { /* 所有通用的樣式 */ }
.primary {
composes: base;
composes: $primary-color from './settings.css';
/* primary 其它樣式 */
}
對於大多數專案,有了 composes
後已經不再需要 Sass/Less/PostCSS。但如果你想用的話,由於 composes
不是標準的 CSS 語法,編譯時會報錯。就只能使用前處理器自己的語法來做樣式複用了。
class 命名技巧
CSS Modules 的命名規範是從 BEM 擴充套件而來。BEM 把樣式名分為 3 個級別,分別是:
Block:對應模組名,如 Dialog
Element:對應模組中的節點名 Confirm Button
Modifier:對應節點相關的狀態,如 disabled、highlight
綜上,BEM 最終得到的 class 名為 dialog__confirm-button--highlight
。使用雙符號 __
和 --
是為了和區塊內單詞間的分隔符區分開來。雖然看起來有點奇怪,但 BEM 被非常多的大型專案和團隊採用。我們實踐下來也很認可這種命名方法。
CSS Modules 中 CSS 檔名恰好對應 Block 名,只需要再考慮 Element 和 Modifier。BEM 對應到 CSS Modules 的做法是:
/* .dialog.css */
.ConfirmButton--disabled {
}
你也可以不遵循完整的命名規範,使用 camelCase 的寫法把 Block 和 Modifier 放到一起:
/* .dialog.css */
.disabledConfirmButton {
}
如何實現CSS,JS變數共享
上面提到的 :export
關鍵字可以把 CSS 中的 變數輸出到 JS 中。下面演示如何在 JS 中讀取 Sass 變數:
/* config.scss */
$primary-color: #f40;
:export {
primaryColor: $primary-color;
}
/* app.js */
import style from 'config.scss';
// 會輸出 #F40
console.log(style.primaryColor);
CSS Modules 使用技巧
CSS Modules 是對現有的 CSS 做減法。為了追求簡單可控,作者建議遵循如下原則:
不使用選擇器,只使用 class 名來定義樣式
不層疊多個 class,只使用一個 class 把所有樣式定義好
所有樣式通過
composes
組合來實現複用不巢狀
上面兩條原則相當於削弱了樣式中最靈活的部分,初使用者很難接受。第一條實踐起來難度不大,但第二條如果模組狀態過多時,class 數量將成倍上升。
一定要知道,上面之所以稱為建議,是因為 CSS Modules 並不強制你一定要這麼做。聽起來有些矛盾,由於多數 CSS 專案存在深厚的歷史遺留問題,過多的限制就意味著增加遷移成本和與外部合作的成本。初期使用中肯定需要一些折衷。幸運的是,CSS Modules 這點做的很好:
如果我對一個元素使用多個 class 呢?
沒問題,樣式照樣生效。
如何我在一個 style 檔案中使用同名 class 呢?
沒問題,這些同名 class 編譯後雖然可能是隨機碼,但仍是同名的。
如果我在 style 檔案中使用了 id 選擇器,偽類,標籤選擇器等呢?
沒問題,所有這些選擇器將不被轉換,原封不動的出現在編譯後的 css 中。也就是說 CSS Modules 只會轉換 class 名相關樣式。
但注意,上面 3 個“如果”儘量不要發生。
CSS Modules 結合 React 實踐
在 className
處直接使用 css 中 class
名即可。
/* dialog.css */
.root {}
.confirm {}
.disabledConfirm {}
import classNames from 'classnames';
import styles from './dialog.css';
export default class Dialog extends React.Component {
render() {
const cx = classNames({
[styles.confirm]: !this.state.disabled,
[styles.disabledConfirm]: this.state.disabled
});
return <div className={styles.root}>
<a className={cx}>Confirm</a>
...
</div>
}
}
注意,一般把元件最外層節點對應的 class 名稱為 root
。這裡使用了 classnames 庫來操作 class 名。
如果你不想頻繁的輸入 styles.**
,可以試一下 react-css-modules,它通過高階函式的形式來避免重複輸入 styles.**
。
CSS Modules 結合歷史遺留專案實踐
好的技術方案除了功能強大炫酷,還要能做到現有專案能平滑遷移。CSS Modules 在這一點上表現的非常靈活。
外部如何覆蓋區域性樣式
當生成混淆的 class 名後,可以解決命名衝突,但因為無法預知最終 class 名,不能通過一般選擇器覆蓋。我們現在專案中的實踐是可以給元件關鍵節點加上 data-role
屬性,然後通過屬性選擇器來覆蓋樣式。
如
// dialog.js
return <div className={styles.root} data-role='dialog-root'>
<a className={styles.disabledConfirm} data-role='dialog-confirm-btn'>Confirm</a>
...
</div>
// dialog.css
[data-role="dialog-root"] {
// override style
}
因為 CSS Modules 只會轉變類選擇器,所以這裡的屬性選擇器不需要新增 :global
。
如何與全域性樣式共存
前端專案不可避免會引入 normalize.css 或其它一類全域性 css 檔案。使用 Webpack 可以讓全域性樣式和 CSS Modules 的區域性樣式和諧共存。下面是我們專案中使用的 webpack 部分配置程式碼:
module: {
loaders: [{
test: /\.jsx?$/,
loader: 'babel'
}, {
test: /\.scss$/,
exclude: path.resolve(__dirname, 'src/styles'),
loader: 'style!css?modules&localIdentName=[name]__[local]!sass?sourceMap=true'
}, {
test: /\.scss$/,
include: path.resolve(__dirname, 'src/styles'),
loader: 'style!css!sass?sourceMap=true'
}]
}
/* src/app.js */
import './styles/app.scss';
import Component from './view/Component'
/* src/views/Component.js */
// 以下為元件相關樣式
import './Component.scss';
目錄結構如下:
src
├── app.js
├── styles
│ ├── app.scss
│ └── normalize.scss
└── views
├── Component.js
└── Component.scss
這樣所有全域性的樣式都放到 src/styles/app.scss
中引入就可以了。其它所有目錄包括 src/views
中的樣式都是區域性的。
總結
CSS Modules 很好的解決了 CSS 目前面臨的模組化難題。支援與 Sass/Less/PostCSS 等搭配使用,能充分利用現有技術積累。同時也能和全域性樣式靈活搭配,便於專案中逐步遷移至 CSS Modules。CSS Modules 的實現也屬輕量級,未來有標準解決方案後可以低成本遷移。如果你的產品中正好遇到類似問題,非常值得一試。