如何在 React 中優(yōu)雅的寫 CSS
本文首發(fā)于政采云前端團隊博客:如何在 React 中優(yōu)雅的寫 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 作用域隔離”這樣的機制,如果我們不通過一些工具或規(guī)范來解決 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 兩者互不影響,可以發(fā)現,雖然 A、B 兩個組件分別只引用了自己的 CSS 文件,但是 CSS 并沒有隔離,兩個 CSS 文件是相互影響的!
隨著 SPA 的流行,JS 可以組件化,按需加載(路由按需加載、組件的 CSS 和 JS 都按需加載),這種情況下 CSS 作用域污染的問題被放大,CSS 被按需加載后由于 CSS 全局污染的問題,在加載出其他一部分代碼后,可能導致現有的頁面上會出現詭異的樣式變動。這樣的問題加大了發(fā)布的風險以及 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的titleh1>
<div class="comB">
<h1 class="title">組件組件的titleh1>
div>
div>
comA 的樣式又成功作用在了組件 B 上。
沒關系,還有解,所有的 class 名以命名空間為前綴。
<div class="comA">
<h1 class="comA__title">組件A的titleh1>
<div class="comB">
<h1 class="comB__title">組件組件的titleh1>
div>
div>
A.less
.comA {
&__title {
color: red;
}
}
B.less
.comB {
&__title {
font-size: 14px;
}
}
如果,我們的樣式還遵循 BEM (Block, Element, Modifier) 規(guī)范,那么,樣式名簡直不要太長!但是問題確實也解決了,但約定畢竟是約定,靠約定和自覺來解決問題畢竟不是好方法,在多人維護的業(yè)務代碼中這種約定來解決 CSS ?污染問題也變得很難。
方案二:CSS in JS
“使用 JS 語言寫 CSS,也是 React 官方有推薦的一種方式。
從 React 文檔進入
https://github.com/MicheleBertoli/css-in-js ,可以發(fā)現目前的 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,目前社區(qū)里最受歡迎的一款 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',
// 樣式名規(guī)則配置
localIdentName: '[name]__[local]--[hash:base64:5]',
},
},
...
App.js
...
import styles from "./App.css";
...
<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 組件庫維護人員基本固定,遵守約定的規(guī)范較為容易,可通過約定規(guī)范來解決不同組件 CSS 相互影響問題
- 由于 ui 組件庫會應用于整個公司的產品,在真正的業(yè)務場景中,雖然不建議,但是可能無法避免需要覆蓋組件樣式的特殊場景,如使用其他兩種方式,不能支持組件樣式覆蓋
(2)如果是業(yè)務代碼/業(yè)務組件中使用
“CSS in JS ?/ CSS Modules
業(yè)務代碼維護人員較多且不固定、代碼水平不一致,只通過規(guī)范來約束不靠譜,無法保證開發(fā)人員嚴格遵守規(guī)范,不能根治 CSS 交叉影響問題,但是從 debug 角度考慮,建議組件外層都添加一個 namespaces 方面定位組件。然后加之 CSS in JS 或 CSS Modules 方案來解決 CSS 交叉影響問題。
CSS in JS 和 CSS Modules 誰優(yōu)誰勝?
CSS Modules 會比 CSS in JS 的侵入性更小,CSS in JS 可以和 JS 共享變量,但個人更喜歡 CSS Modules ,但是誰優(yōu)誰勝無法武斷。
- 如果你的團隊還沒有使用這任一技術,需要考慮的是團隊成員的感受
- 如果已經在使用其中某一種方案,保持一致性即可,相信并這樣走下去
推薦閱讀
我的公眾號能帶來什么價值?(文末有送書規(guī)則,一定要看)
每個前端工程師都應該了解的圖片知識(長文建議收藏)
為什么現在面試總是面試造火箭?「一個有溫度的前端號」
長按識別二維碼關注

點贊分享是對作者最大的支持!
