如何在 React 中優雅的寫 CSS

  • 2019 年 12 月 20 日
  • 筆記

本文首發於政采雲前端團隊博客:如何在 React 中優雅的寫 CSS https://www.zoo.team/article/react-css

引言

問題:CSS 文件分離 != CSS 作用域隔離

看下這樣的目錄結構:

├── src  │   ├──......                   # 公共組件目錄  │   ├── components              # 組件  │   │   └──comA                 # 組件A  │   │       ├──comA.js  │   │       ├──comA.css  │   │       └── index.js  │   │   └──comB                 # 組件B  │   │       ├──comB.js  │   │       ├──comB.css  │   │       └── index.js  │   ├── routes                  # 頁面模塊  │   │   └── modulesA            # 模塊A  │   │       ├──pageA.js         # pageA JS 代碼  │   │       ├──pageA.css        # pageA CSS 代碼  

看目錄結構清晰明了,由於「 CSS 文件分離 != CSS 作用域隔離」這樣的機制,如果我們不通過一些工具或規範來解決 CSS 的作用域污染問題,會產生非預期的頁面樣式渲染結果。

假設我們在組件 A 和組件 B import 引入 comA.css 和 comB.css。

comA.css

.title {      color: red;  }  

comB.css

.title {      font-size: 14px;  }  

最後打包出來的結果為:

.title {      color: red;  }  .title {      font-size: 14px;  }  

我們希望,comA.css 兩者互不影響,可以發現,雖然 A、B 兩個組件分別只引用了自己的 CSS 文件,但是 CSS 並沒有隔離,兩個 CSS 文件是相互影響的!

隨着 SPA 的流行,JS 可以組件化,按需加載(路由按需加載、組件的 CSS 和 JS 都按需加載),這種情況下 CSS 作用域污染的問題被放大,CSS 被按需加載後由於 CSS 全局污染的問題,在加載出其他一部分代碼後,可能導致現有的頁面上會出現詭異的樣式變動。這樣的問題加大了發佈的風險以及 debugger 的成本。

小編我從寫 Vue 到寫 React , Vue 的 scoped 完美的解決了 CSS 的作用域問題,那麼 React 如何解決 CSS 的作用域問題呢?

解決 React 的 CSS 作用域污染方案:

  • 方案一:namespaces
  • 方案二:CSS in JS
  • 方案三:CSS Modules

方案一:namespaces

「利用約定好的命名來隔離 CSS 的作用域

comA.css

.comA .title {      color: red;  }  .comA .……{      ……  }  

comB.css

.comB .title {      font-size: 14px;  }  .comB .……{      ……  }  

嗯,用 CSS 寫命名空間寫起來貌似有點累。

沒事我們有 CSS 預處理器,利用 less、sass、stylus 等預處理器,代碼依然簡潔。

A.less

.comA {      .title {          color: red;      }        .…… {          ……      }  }  

B.less

.comB {      .title {          font-size: 14px;      }        .…… {          ……      }  }  

貌似很完美解決了 CSS 的作用域問題,但是問題來了,假設 AB 組件是嵌套組件。

那麼最後的渲染 DOM 結構為:

<div class="comA">      <h1 class="title">組件A的title</h1>      <div class="comB">          <h1 class="title">組件組件的title</h1>      </div>  </div>  

comA 的樣式又成功作用在了組件 B 上。

沒關係,還有解,所有的 class 名以命名空間為前綴。

<div class="comA">      <h1 class="comA__title">組件A的title</h1>      <div class="comB">          <h1 class="comB__title">組件組件的title</h1>      </div>  </div>  

A.less

.comA {      &__title {          color: red;      }  }  

B.less

.comB {      &__title {          font-size: 14px;      }  }  

如果,我們的樣式還遵循 BEM (Block, Element, Modifier) 規範,那麼,樣式名簡直不要太長!但是問題確實也解決了,但約定畢竟是約定,靠約定和自覺來解決問題畢竟不是好方法,在多人維護的業務代碼中這種約定來解決 CSS 污染問題也變得很難。

方案二:CSS in JS

「使用 JS 語言寫 CSS,也是 React 官方有推薦的一種方式。

從 React 文檔進入

https://github.com/MicheleBertoli/css-in-js ,可以發現目前的 CSS in JS 的第三方庫有 60 余種。

看兩個比較大眾的庫:

  • reactCSS
  • styled-components

reactCSS

「支持 React 、Redux、React Native、autoprefixed、Hover、偽元素和媒體查詢(http://reactcss.com/)

看下官網文檔 :

const styles = reactCSS({    'default': {      card: {        background: '#fff',        boxShadow: '0 2px 4px rgba(0,0,0,.15)',      },    },    'zIndex-2': {      card: {        boxShadow: '0 4px 8px rgba(0,0,0,.15)',      },    },  }, {    'zIndex-2': props.zIndex === 2,  })  
class Component extends React.Component {    render() {      const styles = reactCSS({        'default': {          card: {            background: '#fff',            boxShadow: '0 2px 4px rgba(0,0,0,.15)',          },          title: {            fontSize: '2.8rem',            color: this.props.color,          },        },      })      return (        <div style={ styles.card }>          <div style={ styles.title }>            { this.props.title }          </div>          { this.props.children }        </div>      )    }  }  

可以看出,CSS 都轉化成了 JS 的寫法,雖然沒有學習成本,但是這種轉變還是有一絲不適。

styled-components

「styled-components,目前社區里最受歡迎的一款 CSS in JS 方案(https://www.styled-components.com/)

const Button = styled.a`    /* This renders the buttons above... Edit me! */    display: inline-block;    border-radius: 3px;    padding: 0.5rem 0;    margin: 0.5rem 1rem;    width: 11rem;    background: transparent;    color: white;    border: 2px solid white;    /* The GitHub button is a primary button     * edit this to target it specifically! */    ${props => props.primary && css`      background: white;      color: palevioletred;    `}  `  render(    <div>      <Button        href="https://github.com/styled-components/styled-components"        target="_blank"        rel="noopener"        primary      >        GitHub      </Button>      <Button as={Link} href="/docs" prefetch>        Documentation      </Button>    </div>  )  

與 reactCSS 不同,styled-components 使用了模板字符串,寫法更接近 CSS 的寫法。

方案三:CSS Modules

「利用 webpack 等構建工具使 class 作用域為局部。

CSS 依然是還是 CSS

例如 webpack,配置 css-loader 的 options modules: true。

module.exports = {    module: {      rules: [        {          test: /.css$/,          loader: 'css-loader',          options: {            modules: true,          },        },      ],    },  };  

modules 更具體的配置項參考:https://www.npmjs.com/package/css-loader

loader 會用唯一的標識符 (identifier) 來替換局部選擇器。所選擇的唯一標識符以模塊形式暴露出去。

示例:

webpack css-loader options

options: {    ...,    modules: {      mode: 'local',      // 樣式名規則配置      localIdentName: '[name]__[local]--[hash:base64:5]',    },  },  ...  

App.js

...  import styles from "./App.css";  ...  <div>    <header className={styles["header__wrapper"]}>      <h1 className={styles["title"]}>標題</h1>      <div className={styles["sub-title"]}>描述</div>    </header>  </div>  

App.css

.header__wrapper {    text-align: center;  }    .title {    color: gray;    font-size: 34px;    font-weight: bold;  }    .sub-title {    color: green;    font-size: 16px;  }  

編譯後端的 CSS,classname 增加了 hash 值。

.App__header__wrapper--TW7BP {    text-align: center;  }    .App__title--2qYnk {    color: gray;    font-size: 34px;    font-weight: bold;  }    .App__sub-title--3k88A {    color: green;    font-size: 16px;  }  

總結

(1)如果是 ui 組件庫中使用

「建議使用 namespaces 方案

原因:

  • ui 組件庫維護人員基本固定,遵守約定的規範較為容易,可通過約定規範來解決不同組件 CSS 相互影響問題
  • 由於 ui 組件庫會應用於整個公司的產品,在真正的業務場景中,雖然不建議,但是可能無法避免需要覆蓋組件樣式的特殊場景,如使用其他兩種方式,不能支持組件樣式覆蓋

(2)如果是業務代碼/業務組件中使用

「CSS in JS / CSS Modules

業務代碼維護人員較多且不固定、代碼水平不一致,只通過規範來約束不靠譜,無法保證開發人員嚴格遵守規範,不能根治 CSS 交叉影響問題,但是從 debug 角度考慮,建議組件外層都添加一個 namespaces 方面定位組件。然後加之 CSS in JS 或 CSS Modules 方案來解決 CSS 交叉影響問題。

CSS in JS 和 CSS Modules 誰優誰勝?

CSS Modules 會比 CSS in JS 的侵入性更小,CSS in JS 可以和 JS 共享變量,但個人更喜歡 CSS Modules ,但是誰優誰勝無法武斷。

  • 如果你的團隊還沒有使用這任一技術,需要考慮的是團隊成員的感受
  • 如果已經在使用其中某一種方案,保持一致性即可,相信並這樣走下去