React中的css到底该怎么写?这一直是个饱受争论的话题。本文将结合React CSS的发展史,分别叙述CSS in js与CSS module两种风格中的最佳实践。
行内样式
首先,facebook提出CSS in js的概念,这很奇怪。
我们多年来所学的知识都在宣扬关注点分离的重要性,不应该将标记和css混在一起。但是React行内样式的提出,试图改变关注点分离这一概念。使其从技术分离向组件分离转变。
通过CSS in js,有以下两个优点:
- React将组件作为应用架构的基础单元,通过组合组件来创建应用。
- 可以很方便的和逻辑进行交互。
比如这样(先忽略直接将style对象传入带来的性能问题)
render(){
return (
<div style={{fontWeight: this.props.weight ? 'bold' : 'normal'}}>hello world</div>
)
}
行内样式也有缺点:
- 不支持伪选择器和伪元素
- 不支持媒体查询
- 不支持样式回退,因为js对象不支持两个同名属性
- 不支持动画
- 需要覆盖常规样式的时候,可能会需要
!important来实现了 - 调试不方便
- 很关键的一点,如果是在服务端渲染的话,会使得页面体积变得很大
事实证明,虽然行内样式解决了目标问题,却引发了更多的问题。
CSS Module
如果你认为行内样式的方案不适合自己的团队,但仍然希望尽量紧密结合样式与组件,那么webpack的css-loader就帮了你大忙!
关于webpack和详细配置,本文不再赘述。我们看一下本文主要用到的loader和plugin:
const HTMLWebpackPlugin = require('html-webpack-plugin')
module.exports = {
...
module: {
rules: [
...
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: true
}
}
]
}
]
},
plugins: [
new HTMLWebpackPlugin()
]
}
如上配置完以后,在终端启动项目后,就可以在网页中访问了。接下来我们开始编写具体代码。
首先定义一个普通的css文件
.button {
background-color: blue;
color: #fff;
padding: 10px 20px;
}
然后在js文件中引入
import styles from './index.css'
import语句会导入一个样式对象,其所有属性就是index.css中定义的类,这时候运行一下console,开发者工具会输出一个形如这样的对象。这个key是根据文件散列值和其他一些参数生成的。
{
button: "_2wpxM3yizfwbWee6k0U1D4"
}
接着,我们可以用这个对象去设置按钮的className属性
const Button = () => (
<button className={styles.button}>Click it</button>
)
这样,按钮的样式就设置完成了。
我们打开开发者工具可以看到,button的类名就是上面的_2wpxM3yizfwbWee6k0U1D4。如果查看页面头部,我们还会发现相同的类名已经注入到页面中了。
css-loader允许你在js模块中导入css文件,并且启用modules标记时,所有类名都只作用于导入它们的模块。最后,style-loader接收css-loader转换的结果,并将样式注入到页面头部。
这种用法非常强大,因为我们拥有了css的完整能力及表现性,又结合了局部作用域类名与显示依赖的优点。我们还可以像这样配置localIdentName参数,解决调试时信息不清晰的问题。
module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
}
]
}
]
}
生产环境不需要这样的类名,更注重性能,因此我们想要更简短的类名和散列值。
我们可以在生产环境下使用MiniCssExtractPlugin将样式提取到单独的css文件,并将其放入CDN,从而获得更好的性能。
css-loader还支持一些关键词。
第一个就是global关键词。给任何类添加:global前缀,意味着请求CSS模块不要为当前选择器加上局部作用域。
例如:
:global .button {
...
}
这样,你可以应用不需要局部作用域的样式。比如第三方组件。
第二个就是composes,有了它,就可以从同个文件或外部依赖中引用类名,将其它类的所有样式应用于一个元素。
例如:
.text-red{
color: red;
}
.button {
composes: text-red;
background-color: blue;
color: #fff;
padding: 10px 20px;
}
最终,button类的所有规则以及composes声明的所有规则都能作用于元素。
这个特性非常强大,而且原理很巧妙。你可能以为它和SASS的@extend方法一样,只是将组合类复制到引用它们的位置,其实不是这样。简单来讲,所有组合类名都是逐个应用到DOM中的组件上。
以我们的示例来说,代码如下:
<button class="_2wpxM3yizfwbWee6k0U1D4 Sf8w9cFdQXdRV_i9dgcOq">Click it</button>
注入页面的css如下所示:
.Sf8w9cFdQXdRV_i9dgcOq {
color: red;
}
._2wpxM3yizfwbWee6k0U1D4 {
background-color: blue;
color: #fff;
padding: 10px 20px;
}
原子级CSS
原子级CSS又称函数式CSS,是CSS的一种使用方式,即每个类只有一条规则。
例如,可以用一个类来设置底部外边距为0:
.mb0 {
margin-bottom: 0;
}
然后用另一个类设置font-weight为bold
.fwb {
font-weight: bold;
}
然后将这些原子类用在元素上:
<span class="mb0 fwb">Hello World</span>
这种技巧存在争议,但很高效。一方面,类是在CSS文件中定义,却在视图层组合,每次修改元素的样式都要同时修改两个地方;另一方面,它可以超快地搭建页面。
其实,只要所有基本规则都定好,将这些类应用于元素或者用它们生成新的样式都非常快,这是一大优点。其次,使用原子级CSS可以控制css文件的大小,因为创建新组建时可以复用已有类的样式,不需要编写新样式,这对性能很有好处。
原子级CSS Module
我们可以将CSS Module与原子级CSS结合起来使用。
查看以下示例:
.title {
composes: mb0 fwb;
}
<span class="title">Hello World</span>
这种做法非常好,因为样式逻辑仍然保留在CSS中,同时CSS模块利用composes将所有单个类聚合到一个类中。
上述代码的渲染结果如下所示:
<span class="title--3JCJR mb0--21SyP fwb--1JRhZ">Hello World</span>
此处的title、mb0以及fwb都是自动加到元素上的。并且它们都只作用于局部。这样,我们就用上了CSS Module的所有优势。
styled-components
最后,让我们看一下CSS in js中的王者——styled-components。 这个库可以说考虑到了其他组件样式库遇到的所有问题。
在安装styled-components之后,我们可以这样使用它:
import styled from 'styled-components'
const Button = styled.button`
background-color: blue;
color: #fff;
padding: 10px 20px;
`
可以看到它使用了模板字符串,这意味着它可以用js的全部能力为元素添加样式。 这种看似奇怪的语法会返回普通的React组件Button,它渲染了一个按钮元素,并加上了模板中定义的样式。先创建唯一的类名,再将它加到元素上,最后向页面文档头部注入相应的样式。至此,样式生效了。
渲染的组件如下所示:
<button class="kYvFOg">Click it</button>
页面上添加的样式如下:
.kYvFOg {
background-color: blue;
color: #fff;
padding: 10px 20px;
}
这个库的优点如下在于支持几乎所有的css特性。比如,它支持SASS风格的伪类语法:
const Button = styled.button`
background-color: blue;
color: #fff;
padding: 10px 20px;
&:hover {
background-color: #fff;
color: blue;
}
`
它也支持媒体查询:
const Button = styled.button`
background-color: blue;
color: #fff;
padding: 10px 20px;
&:hover {
background-color: #fff;
color: blue;
}
@media (max-width: 480px) {
padding: 10px 10px;
}
`
它还可以很方便地覆盖样式,并设置不同属性来多次复用该组件。当你使用了形如antd之类的UI库,并希望自定义一些样式时,这个优点会非常明显。
结尾
CSS in or not in js ? this is a question.
我觉得没有绝对的最佳实践,只有在工程迭代的过程中,不停的找寻最适合工程、最适合团队的方案,这才是最明智的选择。
参考文献:
- 《React设计模式与最佳实践》第七章