如何在 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 ,但是誰優誰勝無法武斷。
- 如果你的團隊還沒有使用這任一技術,需要考慮的是團隊成員的感受
- 如果已經在使用其中某一種方案,保持一致性即可,相信並這樣走下去