CSS 是前端領域中進化最慢的一塊。因爲 ES2015/2016 的快速普及和 Babel/Webpack 等工具的迅猛發展,CSS 被遠遠甩在了後面,逐漸成爲大型項目工程化的痛點。也變成了前端走向完全模塊化前必須解決的難題。
模塊化解決了JS做用域的問題,可是CSS仍是會存在樣式覆蓋的問題,由於最後打包最終會生成一份文件而不是開發時候的那樣分模塊分做用域。
因此,咱們今天討論的是如何在模塊化的工程下放心的寫CSS樣式而不擔憂樣式覆蓋,推薦的方案就是CSS Module。javascript
CSS 模塊化重要的是要解決好兩個問題:CSS 樣式的導入和導出。css
Sass/Less/PostCSS 等前仆後繼試圖解決 CSS 編程能力弱的問題,結果它們作的也確實優秀,但這並無解決模塊化最重要的問題。Facebook 工程師 Vjeux 首先拋出了 React 開發中遇到的一系列 CSS 相關問題。總結以下:前端
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。java
CSS 模塊化的解決方案有不少,但主要有兩類:react
Radium,jsxstyle,react-style 屬於這一類。
優勢是能給 CSS 提供 JS 一樣強大的模塊化能力。
缺點是不能利用成熟的 CSS 預處理器(或後處理器),Sass/Less/PostCSS,:hover 和 :active 僞類處理起來複雜。
表明是 CSS Modules。
CSS Modules 能最大化地結合現有 CSS 生態和 JS 模塊化能力,API 簡潔到幾乎零學習成本。
發佈時依舊編譯出單獨的 JS 和 CSS。它並不依賴於 React,只要你使用 Webpack,能夠在 Vue/Angular/jQuery 中使用。
CSS Modules 內部經過 ICSS 來解決樣式導入和導出這兩個問題。分別對應 :import 和 :export 兩個新增的僞類。webpack
:import("path/to/dep.css") { localAlias: keyFromDep; /* ... */ } :export { exportedKey: exportedValue; /* ... */ }
但直接使用這兩個關鍵字編程太麻煩,實際項目中不多會直接使用它們,咱們須要的是用 JS 來管理 CSS 的能力。結合 Webpack 的 css-loader 後,就能夠在 CSS 中定義樣式,在 JS 中導入。git
啓用 CSS Moduleses6
// webpack.config.js css?modules&localIdentName=[name]__[local]-[hash:base64:5]
加上 modules 即爲啓用,localIdentName 是設置生成樣式的命名規則。
也能夠這樣設置:github
test: /\.less$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, localIdentName: '[name]__[local]-[hash:base64:5]', }, }, ], },
樣式文件Button.css:web
.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">Submit</button>
注意到 button--normal-abc53 是 CSS Modules 按照 localIdentName 自動生成的 class 名。其中的 abc53 是按照給定算法生成的序列碼。通過這樣混淆處理後,class 名基本就是惟一的,大大下降了項目中樣式覆蓋的概率。同時在生產環境下修改規則,生成更短的class名,能夠提升CSS的壓縮率。
上例中 console 打印的結果是:
Object { normal: 'button--normal-abc53', disabled: 'button--disabled-def884', }
CSS Modules 對 CSS 中的 class 名都作了處理,使用對象來保存原 class 和混淆後 class 的對應關係。
經過這些簡單的處理,CSS Modules 實現瞭如下幾點:
樣式默認局部
使用了 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-daf62 button--normal-abc53">Submit</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 個級別,分別是:
綜上,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 { }
模塊化命名實踐:MBC 【僅供參考參考】
M:module 模塊(組件)名
B:block 元素塊的功能說明
C:custom 自定義內容
如何實現CSS,JS變量共享
注:CSS Modules 中沒有變量的概念,這裏的 CSS 變量指的是 Sass 中的變量。
上面提到的 :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 數量將成倍上升。
必定要知道,上面之因此稱爲建議,是由於 CSS Modules 並不強制你必定要這麼作。聽起來有些矛盾,因爲多數 CSS 項目存在深厚的歷史遺留問題,過多的限制就意味着增長遷移成本和與外部合做的成本。初期使用中確定須要一些折衷。幸運的是,CSS Modules 這點作的很好:
若是我對一個元素使用多個 class 呢?
沒問題,樣式照樣生效。
如何我在一個 style 文件中使用同名 class 呢?
沒問題,這些同名 class 編譯後雖然多是隨機碼,但還是同名的。
若是我在 style 文件中使用僞類,標籤選擇器等呢?
沒問題,全部這些選擇器將不被轉換,原封不動的出如今編譯後的 css 中。也就是說 CSS Modules 只會轉換 class 名和 id 選擇器名相關的樣式。
但注意,上面 3 個「若是」儘可能不要發生。
CSS Modules 結合 React 實踐
首先,在 CSS loader中開啓CSS Module:
{ loader: 'css-loader', options: { modules: true, localIdentName: '[local]', }, },
在 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.xx,能夠試一下 react-css-modules,它經過高階函數的形式來避免重複輸入 styles.xx。
注意⚠️
React 中使用CSS Module,能夠設置
1.多個class的狀況
// 可使用字符串拼接 className = {style.oneclass+' '+style.twoclass} //可使用es6的字符串模板 className = {`${style['calculator']} ${style['calculator']}`}
2.若是class使用的是連字符可使用數組方式style['box-text']
3.一個class是父組件傳下來的
若是一個class是父組件傳下來的,在父組件已經使用style轉變過了,在子組件中就不須要再style轉變一次,例子以下:
//父組件render中 <CalculatorKey className={style['key-0']} onPress={() => this.inputDigit(0)}>0</CalculatorKey> //CalculatorKey 組件render中 const { onPress, className, ...props } = this.props; return ( <PointTarget onPoint={onPress}> <button className={`${style['calculator-key']} ${className}`} {...props}/> </PointTarget> )
子組件CalculatorKey接收到的className已經在父組件中style轉變過了
4.顯示undefined
若是一個class你沒有在樣式文件中定義,那麼最後顯示的就是undefined,而不是一個style事後的沒有任何樣式的class, 這點很奇怪。
/* HTML */ <template> <h1 @click="clickHandler">{{ msg }}</h1> </template> /* script */ <script> module.exports = { data: function() { return { msg: 'Hello, world!' } }, methods:{ clickHandler(){ alert('hi'); } } } </script> /* scoped CSS */ <style scoped> h1 { color: red; font-size: 46px; } </style>
在Vue 元件檔,透過上面這樣的方式提供了一個template (HTML)、script 以及帶有scoped 的style 樣式,也仍然能夠保有過去HTML、CSS 與JS 分離的開發體驗。但本質上還是all-in-JS 的變種語法糖。
值得一提的是,當style標籤加上scoped屬性,那麼在Vue元件檔通過編譯後,會自動在元件上打上一個隨機的屬性 data-v-hash
,再透過CSS Attribute Selector的特性來作到CSS scope的切分,使得即使是在不一樣元件檔裏的h1也能有CSS樣式不互相干擾的效果。固然開發起來,比起JSX、或是inline-style等的方式,這種折衷的做法更漂亮。
幾點思考:Stop using CSS in JavaScript for web development
參考連接:
1.https://github.com/camsong/bl...
2.https://github.com/ckinmind/R...
3.從Vue 來看CSS 管理方案的發展