BTOA

浅谈原子化 CSS

2024-07-21 12:40 · CSS

Tailwind 是我在工作和日常开发中最喜欢使用的 CSS 框架,它使用的是原子化 CSS 原则,今天就借这篇文章简单讲讲这个概念吧。

原子化 CSS 这个词最早由 Thierry Koblentz 在他的文章《Challenging CSS Best Practices》中提出,它的定义是:

Atomic CSS is the approach to CSS architecture that favors small, single-purpose classes with names based on visual function.

简单来讲,下面这些 CSS 就是原子化 CSS:

.display-block {
  display: block;
}

.margin-10 {
  margin: 10px;
}

在谈论应用之前,我想先说说原子化 CSS 的诞生背景,我们来看一篇对 Thierry Koblentz 的采访:The Making of Atomic CSS: An Interview With Thierry Koblentz。在采访中,Thierry Koblentz 提到有一天他的主管 Michael Montesano 问他有没有一种方法能不改动全局 CSS 就可以改变页面中某个元素的样式,因为全局 CSS 的变动可能会引起其他元素样式的改变。Thierry Koblentz 给出了一套解决方案,叫做“utility-sheet”:

a sheet meant to let developers achieve their styling without the need to edit or add rules in a stylesheet.

这个方案其实就是一套原子化 CSS,作为最初始的方案,它只有限定的规则集,因此最后并没有得到广泛应用。直到后来 ACSS 诞生,才让原子化 CSS 进入更多人的视野。ACSS 语法更类似于函数式语言,它可以根据你传入的参数生成对应的 CSS 样式:

.fz(20px) {
  font-size: 20px;
}

.d(block) {
  display: block;
}

其实在这一阶段,原子化 CSS 的基础功能和 Tailwind 已经很接近了,只是语法上存在较大的不同。那么原子化 CSS 究竟想要解决什么问题呢?我们借用 Thierry Koblentz 文章中的例子进行说明,文章中给出了一段 HTML 结构:

<div class="media">
  <a href="https://twitter.com/thierrykoblentz" class="img">
    <img src="thierry.jpg" alt="me" width="40" />
  </a>
  <div class="bd">@thierrykoblentz 14 minutes ago</div>
</div>

它的 CSS 样式如下:

.media {
  margin: 10px;
}

.media,
.bd {
  overflow: hidden;
  _overflow: visible;
  zoom: 1;
}

.media .img {
  float: left;
  margin-right: 10px;
}

.media .img img {
  display: block;
}

最后的效果如下图所示:

接着,第一个需求产生了:需要让图片显示在文字右边而不是左边。于是我们新增了以下的 CSS 样式,并应用到 HTML 结构中:

.media .imgExt {
  float: right;
  margin-left: 10px;
}
<div class="media">
  <a href="https://twitter.com/thierrykoblentz" class="imgExt">
    <img src="thierry.jpg" alt="me" width="40" />
  </a>
  <div class="bd">@thierrykoblentz 14 minutes ago</div>
</div>

之后,第二个需求产生了:如果这个结构出现在页面的右栏位置,显示的文字要变小。这时我们可以包一个 div 标签在最外层:

#rightRail .bd {
  font-size: smaller;
}
<div id="rightRail">
  <div class="media">
    <a href="https://twitter.com/thierrykoblentz" class="img">
      <img src="thierry.jpg" alt="me" width="40" />
    </a>
    <div class="bd">@thierrykoblentz 14 minutes ago</div>
  </div>
</div>

在这个例子中,Thierry Koblentz 提到了传统 CSS 方案的许多弊端,我们只选取最重要的一个:上述 6 个 CSS 样式中,有 4 个是跟语义(context)相关的(注:所有的二级样式),这 4 个样式难以被其他元素复用。就拿需求二中的 CSS 样式来讲,如果在项目协作中有人需要使用,那么必须包裹在 rightRail 这个 id 下,拿掉这个 id 就会让样式失效。针对这种弊端,Thierry Koblentz 用原子化 CSS 对上述例子进行了改写:

.Bfc {
  overflow: hidden;
  zoom: 1;
}

.M-10 {
  margin: 10px;
}

.Fl-start {
  float: left;
}

.Mend-10 {
  margin-right: 10px;
}

.Fz-s {
  font-size: smaller;
}
<div class="Bfc M-10">
  <a href="https://twitter.com/thierrykoblentz" class="Fl-start Mend-10">
    <img src="thierry.jpg" alt="me" width="40" />
  </a>
  <div class="Bfc Fz-s">@thierrykoblentz 14 minutes ago</div>
</div>

现在所有的样式都和语义无关,可以随意进行组合,如果我需要让字体变小,只需要添加 Fz-s 这个类名即可。那么这种优化有什么优势呢?假如现在有两个组件 UserProfile 和 AdminProfile,它们的昵称部分都使用了 font-size: 20px 的样式,如果按照语义化 CSS 来写,需要用到两个类,而原子化 CSS 只需要一个类:

.user .nick {
  font-size: 20px;
}

.admin .nick {
  font-size: 20px;
}
.font-lg {
  font-size: 20px;
}

原子化 CSS 的复用性更高,因此打包的 CSS 文件会更小,这无形中加快了网站的打开速度。由于原子化 CSS 做到了样式和语义的分离,因此我若不想让 UserProfile 的昵称变大,只需要删除 HTML 结构中的 font-lg 类即可,不用担心其他地方会受影响。

虽然原子化 CSS 存在许多优势,但仍有开发者认为原子化 CSS 不过是奇淫巧计,因为它完全破坏了 HTML 和 CSS 分离的规则,这和 inline-style 并没有什么区别。在这里我简单谈一下自己的看法吧,关于规则,我想借用 Tailwind 作者 Adam Wathan 说的一句话回答:

Neither is inherently "wrong"; it's just a decision made based on what's more important to you in a specific context. For the project you're working on, what would be more valuable: restyleable HTML, or reusable CSS?

原子化 CSS 和语义化 CSS 并不存在谁对谁错的问题,它们只是开发使用的工具。在 JSX 诞生之前,HTML、CSS 和 JavaScript 分离是约定俗成的规则,但随着 JSX 的出现,更多开发者意识到 HTML 和 JavaScript 合并的写法能让开发更加快捷简便。作为现在广泛使用的框架,已经没人会说 React 是“奇淫巧计”了。至于原子化 CSS 和 inline-style 有何区别,首先是优先级,inline-style 的优先级是最高的,在客制化 CSS 时并不是很好的选择,其次,inline-style 不支持伪类和伪元素,而原子化 CSS 支持,最后,原子化 CSS 的类名不带具体属性,而 inline-style 是一种 key-value 结构,长度很明显比原子化 CSS 的类名长。

不得不承认,原子化 CSS 最被人诟病的地方是冗长的类名,然而,没有哪个开发工具是完美的,我们能做的只有尽力去适应和改善不完美。假如某个项目中有个 Button 按钮需要复用多次,并且该按钮使用了原子化 CSS,并不意味着我们需要复制一堆类名,现在的开发框架已经非常先进了,完全可以使用组件来改写,组件化开发的一个优势就是解决复用性问题。至于有人提到使用 Tailwind 开发记不住类名,或者类名排序混乱,现在已经有许多工具可以解决这些问题:VSCode 的 Tailwind CSS IntelliSense 插件,可以补全类名并且直接查看对应的 CSS 样式;prettier-plugin-tailwindcss 依赖包可以根据开发者的设置格式化 Tailwind 类名,甚至还支持 cva 格式。

当时我从入门 Tailwind 到能够比较熟练使用,只用了不到一周的时间,我觉得 Tailwind 的学习成本和其他语言相比真的很小了。最后,在本文的结尾,我作为一个开发者,最想说的一句话是:

开发中的规则并不是束缚,而是引导,在实际需求下,当存在比规则更优的选择时,我们应该大胆突破,而不是墨守成规。