本文首发于政采云前端团队博客:如何在 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、伪元素和媒体查询
看下官网文档 :
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 方案
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 ,但是谁优谁胜无法武断。
- 如果你的团队还没有使用这任一技术,需要考虑的是团队成员的感受
- 如果已经在使用其中某一种方案,保持一致性即可,相信并这样走下去
招贤纳士
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。