27. 精读《css-in-js 杀鸡用牛刀》

本期精读的文章是:css-in-js 杀鸡用牛刀

1 引言

继 精读《请停止 css-in-js 的行为》 这篇文章之后,我们又读了一篇抵制 css-in-js 的文章,虽然大部分观点都有道理,但部分存在可商榷之处,让我们分析一下这篇文章,了解 css 还做了哪些努力,以及 css-in-js 会如何发展。

2 内容概要

2.1 结构/行为 vs 样式

作者认为,模块化 jsx 让 html 结构与行为耦合在一起是很有价值的,然而样式却不应该与模块耦合起来,因为样式是一种全局行为。许多时候需要对网站进行全局的设计,将样式分散到模块中会导致更多的理解成本。

2.2 松耦合与紧耦合

将样式与模块松耦合,系统会获得更大的自由度与拓展性。如果样式与结构松耦合,一套看似相似的的元素,可能拥有完全不同的底层结构。然而交互必须与结构紧耦合,因为交互依赖于结构。

2.3 视觉一致性问题

局部样式会阻碍视觉一致性,只有全局化样式才能保证视觉一致性。

2.4 代码复用问题

如果每个组件维护自己的样式,那么会存在许多样式代码复制粘贴的问题,复制粘贴的代码可维护性极低。

3 精读

无论是 css-in-js 还是 css 预编译的尝试,各自都具有强大优点,本文对 css-in-js 提出的质疑我认为是欠妥当的,下面谈谈 css-in-js 如何解决作者提出的问题,以及简单介绍 OOCSS, SMACSS, BEM, ITCSS, 和 ECSS 的思路。

3.1 css-in-js 依然具备视觉一致性

文中提出,网站样式要从全局考虑,模块化样式行为的优点是解决了样式冲突问题,但因此也削弱了对全局样式的把控。

开发单个组件的样式分为两种情况,分别是明确风格的组件与样式独立的组件,在样式独立组件中,由于不确定会被哪些主题的网站所引用,因此无论是全局 css 还是局部 css,都无法控制样式。在明确风格的情况下,可以先把此风格的基色确定下来,无论是抽成 sass 变量还是 js 变量,都具有可复用性。

全局 css 的开发,适合自上而下控制,组件通过定义 class 而不需要关心具体样式,通过全局 class 统一调控整体风格。而 css-in-js 是自下而上的,但需要预先抽出整体风格的样式模块,其效果与全局 css 是等价的。

全局 css 控制风格:

代码语言:javascript
复制
<style>
	.container{}
	.list-item{}
	.submit-button{}
</style>

<div className="container">
<div className="list-item"></div>
<div className="list-item"></div>
<div className="submit-button"></div>
</div>

css-in-js 风格:

代码语言:javascript
复制
const CommonContainer = styled.div const CommonListItem = styled.div
const CommonSubmitButton = css``

export const Container = styled(CommonContainer) export const ListItem = styled(CommonListItem)
export const CommonSubmitButton = styled.div${CommonSubmitButton}

而 css-in-js 运行时的样式解析,让我们更轻易的切换主题。比如我们抽出一个公共样式包,业务代码中的色值都从此样式包中引用,那么在不同的环境下,公共样式包可能通过所在宿主环境的判断,返回给业务代码不同的色值,甚至与宿主环境配合,从宿主环境拿到注入的颜色,实现一套代码在运行时轻松换肤。

3.2 css-in-js 仍具备代码复用性

文中观点提出,css-in-js 这种局部样式行为,会导致公共样式、方法难以复用,导致各个模块参杂着大量重复代码。因为 sass 通过定义全局变量、mixins 方法让样式更具有复用性。

我觉得这是一种误解,在 css-in-js 模式中,通过全局合理的设计,使用 js 文件存放颜色变量、公共方法、可能会复用的 css 代码块,其复用能力远大于 sass。

3.3 OOCSS

OOCSS 成为 css 的面向对象加强版,每个 class 只处理一件事:

代码语言:javascript
复制
.size {width: 25%;}
.bgBlue {background:blue}
.g-bd2{margin:0 0 10px;}

网易 NEC 就大量使用了这种思想。

这样的好处在于避免了 class 之间的冗余,让我们更容易创建可复用的 class,也不会在命名上纠结。

然而,先不说 oocss 带来的巨大零散 class 导致的维护成本,以及修改 class 导致的巨大风险,class 的本意是语义化,如果让 class 使用一堆对象描述堆砌,我们将很难定位一个元素,也很难描述这个元素的含义。

3.4 SMACSS

为css分类

SMACSS 认为 css 有 5 个类别:

  1. Base 基础样式
  2. Layout 布局样式
  3. Module 模块样式
  4. State 状态样式
  5. Theme 主题样式

我们通过这 5 种类别来拼凑出完整的 class,我感觉就是对 OOCSS 的进一步规范和约束。

命名规则

对这 5 种类别,在命名时要加上对应前缀,分别是:

  1. Base 属于基础元素,比如 div p,不需要命名
  2. Layout 使用 .l-.layout-前缀
  3. Module 使用模块名命名,比如文章区块就叫 .article
  4. State 使用 .is- 前缀,比如 .is-show
  5. Theme 使用 .theme- 前缀

我觉得这样在语义化的基础上,拆分了状态、主题、布局,着实增强了 css 可读性。

最小化适配深度

尽可能减少适配层级,虽然增加适配层级会减少冲突发生率,但是会增加额外的阅读负担,以及一些 bug(旧版 ie 层级超过 255 导致样式失效)。

像 css-modules 这种解决方案恰恰反其道而行之,通过层级避免冲突,通过预编译解决阅读负担,然而在没有预编译的情况下,最小化适配深度原则依然是最有效的。

3.5 BEM

BEM 规范更像是 SMACSS 分类的加强版,通过 __element 表述后代,--modifier 表述状态,比如:

代码语言:javascript
复制
.article {}
.article__label {} /* label 元素 /
.article__label--selected {} /
label 元素处于被选中状态 */

3.6 ITCSS

类似 SMACSS 对 css 元素进行了分层:

  • Settings – 与预处理器一起使用,包含颜色、字体等定义
  • Tools – 工具与方法,比如 mixins,Settings 与 Tools 都不会产生任何 css 代码,仅仅是辅助函数与变量
  • Generic – 通用层,比如 reset htmlbody 的样式
  • Elements – 对通用元素的样式重置,比如  a p div 等元素的样式重置
  • Objects – 类似 OOCSS 中的对象,描述一些常用的基础状态
  • Components – 对组件样式的定义,一个 UI 元素基本由 Objects 与 Components 组成
  • Utilities – 工具类,比如 .hidden

ITCSS 的分层是非常有借鉴意义的,即便在 css-in-js 设计中,也可以参考此模式定义结构。

3.7 ECSS

ECSS 的规范是这样的:.nsp-Component_ChildNode-variant

  • nsp 一个尽量简短的命名空间
  • Component 文件名
  • ChildNode 子元素名
  • variant 额外内容

例子:

代码语言:javascript
复制
<div class="tl-MediaObject">
<a href="#" class="tl-MediaObject_Link">
<img class="tl-MediaObject_Media" src="mini.jpg" alt="User">
</a>
<div class="tl-MediaObject_Attribution">@BF 14 minutes ago</div>
</div>

更多细节可以看此 PPT

4 总结

虽然我认为这篇文章提出的 css-in-js 缺点大部分存在漏洞,但它警示了我们,css 设计的初衷是全局化控制样式,即便产生了样式冲突、混乱的问题,但我们仍要记住,在模块化开发的今天,仍要保持网站风格的整体性,即便使用了 css-in-js 的开发方式。

虽然作者呼吁我们不要只顾着 css-in-js,要放眼看看 OOCSS, SMACSS, BEM, ITCSS, 和 ECSS 等基于原生 css 的解决方案,但我觉得把这些思想运用到 css-in-js 是个不错的选择 :p

讨论地址是:精读《css-in-js 杀鸡用牛刀》 · Issue #38 · dt-fe/weekly