聊聊原子类(Atomic CSS)

2021-07-26 · 技术 · 约 14 分钟读完

看看 inline style killer。

借着 Tailwind CSS 的热度,这几年原子类再次走入前端视野。我对它多有耳闻,但一直不理解为什么会火——这和写 inline style 能有多大区别呢?

这两天和同事聊到原子类,突然脑袋灯泡一亮,理解了它的价值所在。在此权作记录。

什么是原子类

通常我们写 CSS,都是在一个类里写一系列 property,例如:

<style>
  .card {
    padding: 20px;
    font-size: 14px;
    color: #000000;
  }
</style>

<div class="card"></div>

而所谓原子类,就是把每一句 property 都拆成一个 CSS 类,让它们像「原子」一样,通过类的组合,拼接成最终的样式。

用原子类改写上面的样式:

<style>
  .p-20 {
    padding: 20px;
  }
  .fs-md {
    font-size: 14px;
  }
  .fc-black {
    color: #000000;
  }
</style>

<div class="p-20 fs-md fc-black"></div>

容易发现,这种写法和古早的 inline style 很相似:

<div style="padding: 20px; font-size: 14px; color: #000000;"></div>

这是 inline style 的复辟吗?原子类是不是多此一举?

为什么不用 inline style

权重

CSS 选择器权重是最明显的优势。

inline style 权重高到离谱,以至于几乎没法覆写( (1, 0, 0, 0) ),而 class 仅为 (0, 0, 1, 0)

同时,由于原子类通常写法是平铺,这避免了选择器嵌套,使得覆盖样式时几乎无需考虑权重问题:

<!-- 传统写法 -->
<style>
  .parent .child {
    color: red; /* 该样式选择器权重为 (0, 0, 2, 0),最少要写两个 class 才能覆盖得了 */
  }
</style>

<div class="parent">
  <div class="child"></div>
</div>

<!-- 原子类写法 -->
<style>
  .fc-red {
    color: red; /* 该样式选择器权重为 (0, 0, 1, 0),只需一个 class 就能覆盖 */
  }
</style>

<div>
  <div class="fc-red"></div>
</div>

实现 inline style 做不到的样式

举例:

.hover-red {
  &:hover {
    color: red;
  }
}

规范设计系统,减少输入负担

原子类应当是配合设计系统使用的。由于它限定了样式的数量,前端样式不会有太多自定义内容,从而被强制规范化。同时前端与 UI 设计系统的一致,也使得「通过转换工具,将样式直接从设计稿套用到代码」成为可能。

原子类的简短名称,避免了每次都要输入冗长的 style 语句,更短的输入内容也能减少输错字符的情况。

为什么不用常规 class

不再重要的「可复用性」

inline style 之所以成为「最差实践」,一是样式权重高,二是可复用性差。推荐使用 class 的一个原因也是它的高复用性。但现代 Web 开发中,class 的复用性是否依然重要?

现在复用的基本单位已是整个组件:当另一处用到相同内容时,不再是编写 HTML + 复用 CSS,而是将整个组件 import 过去。

在一个组件内部能有多少复用的样式呢?

能复用的,多半是些全局都可能用到的样式。例如 ".ellipsis"".primary-color"——这种一般可以称做「样式规范」,也正是原子类所包含的内容,使用原子类等于已经带上了这些场景中的复用性。

减小打包体积

常规 class 中不可避免会有重复的 property,以致存在冗余。更换为原子类能减少此类冗余,对于不冗余的内容也能减少字符数量,总体上减小打包体积。

搭建设计系统的必经之路

由于公司项目设计系统不规范,我设想过用一整套工具链,在前端和 UI 之间统一与规范设计系统:

  • 设计部门搭建设计系统,通过 lint 规范设计稿必须遵循系统
  • 自动化工具,将设计系统导出生成 NPM 包
  • 前端项目中引入 NPM 包,此时可在 CSS 中引用设计系统的样式
  • 为了方便引用设计系统,它的样式用 "pd-lg" "font-sm" 之类的代号表示

    • 设计稿中包含代号信息,方便开发人员引用
    • 在 CSS 中可混写 property 和代号,代号使用特定前缀区分。通过 Babel 等将样式代号转换为实际样式。例如:

      .card {
        margin: 10px 0;
        /pd-lg; /* 代号 */
        /font-sm; /* 代号 */
        color: #000000;
      }
      
  • 编写 VSCode 插件,功能:代号说明、代号自动补全、lint 错误代号、将现有 CSS property 转为代号
  • 设计系统有变化,更新 NPM 包;前端 NPM 包升级后,相关样式自动统一改变

接着我发现,前端 CSS 混写的思路,和这个 Babel macro 很像;

进一步我发现,其实我就是在做一个原子类。

为什么不用 CSS-in-JS

上手简单

相比需要配置 Webpack loader、Babel plugin 的 CSS-in-JS 方案,理想的原子类只需在全局引入一个 .css 文件就能立即使用。

这里要提一下 Tailwind CSS,它由于本身复杂性,以及带有 hack,所以需要做一定配置。如果是稍简单一点的原子类方案,不需要这么做。

易于迁移

除 CSS Modules 外,大部分 CSS-in-JS 方案和框架强绑定。例如使用 styled-components 的 React 项目,改造成 Vue 需要大量改写样式。原子类由于本身仅仅是 class,在各框架中都支持,能无缝迁移。

原子类的问题

正所谓「没有银弹」,原子类只是解决上述问题比较普适的一个方案。经历过潮流的一次轮回,它仍未完全替代传统 class,正是因为其本身存在的一些问题。

有一定学习成本

原子类本身有一定的学习成本,虽然有相关插件能减少认知负担,使用时还是要大量查阅文档。

不能完全取代 class

class 除了提供样式,也具有「语义化」的功能。一个清晰的 class,能告诉读者这个 HTML 元素起到什么作用。

它还作为 HTML 元素的标记:传统 Web 开发常常使用 querySelector(className) 来获取 DOM 元素。如果仅使用原子类,就只能用 id 获取元素,或者迁移到现代 Web 框架。

Tailwind CSS 作者也指出,「该用传统 class 时就用 class」

这些场景其实在开发中是很常见的:

/* 特殊伪元素 */
.list-item::before {
  content: counter(name);
}

/* 特殊伪类 */
.item:nth-child(n + 1) {
  color: red;
}
.item:nth-child(n + 2) {
  color: green;
}
.item:nth-child(n + 3) {
  color: blue;
}

/* 自定义动画 */
@keyframe boom {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(3);
  }
  100% {
    transform: scale(0);
  }
}

对于此类特殊场景,常规原子类无法满足,仍须为其编写专用 class

我为什么不用原子类

原子类就聊到这里。

你可能会问,我在用原子类吗?我的回答是「没有」。原因有三:

  1. 无法实现复杂效果
  2. 公司的项目没有完善的设计系统
  3. 自己的项目懒得搞设计系统

以上。