寫在前面
讀文先看此圖,能先有個大體概念:
閱讀本文需要 11m 24s。
CSS Modules介紹
CSS Modules是什麼東西呢?首先,讓我們從官方文件入手:
GitHub – css-modules/css-modules: Documentation about css-modules
A CSS Module is a CSS file in which all class names and animation names are scoped locally by default. CSS模組就是所有的類名都只有區域性作用域的CSS檔案。
所以CSS Modules既不是官方標準,也不是瀏覽器的特性,而是在構建步驟(例如使用Webpack或Browserify)中對CSS類名選擇器限定作用域的一種方式(通過hash實現類似於名稱空間的方法)。
It doesn’t really matter in the end (although shorter class names mean shorter stylesheets) because the point is that they are dynamically generated, unique, and mapped to the correct styles.在使用CSS模組時,類名是動態生成的,唯一的,並準確對應到原始檔中的各個類的樣式。
這也是實現樣式作用域的原理。它們被限定在特定的模板裡。例如我們在buttons.js裡引入buttons.css檔案,並使用.btn的樣式,在其他元件裡是不會被.btn影響的,除非它也引入了buttons.css.
可我們是出於什麼目的把CSS和HTML檔案搞得這麼零碎呢?我們為什麼要使用CSS模組呢?
為什麼我們需要CSS模組化
CSS全域性作用域問題
CSS的規則都是全域性的,任何一個元件的樣式規則,都對整個頁面有效。相信寫css的人都會遇到樣式衝突(汙染)的問題。
於是一般這麼做(筆者都做過):
* class命名寫長一點吧,降低衝突的機率
* 加個父元素的選擇器,限制範圍
* 重新命名個class吧,比較保險
所以亟待解決的問題就是css區域性作用域避免全域性樣式衝突(汙染)的問題
JS CSS無法共享變數
複雜元件要使用 JS 和 CSS 來共同處理樣式,就會造成有些變數在 JS 和 CSS 中冗餘,CSS前處理器/後處理器 等都不提供跨 JS 和 CSS 共享變數這種能力。
健壯並且擴充套件方便的CSS
作為有追求的工程師,編寫健壯並且擴充套件方便的CSS一直是我們的目標。那麼如何定義健壯並且擴充套件方便?有三個要點:
- 面向元件 – 處理 UI 複雜性的最佳實踐就是將 UI 分割成一個個的小元件 Locality_of_reference 。如果你正在使用一個合理的框架,JavaScript 方面就將原生支援(元件化)。舉個例子,React 就鼓勵高度元件化和分割。我們希望有一個 CSS 架構去匹配。
- 沙箱化(Sandboxed) – 如果一個元件的樣式會對其他元件產生不必要以及意想不到的影響,那麼將 UI 分割成元件並沒有什麼用。就這方面而言,CSS的全域性作用域會給你造成負擔。
- 方便 – 我們想要所有好的東西,並且不想產生更多的工作。也就是說,我們不想因為採用這個架構而讓我們的開發者體驗變得更糟。可能的話,我們想開發者體驗變得更好。
CSS模組化方案分類
CSS 模組化的解決方案有很多,但主要有三類。
CSS 命名約定
規範化CSS的模組化解決方案(比如BEM BEM — Block Element Modifier,OOCSS,AMCSS,SMACSS,SUITCSS)
但存在以下問題:
* JS CSS之間依然沒有打通變數和選擇器等
* 複雜的命名
CSS in JS
徹底拋棄 CSS,用 JavaScript 寫 CSS 規則,並內聯樣式。 React: CSS in JS // Speaker Deck。Radium,react-style 屬於這一類。但存在以下問題:
* 無法使用偽類,媒體查詢等
* 樣式程式碼也會出現大量重複。
* 不能利用成熟的 CSS 前處理器(或後處理器)
使用JS 來管理樣式模組
使用JS編譯原生的CSS檔案,使其具備模組化的能力,代表是 CSS Modules GitHub – css-modules/css-modules: Documentation about css-modules 。
CSS Modules 能最大化地結合現有 CSS 生態(前處理器/後處理器等)和 JS 模組化能力,幾乎零學習成本。只要你使用 Webpack,可以在任何專案中使用。是筆者認為目前最好的 CSS 模組化解決方案。
CSS Modules 使用教程
啟用 CSS Modules
1 2 |
// webpack.config.js css?modules&localIdentName=[name]__[local]-[hash:base64:5] |
加上 modules
即為啟用,localIdentName
是設定生成樣式的命名規則。
1 2 |
/* components/Button.css */ .normal { /* normal 相關的所有樣式 */ } |
1 2 3 4 |
// components/Button.js import styles from './Button.css'; console.log(styles); buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>` |
生成的 HTML 是
1 |
<button class="button--normal-abc53">Submit</button> |
注意到 button--normal-abc53
是 CSS Modules 按照 localIdentName
自動生成的 class 名。其中的abc53
是按照給定演算法生成的序列碼。經過這樣混淆處理後,class 名基本就是唯一的,大大降低了專案中樣式覆蓋的機率。同時在生產環境下修改規則,生成更短的 class 名,可以提高 CSS 的壓縮率。
上例中 console 列印的結果是:
1 2 3 4 |
Object { normal: 'button--normal-abc53', disabled: 'button--disabled-def886', } |
CSS Modules 對 CSS 中的 class 名都做了處理,使用物件來儲存原 class 和混淆後 class 的對應關係。
通過這些簡單的處理,CSS Modules 實現了以下幾點:
* 所有樣式都是區域性作用域 的,解決了全域性汙染問題
* class 名生成規則配置靈活,可以此來壓縮 class 名
* 只需引用元件的 JS 就能搞定元件所有的 JS 和 CSS
* 依然是 CSS,幾乎 0 學習成本
CSS Modules 在React中的實踐
那麼我們在React中怎麼使用?
手動引用解決
在 className
處直接使用 css 中 class
名即可。
1 2 3 4 5 6 7 8 9 10 11 |
import React from 'react'; import styles from './table.css'; export default class Table extends React.Component { render () { return <div className={styles.table}> <div className={styles.row}> </div> </div>; } } |
渲染出來的元件出來
1 2 3 4 |
<div class="table__table___32osj"> <div class="table__row___2w27N"> </div> </div> |
react-css-modules
如果你不想頻繁的輸入 styles.**
,有一個 GitHub – gajus/react-css-modules: Seamless mapping of class names to CSS modules inside of React components.,它通過高階函式的形式來生成className
,不過不推薦使用,後文會提到。
API也很簡單,給元件外包一個CSSModules即可。
1 2 3 4 5 6 7 8 9 10 11 12 |
import React from 'react'; import CSSModules from 'react-css-modules'; import styles from './table.css'; class Table extends React.Component { render () { return <div styleName='table'> </div>; } } export default CSSModules(Table, styles); |
不過這樣我們可以看到,它是需要執行時的依賴,而且需要在執行時才獲取className,效能損耗大,那麼有沒有方便又接近無損的方法呢?答案是有的,使用babel外掛babel-plugin-react-css-modules
GitHub – gajus/babel-plugin-react-css-modules: Transforms styleName to className using compile time CSS module resolution. 把className
獲取前置到編譯階段。
babel-plugin-react-css-modules
babel-plugin-react-css-modules
可以實現使用styleName
屬性自動載入CSS模組。我們通過該babel外掛來進行語法樹解析並最終生成className
。
來看看元件的寫法,現在你只需要把className
換成styleName
即可獲得CSS區域性作用域的能力了,是不是非常簡單。
1 2 3 4 5 6 7 8 9 10 11 |
import React from 'react'; import styles from './table.css'; class Table extends React.Component { render () { return <div styleName='table'> </div>; } } export default Table; |
工作原理
那麼該babel外掛是怎麼工作的呢?讓我們從官方文件入手:
筆者不才 ,稍作翻譯如下:
1. 構建每個檔案的所有樣式表匯入的索引(匯入具有.css
或.scss
副檔名的檔案)。
2. 使用postcss 解析匹配到的css檔案
3. 遍歷所有 JSX 元素宣告
4. 把styleName
屬性解析成匿名和命名的區域性css模組引用
5. 查詢與CSS模組引用相匹配的CSS類名稱:
* 如果styleName
的值是一個字串字面值,生成一個字串字面值。
* 如果是JSXExpressionContainer,在執行時使用helper函式來構建如果styleName
的值是一個jSXExpressionContainer
, 使用輔助函式([getClassName
]在執行時構造className
值。
6. 從元素上移除styleName
屬性。
7. 將生成的className
新增到現有的className
值中(如果不存在則建立className
屬性)。
使用例項
在成熟的專案中,一般都會用到CSS前處理器或者後處理器。
這裡以使用了stylus
CSS前處理器為例子,我們來看下如何使用。
- 安裝依賴
1 |
npm install -save-dev sugerss babel-plugin-react-css-modules |
- 編寫Webpack配置
123456789101112131415161718// webpack.config.jsmodule: {loaders: [{test: /\.js?$/,loader: [['babel-plugin-react-css-modules',{generateScopedName:'[name]__[local]',filetypes: {".styl": "sugerss"}}]]}, {test: /\.module.styl$/,loader: 'style!css?modules&localIdentName=[name]__[local]!styl?sourceMap=true'}, {test: /\.styl$/,loader: 'style!css!styl?sourceMap=true'}]}
- 元件寫法
1 2 3 4 5 6 7 8 9 10 11 |
import React from 'react'; import './table.module.styl'; class Table extends React.Component { render () { return <div styleName='table'> </div>; } } export default Table; |
如上,你可以通過配置Webpack中module.loaders的test路徑Webpack-module-loaders-configuration,來區分樣式檔案是否需要CSS模組化。
搭配sugerss
這個postcss
外掛作為stylus
的語法載入器,來支援babel外掛babel-plugin-react-css-modules
的語法解析。
最後我們回過頭來看下,我們React元件只需要把className
換成styleName
,搭配以上構建配置,即可實現CSS模組化 。
最後
CSS Modules 很好的解決了 CSS 目前面臨的模組化難題。支援與 CSS處理器搭配使用,能充分利用現有技術積累。如果你的產品中正好遇到類似問題,非常值得一試。
希望大家都能寫出健壯並且可擴充套件的CSS,以上。