CSS Modules 詳解及 React 中實踐

CSS 是前端領域中進化最慢的一塊。因爲 ES2015/2016 的快速普及和 Babel/Webpack 等工具的迅猛發展,CSS 被遠遠甩在了後面,逐漸成爲大型項目工程化的痛點。也變成了前端走向完全模塊化前必須解決的難題。 javascript

CSS 模塊化的解決方案有不少,但主要有兩類。一類是完全拋棄 CSS,使用 JS 或 JSON 來寫樣式。Radiumjsxstylereact-style 屬於這一類。優勢是能給 CSS 提供 JS 一樣強大的模塊化能力;缺點是不能利用成熟的 CSS 預處理器(或後處理器) Sass/Less/PostCSS,`:hover` 和 `:active` 僞類處理起來複雜。另外一類是依舊使用 CSS,但使用 JS 來管理樣式依賴,表明是 CSS Modules。CSS Modules 能最大化地結合現有 CSS 生態和 JS 模塊化能力,API 簡潔到幾乎零學習成本。發佈時依舊編譯出單獨的 JS 和 CSS。它並不依賴於 React,只要你使用 Webpack,能夠在 Vue/Angular/jQuery 中使用。是我認爲目前最好的 CSS 模塊化解決方案。近期在項目中大量使用,下面具體分享下實踐中的細節和想法。 css


CSS 模塊化遇到了哪些問題?

CSS 模塊化重要的是要解決好兩個問題:CSS 樣式的導入和導出。靈活按需導入以便複用代碼;導出時要可以隱藏內部做用域,以避免形成全局污染。Sass/Less/PostCSS 等前仆後繼試圖解決 CSS 編程能力弱的問題,結果它們作的也確實優秀,但這並無解決模塊化最重要的問題。Facebook 工程師 Vjeux 首先拋出了 React 開發中遇到的一系列 CSS 相關問題。加上我我的的見解,總結以下: html


1. 全局污染

CSS 使用全局選擇器機制來設置樣式,優勢是方便重寫樣式。缺點是全部的樣式都是全局生效,樣式可能被錯誤覆蓋,所以產生了很是醜陋的 `!important`,甚至 inline `!important` 和複雜的[選擇器權重計數表](Selectors Level 3),提升犯錯機率和使用成本。Web Components 標準中的 Shadow DOM 能完全解決這個問題,但它的作法有點極端,樣式完全局部化,形成外部沒法重寫樣式,損失了靈活性。 前端

2. 命名混亂 java


因爲全局污染的問題,多人協同開發時爲了不樣式衝突,選擇器愈來愈複雜,容易造成不一樣的命名風格,很難統一。樣式變多後,命名將更加混亂。 react

3. 依賴管理不完全 webpack

組件應該相互獨立,引入一個組件時,應該只引入它所須要的 CSS 樣式。但如今的作法是除了要引入 JS,還要再引入它的 CSS,並且 Saas/Less 很難實現對每一個組件都編譯出單獨的 CSS,引入全部模塊的 CSS 又形成浪費。JS 的模塊化已經很是成熟,若是能讓 JS 來管理 CSS 依賴是很好的解決辦法。Webpack 的 `css-loader` 提供了這種能力。 git

4. 沒法共享變量 github

複雜組件要使用 JS 和 CSS 來共同處理樣式,就會形成有些變量在 JS 和 CSS 中冗餘,Sass/PostCSS/CSS 等都不提供跨 JS 和 CSS 共享變量這種能力。 web

5. 代碼壓縮不完全

因爲移動端網絡的不肯定性,如今對 CSS 壓縮已經到了變態的程度。不少壓縮工具爲了節省一個字節會把 '16px' 轉成 '1pc'。但對很是長的 class 名卻無能爲力,力沒有用到刀刃上。

上面的問題若是隻憑 CSS 自身是沒法解決的,若是是經過 JS 來管理 CSS 就很好解決,所以 Vjuex 給出的解決方案是徹底的 CSS in JS,但這至關於徹底拋棄 CSS,在 JS 中以 Object 語法來寫 CSS,估計剛看到的小夥伴都受驚了。直到出現了 CSS Modules。


CSS Modules 模塊化方案

CSS Modules 內部經過 [ICSS](css-modules/icss · GitHub) 來解決樣式導入和導出這兩個問題。分別對應 `: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">Submit</button>

注意到 `button--normal-abc53` 是 CSS Modules 按照 `localIdentName` 自動生成的 class 名。其中的 `abc53` 是按照給定算法生成的序列碼。通過這樣混淆處理後,class 名基本就是惟一的,大大下降了項目中樣式覆蓋的概率。同時在生產環境下修改規則,生成更短的 class 名,能夠提升 CSS 的壓縮率。

上例中 console 打印的結果是:

Object {
  normal: 'button--normal-abc53',
  disabled: 'button--disabled-def886',
}

CSS Modules 對 CSS 中的 class 名都作了處理,使用對象來保存原 class 和混淆後 class 的對應關係。

經過這些簡單的處理,CSS Modules 實現瞭如下幾點:

  • 全部樣式都是 local 的,解決了命名衝突和全局污染問題
  • class 名生成規則配置靈活,能夠此來壓縮 class 名
  • 只需引用組件的 JS 就能搞定組件全部的 JS 和 CSS
  • 依然是 CSS,幾乎 0 學習成本

樣式默認局部

使用了 CSS Modules 後,就至關於給每一個 class 名外加加了一個 `:local`,以此來實現樣式的局部化,若是你想切換到全局模式,使用對應的 `:global`。

`:local` 與 `:global` 的區別是 CSS Modules 只會對 `:local` 塊的 class 樣式作 `localIdentName` 規則處理,`: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-fec26 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 個級別,分別是:

  • 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變量共享

注: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 名來定義樣式
  • 不層疊多個 class,只使用一個 class 把全部樣式定義好
  • 不嵌套
  • 使用 `composes` 組合來實現複用

上面兩條原則至關於削弱了樣式中最靈活的部分,初使用者很難接受。第一條實踐起來難度不大,但第二條若是模塊狀態過多時,class 數量將成倍上升。

必定要知道,上面之因此稱爲建議,是由於 CSS Modules 並不強制你必定要這麼作。聽起來有些矛盾,因爲多數 CSS 項目存在深厚的歷史遺留問題,過多的限制就意味着增長遷移成本和與外部合做的成本。初期使用中確定須要一些折衷。幸運的是,CSS Modules 這點作的很好:

1. 若是我對一個元素使用多個 class 呢?

沒問題,樣式照樣生效。

2. 如何我在一個 style 文件中使用同名 class 呢?

沒問題,這些同名 class 編譯後雖然多是隨機碼,但還是同名的。

3. 若是我在 style 文件中使用了 id 選擇器,僞類,標籤選擇器等呢?

沒問題,全部這些選擇器將不被轉換,原封不動的出如今編譯後的 css 中。也就是說 CSS Modules 只會轉換 class 名相關樣式。

但注意,上面 3 個「若是」儘可能不要發生


CSS Modules 結合 React 實踐

在 `className` 處直接使用 css 中 `class` 名便可。

.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](https://www.npmjs.com/package/classnames) 庫來操做 class 名。

若是你不想頻繁的輸入 `styles.**`,能夠試一下 [react-css-modules](gajus/react-css-modules · GitHub),它經過高階函數的形式來避免重複輸入 `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 部分配置代碼:

// webpack.config.js 局部
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 的實現也屬輕量級,將來有標準解決方案後能夠低成本遷移。若是你的產品中正好遇到相似問題,很是值得一試。

相關文章
相關標籤/搜索