《React最佳实践系列》之css篇

http://img1.juimg.com/180124/330595-1P124141G060.jpg

React中的css到底该怎么写?这一直是个饱受争论的话题。本文将结合React CSS的发展史,分别叙述CSS in js与CSS module两种风格中的最佳实践。

行内样式

首先,facebook提出CSS in js的概念,这很奇怪。
我们多年来所学的知识都在宣扬关注点分离的重要性,不应该将标记和css混在一起。但是React行内样式的提出,试图改变关注点分离这一概念。使其从技术分离向组件分离转变。

通过CSS in js有以下两个优点:

  1. React将组件作为应用架构的基础单元,通过组合组件来创建应用。
  2. 可以很方便的和逻辑进行交互。

比如这样(先忽略直接将style对象传入带来的性能问题)

1
2
3
4
5
render(){
return (
<div style={{fontWeight: this.props.weight ? 'bold' : 'normal'}}>hello world</div>
)
}

行内样式也有缺点:

  1. 不支持伪选择器和伪元素
  2. 不支持媒体查询
  3. 不支持样式回退,因为js对象不支持两个同名属性
  4. 不支持动画
  5. 需要覆盖常规样式的时候,可能会需要!important来实现了
  6. 调试不方便
  7. 很关键的一点,如果是在服务端渲染的话,会使得页面体积变得很大

事实证明,虽然行内样式解决了目标问题,却引发了更多的问题。

CSS Module

如果你认为行内样式的方案不适合自己的团队,但仍然希望尽量紧密结合样式与组件,那么webpackcss-loader就帮了你大忙!
关于webpack和详细配置,本文不再赘述。我们看一下本文主要用到的loaderplugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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文件

1
2
3
4
5
.button {
background-color: blue;
color: #fff;
padding: 10px 20px;
}

然后在js文件中引入

1
import styles from './index.css'

import语句会导入一个样式对象,其所有属性就是index.css中定义的类,这时候运行一下console,开发者工具会输出一个形如这样的对象。这个key是根据文件散列值和其他一些参数生成的。

1
2
3
{
button: "_2wpxM3yizfwbWee6k0U1D4"
}

接着,我们可以用这个对象去设置按钮的className属性

1
2
3
const Button = () => (
<button className={styles.button}>Click it</button>
)

这样,按钮的样式就设置完成了。
我们打开开发者工具可以看到,button的类名就是上面的_2wpxM3yizfwbWee6k0U1D4。如果查看页面头部,我们还会发现相同的类名已经注入到页面中了。

css-loader允许你在js模块中导入css文件,并且启用modules标记时,所有类名都只作用于导入它们的模块。最后,style-loader接收css-loader转换的结果,并将样式注入到页面头部。

这种用法非常强大,因为我们拥有了css的完整能力及表现性,又结合了局部作用域类名与显示依赖的优点。我们还可以像这样配置localIdentName参数,解决调试时信息不清晰的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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模块不要为当前选择器加上局部作用域。

例如:

1
2
3
:global .button {
...
}

这样,你可以应用不需要局部作用域的样式。比如第三方组件。

第二个就是composes,有了它,就可以从同个文件或外部依赖中引用类名,将其它类的所有样式应用于一个元素。

例如:

1
2
3
4
5
6
7
8
9
10
.text-red{
color: red;
}

.button {
composes: text-red;
background-color: blue;
color: #fff;
padding: 10px 20px;
}

最终,button类的所有规则以及composes声明的所有规则都能作用于元素。

这个特性非常强大,而且原理很巧妙。你可能以为它和SASS@extend方法一样,只是将组合类复制到引用它们的位置,其实不是这样。简单来讲,所有组合类名都是逐个应用到DOM中的组件上。

以我们的示例来说,代码如下:

1
<button class="_2wpxM3yizfwbWee6k0U1D4 Sf8w9cFdQXdRV_i9dgcOq">Click it</button>

注入页面的css如下所示:

1
2
3
4
5
6
7
8
9
.Sf8w9cFdQXdRV_i9dgcOq {
color: red;
}

._2wpxM3yizfwbWee6k0U1D4 {
background-color: blue;
color: #fff;
padding: 10px 20px;
}

原子级CSS

原子级CSS又称函数式CSS,是CSS的一种使用方式,即每个类只有一条规则。

例如,可以用一个类来设置底部外边距为0:

1
2
3
.mb0 {
margin-bottom: 0;
}

然后用另一个类设置font-weight为bold

1
2
3
.fwb {
font-weight: bold;
}

然后将这些原子类用在元素上:

1
<span class="mb0 fwb">Hello World</span>

这种技巧存在争议,但很高效。一方面,类是在CSS文件中定义,却在视图层组合,每次修改元素的样式都要同时修改两个地方;另一方面,它可以超快地搭建页面。

其实,只要所有基本规则都定好,将这些类应用于元素或者用它们生成新的样式都非常快,这是一大优点。其次,使用原子级CSS可以控制css文件的大小,因为创建新组建时可以复用已有类的样式,不需要编写新样式,这对性能很有好处。

原子级CSS Module

我们可以将CSS Module与原子级CSS结合起来使用。

查看以下示例:

1
2
3
4
5
.title {
composes: mb0 fwb;
}

<span class="title">Hello World</span>

这种做法非常好,因为样式逻辑仍然保留在CSS中,同时CSS模块利用composes将所有单个类聚合到一个类中。

上述代码的渲染结果如下所示:

1
<span class="title--3JCJR mb0--21SyP fwb--1JRhZ">Hello World</span>

此处的titlemb0以及fwb都是自动加到元素上的。并且它们都只作用于局部。这样,我们就用上了CSS Module的所有优势。

styled-components

最后,让我们看一下CSS in js中的王者——styled-components。
这个库可以说考虑到了其他组件样式库遇到的所有问题。

在安装styled-components之后,我们可以这样使用它:

1
2
3
4
5
6
7
import styled from 'styled-components'

const Button = styled.button`
background-color: blue;
color: #fff;
padding: 10px 20px;
`

可以看到它使用了模板字符串,这意味着它可以用js的全部能力为元素添加样式。
这种看似奇怪的语法会返回普通的React组件Button,它渲染了一个按钮元素,并加上了模板中定义的样式。先创建唯一的类名,再将它加到元素上,最后向页面文档头部注入相应的样式。至此,样式生效了。

渲染的组件如下所示:

1
<button class="kYvFOg">Click it</button>

页面上添加的样式如下:

1
2
3
4
5
.kYvFOg {
background-color: blue;
color: #fff;
padding: 10px 20px;
}

这个库的优点如下在于支持几乎所有的css特性。比如,它支持SASS风格的伪类语法:

1
2
3
4
5
6
7
8
9
const Button = styled.button`
background-color: blue;
color: #fff;
padding: 10px 20px;
&:hover {
background-color: #fff;
color: blue;
}
`

它也支持媒体查询:

1
2
3
4
5
6
7
8
9
10
11
12
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设计模式与最佳实践》第七章
坚持原创技术分享,您的支持将鼓励我继续创作!