为什么我不推荐你使用“结构化样式”

在 Web 开发中,结构、样式、逻辑代码的组织关系是一个老生常谈的问题。当我们普遍认为“组件化”是解决工程复杂度的最佳实践时,很多人会将结构-样式-逻辑的内聚和耦合混淆起来。以样式为例,一种常见的情况是这样的:

.list {
  /* ... */
}
.list .list-item {
  /* ... */
}

当然这只是一个使用基本 CSS 的例子。如果你使用 Less/SCSS/Stylus,可以构造出更加“结构化”的代码,但它们本质上并没有不同。

今天想要和大家一起讨论的就是,为什么我觉得这是一种不好的设计,以及在当下,如何解决复杂样式的抽象问题。

白马首先是马,还是白色的东西?

这里请让我举一个很常见的例子。为了更好地表达现代工程中可能出现的问题,下面我会使用 SCSS 作为示例。

假设我们的组件是一个卡片,卡片中有一个徽标。我们很容易写出这样的实现:

.card {
  display: flex;
}
.badge {
  appearance: none;
}

现在我们需要实现一个特性:当卡片处于禁用状态时,徽标的透明度将会提高。接下来,我们有两种选择:

第一种是:卡片的禁用状态与卡片直接相关,因此这个需求应该是:

.card {
  display: flex;
+ &.is-disabled .badge {
+   opacity: 0.5;
+ }
}
.badge {
  appearance: none;
}

第二种是:尽管禁用是卡片的状态,但本质上我们是在实现一个徽标的变体。因此应该实现为:

.card {
  display: flex;
}
.badge {
  appearance: none;
+ .card.is-disabled & {
+   opacity: 0.5;
+ }
}

你认为比较合理的是哪一种呢?


如果这个例子不那么直观,我们可以将它扩展一下。在一个复杂的项目中,我们可以给出更多条件:

  • 徽标本身就有透明度设置
  • 卡片中除了徽标,还有更多结构

于是这个代码可能是:

.card {
  display: flex;
}
.header {
  /* ... */
}
.body {
  /* ... */
}
.badge {
  appearance: none;
  opacity: 0.8;
}

不装了,我摊牌了,其实我就是【第二种方式】的鼓吹者。因为只有把同一个元素在不同条件下的样式都放在一起,才能确保该元素的样式能够得到统一维护。

本质上,.card.is-disabled & 相当于是 .badge 的“Props”,我们总是应该尽可能把针对 Props 的逻辑放在组件内部,而不是在父组件完成处理再传入一个大的 children 进来(你会这么做吗?)。

你首先是你自己,然后才是……

这个例子和我说的“结构化样式”有什么关系呢?当然有。我在示例中使用的样式都是“扁平化”的,假设我们采用嵌套的形式编写,它可能会是:

.card {
  display: flex;
  .header {
    /* ... */
    .badge {
      appearance: none;
      opacity: 0.8;
    }
  }
  .body {
    /* ... */
  }
}

那么此时,我应该如何将 .card.is-disabled & 条件添加到已有的样式里呢?

当然,你可以觉得,既然这样,那在这种条件下,我们就不要使用层层嵌套的形式了嘛,直接把它挪到外面,这样我们也不用纠结前面讨论的问题了!

.card {
  display: flex;
  .header {
    /* ... */
  }
  .badge {
    appearance: none;
    opacity: 0.8;
  }
+ &.is-disabled .badge {
+   opacity: 0.5;
+ }
  .body {
    /* ... */
  }
}

那么在这里,我的问题是:凭啥?为什么我需要为了增加一个变体,改变原有的样式结构,甚至影响到样式的优先级

这里我想表达我的第二个观点:我们总是应该保持工程样式的最简化。这不仅是为了可读性,同时也是“面向结构的样式”和“面向元素的样式”之间的 PK。我的意思是,即使不使用嵌套,我也不会使用下面的写法:

.card {
  /* ... */
}
.card .header {
  /* ... */
}
.card .header .badge {
  /* ... */
}
.card.is-disabled .header .badge {
  /* ... */
}

在上面的这些例子里,.badge 不因 .card.header 而存在,尽可能保持非结构化更有助于我们在原本的样式中进行状态派生,而无论状态改变发生在哪个元素上;更何况,简单的样式能够让我们尽可能避免为了解决样式冲突而产生的 .badge.badge.badge 或者 !important

BEM 是组织样式代码的方式吗?

在上面的例子里,我始终没有聊到的一个原因是打包体积。一旦我们讨论到体积问题,反驳者会提出这样的东西:

.card {
  &-header {
    /* ... */
    &-badge {
      /* ... */
    }
  }
}

仿佛这样就用他们想要的样子实现了“扁平化”。从我们前面讨论的例子不难看出,这实际上和 class 嵌套并无二致。反对者可能会拿出 BEM 来反驳,并指出它是曾经很多主流组件库的“标准”:

.card {
  &__header {
    /* ... */
    &__badge {
      /* ... */
    }
  }
}

而我想指出的则是:BEM 不是用来组织样式代码的!BEM 的唯一作用是“命名”。它诞生的时候之所以流行,是因为解决了这些问题:

  • 因为 HTML 的元素过于“裸”,我们不得不在很多层级上添加很多元素,于是结构里出现了很多 .container、.wrapper、.content、.body……
  • 由于 CSS 的“Cascading”性质,我们总是希望尽量避免 class 名称的冲突,于是一种唯一性的生成逻辑可以最低成本帮助我们规避命名冲突

所有的这些优势,都和结构-样式的关联无关,BEM 最初就是为了解决命名问题。如今我们有 CSS Modules,有 Scoped CSS,有各种 CSS-in-JS 方案,命名的问题早就不存在了,BEM 现在主要的作用变成了降低命名成本,毕竟——

There are only two hard things in Computer Science: cache invalidation and naming things. —— Phil Karlton

预处理:CSS 的遗产

既然讨论到 BEM,我们不得不聊到 CSS Nesting。如果你还不知道,Chrome 120 也就是 2023 年开始,浏览器开始原生支持样式嵌套:

.badge {
  appearance: none;
  .card.is-disabled & {
    opacity: 0.5;
  }
}

它等价于 :is(.card.is-disabled) .badge。使用 CSS Nesting 或许是一种历史趋势,因为 Sass 团队一直在致力于使 SCSS 的嵌套与 CSS 兼容,即使其再次成为 CSS 的超集。

但是如果你也想使用它,你必须知道:

因此,从向前兼容的角度,使用扁平化+非结构化 CSS 也是一种更好的选择。当然,扁平-嵌套的问题,已经有很多人讨论过了,这并不是我们今天要聊的重点;不过,使用扁平化样式会让我们自然而然使用“非结构化”样式,尽管这两者没有强烈的联系。

TailwindCSS 和内联样式

在这里我想讨论的另一个问题是,怎样脱离结构声明样式?

一个反直觉的事实是,“CSS 命名”本身就是在和结构耦合。当我们写下

.card {}

实际上就预设了这个元素的某种“结构”——它是一个卡片,所以它可能会有头部、主题内容,甚至带点交互。所以这里可能比较让人难以接受的事实是:真正的“非结构化样式”其实就是内联样式


不论是 Styled Components 还是 CSS Modules,这些方案本质上都是在用“非结构化样式”代替“结构化样式”,实现的方式就是“避免 CSS 命名”。实际上他们都是在做这样的事情:

const card = 'display: flex'
const header = 'font-weight: 500'
const badge = 'opacity: 0.7'

return (
  #div style={card}#
    #div style={header}#
      #div style={badge}##/div#
    #/div#
  #/div#
)

和直接写 CSS 的区别在哪里呢?难道只是 class 变成了 style

——其实最大的区别就是,CSS-in-JS 是“不 Cascading 的”,我声明的标识符 card 就是唯一的,只在我使用它的地方生效,而不会应用在其他的元素上。对于 Web 应用来说,这种有限的预期才是我们通常想要的。这也是我讨论“非结构化”的初衷。


所以,就像很多批评的声音一样,TailwindCSS 是不是就是内联样式?没错。但是这就是我们想要的。内联样式(非结构化样式)就是比语义化样式(结构化样式)更适合现代前端工程,正因如此 TailwindCSS 才会越来越流行。所以,如果你还不能接受内联样式和 TailwindCSS,先从淘汰掉你的“结构化样式”开始吧。


Powered by Sairin