关于 React 为什么使用 JS Object 代替 CSS 来创建样式

开始讨论

晚上 FE 群里,设计师 @l 突然提出了一个问题:为什么 React Native 使用 JS 来做样式,而不是使用 CSS?
因为 React.js 开发 Web 的时候,是可以使用 className 的,我们可以使用 CSS 的类来为各个组件、模块定义样式。但 React Native 只能使用 JS Object,我发现自己好像没有完全注意过这个问题(可能是因为没写过 RN)..

决定看看到底是为什么。Google 了一把之后找到了这个页面,里面说明了一些原因。最先映入眼帘的就是 “React Native doesn’t implement CSS…”。Cool,看来 React 在浏览器中的 CSS Parsing 过程没有办法直接搬到移动端上,所以 RN 只保留了 React 的 JS Object Style 特性。

下面是一个 slides,是 Facebook 的 frontend infrastructure 工程师 Christoper Chedeau (他的 id 叫 vjeux) 对这个有争议决定的解释,到底是因为为什么 FB 决定在 React 上使用 CSS in JS 形式的 Style。

在规模性使用 CSS 的时候,主要会遇到以下 7 大问题:

以开发一个 button 的样式举例,.button,和被按下后的状态 .button-depressed

1. 全局命名空间

我们开发了两个 CSS 类,会产生两个全局的 CSS 变量(就是全局的两个类),我们在 JS 中已经了解到全局变量的坏处(如可能被覆盖值、被重定义),但在 CSS 的大陆上我们依旧在使用他们。

在 Facebook,工程师们使用了一种叫 cx(CSS Extension)的工具,为 CSS 提供命名空间,使用如下

/* button.css */
.button/container {
  padding: 5px;
}

相应的,在 jsx/js 中

/* button.js */
<div className={cx("button/container")}>

2. CSS 文件之间可能存在依赖

长久以来。。我们一直告诫开发者每次要用什么样式,就要通过 requireCSS 将 CSS 文件引入。不过一个问题来了,有的时候有其他的文件已经 require 了一些 CSS 但你却忘了,不过依旧能正常运行(这其实很不安全不是么?!万一哪天 break down 了很难找问题)。

cx 可以支持一些静态分析,帮助你避免这类问题,所以 issue 2,我们也算解决了。

3. 消除 Dead Code

如果你使用了 cx,那么 css 和 className 中使用的类名都一致了!如果什么时候 className 不再使用了,同时干掉 css 里的就好了。(vjeux 在这里的解释还是关于 cx 的,感觉这个似乎没什么重要,还是手动删除)

4. 压缩

cx 会对类名替换,酷,这样你连 CSS 的名字都猜不出来了

5. CSS 与 JS 共享变量

这个问题非常有趣,而且也很常见。是 CSS 之间共享变量吗?我们说的并不是 less、sass 那种变量的定义,而是 JS 和 CSS 之间 share 变量。
vjeux 的栗子并不好吃,我举个栗子吧:
假设你需要使用 JS 动态创建一些图形,如圆点(为什么是圆点?…好吧其实随便是什么都可以),然后你要计算点击区域的左右上下边界,怎么办呢?当然要知道他的半径或者宽高啊。有人说可以使用 JS 获取宽高,如果我设置的是 borderWidth 之类的呢,或者还有 padding?你打算全部获取到然后计算一把吗?呵呵当然不是。最简单的当然是直接写一个常量来代表半径、宽高这类值了。
然后突然有一天 designer 找到你,我们的小圆点好丑啊,我希望把它改成大圆盘,半径从 0.5px 变成 50px。这时候你要改的就不仅仅是 CSS 了,还有一堆隐藏在 JS 中的常量值。你要去 grep 他们的名字吗?

Facebook 又想到了一个方法,叫 “CSS Variable”,通过一个 php 文件把 CSS 里的常量导出为一个 array,为 JS 暴露一个叫 cssVar 的函数来调用。

.button/container {
  padding: var(button-padding);
}
/* CSSVar.php */
$CSSVar = array(
  'button-padding' => 5
);
var buttonPadding = cssVar('button-padding');

酷,可以让 CSS 和 JS 共享变量了

以上的 5 个问题,其实我们(FB)现有的工具都可以 handle,那么下面两个问题,就不那么好解决了。

6. 非确定性的样式

在一个 CSS 文件中,如果你对某一个样式(一个选择器的样式)定义了多次,属性有重复,后者就会覆盖前者 (the last one in the file wins)。

.small { font-size: 12px; }
html { font-size: 12px; }
.small { font-size: 22px; }

以上,最后 .small 是 22px,很简单,因为它在后面…这个方法也常用于自定义样式框架,比如我们基于 bootstrap 做一些修改、定制,用自己的文件 concat 在最后,借以覆盖 bs 某些选择器原有的样式。

但是这还有个问题,不是所有情况下,CSS 文件都只有一个的。如果某一个样式写在不同的文件中,而这些样式文件是异步加载的(因为这样可以快起来啊),浏览器接收到同一选择器不同样式的顺序就不一定了,那用户到底应该看到什么样子?谁来决定呢?

我们先放着这个问题不管,来看下一个

7. 隔离

假设我们的 designer 非常赞,设计了一套基础的 UI 库,可以给大家用,可能包含 dropdown、menu 等,花了很大精力设计了一套可以兼容各种场景的 API。不过当设计和开发希望加入一些新特性的时候,我们可能需要跟各个 team 的维护者聊一聊,看看到底怎样做比较合适,能适应他们目前的代码设计。(天呐真是太麻烦了)

酷,最后终于搞定了!开发者写了一些奇怪的 trick 的语法来兼容我们各个团队的需求,然后奇妙的事情发生了,页面全挂了~!…当然这可能是比较糟糕的预期,情况是我们可能很容易破坏各个选择器直接的隔离性,因为新的 CSS Selector 可能影响组件内的许多东西

<Menu className={cx("menu")}>
  <MenuList order="1" />
  <MenuList order="2" />
  <ButtonGroups />
</Menu>
.menu > a {
  /* blablabla... */
}

当我们改动了 menu 这个样式,谁知道会发生什么呢?<a> 最终影响的到底是 list 呢还是 button group 呢?如果我们的 dom 结构再变化,还会影响什么呢?要靠猜的吧!哈哈

CSS in JS

我们解决了 7 个问题中的 5 个,6 和 7 则难得不行。继续引入 CSS,反而可能会让这些问题更复杂。我们不可能再所有地方,大规模、小规模都完美使用 CSS,毕竟不是每个写 React 都是专业的前端工程师不是吗?

来疯狂一把,为什么不试试 JS 呢?!下面是一段 inline style 的 React 样式。(等等你说 inline style?那不是 Web 开发中不推荐的方法吗?)

var styles = {
  container: {
    padding: 2,
    backgroundColor: '#eee',
  },
  depressed: {
    borderRadius: 2,
  }
}

<div style={styles.container}></div>

我就写短一点,你们感受一下就好了。除了结构化的 JS 对象,以及 CSS 属性变成了驼峰命名,px 值变成了数字,inline style 好像没那么糟糕了,不是吗?因为这里的 inline,并不是变成了 style="a:1; b:2; c:3;" 这样难以辨认的字符串,而是让你用 js 的对象来定义,只在 React 组件的 style 属性那里传入一下样式的引用。JS 对象,就好像映射了一个个 CSS 的 class,而 inline style refer,就是一个隔离的好办法。

当引入 JS 后,玩法就更多了。我们甚至可以加入逻辑,比如函数、条件等等

propTypes: {
  isDpressed: React.PropTypes.bool
}

<div style={Object.assign(
  styles.container,
  this.props.isDpressed && styles.depressed
)}>
</div>

用以上的代码,我们已经完成了一个样式根据状态的变化:当变成 isDepressed (被按下) 的状态,就应用 depressed 的样式。

尾声

我觉得综上所述,你可以理解为,FB 发现 CSS 不好解决的问题可以通过 JS 来解决,所以用了 JS。而不是强行必须照搬 Web 开发原有的一套。React 在 Web 端其实还是可以使用 CSS 的,用 className 的话。但 React Native 就不行了,但有了这样的样式设计,无论是为了解决问题,还是减少 RN 实现复杂度,都有极大的帮助,所以 RN 最后的官方 doc 说的是压根没实现 CSS。(我并不知道他们以后打不打算实现 CSS)

如果你有兴趣追根溯源,请继续看

下面这篇是 vjeux 发表在 speakerdeck 上的 slide,解释了 React 使用 JavaScript 代替 CSS 来创建样式的初衷。