什么是原子化 CSS?

原子化 CSS 是一种 CSS 编写的方法论,它的主要思想是将 CSS 样式规则拆分成小的、独立的、可复用的部分,这些部分被称为“原子类”

在原子化 CSS 中,每个原子类只包含一个很小的样式规则,比如设置颜色、字体大小、边距、背景等。通过将这些小的样式规则组合起来,可以构建出复杂的页面。

比如,我们平时写 css 是这样的:

1
2
3
4
5
6
7
<div class="aaa"></div>

.aaa {
font-size: 16px;
border: 1px solid #000;
padding: 4px;
}

在 html 里指定 class,然后在 css 里定义这个 class 的样式。

也就是 class 里包含多个样式:

而原子化 css 是这样的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div class="text-base p-1 border border-black border-solid"></div>

.text-base {
font-size: 16px;
}
.p-1 {
padding: 4px;
}
.border {
border-width: 1px;
}
.border-black {
border-color: black;
}
.border-solid {
border-style: solid;
}

定义一些细粒度的 class,叫做原子 class,然后在 html 里直接引入这些原子化的 class。

原子化 CSS 的优势包括:

  1. 可复用性:由于样式规则被拆分成独立的原子类,可以在不同的元素上重复使用。这样可以减少代码冗余并提高代码的可维护性。
  2. 灵活性:通过组合不同的原子类,可以快速构建出不同样式和布局的组件。
  3. 性能优化:由于原子类是独立的小样式规则,可以利用浏览器的缓存机制,减少下载和解析的数据量,从而提高页面加载速度。
  4. 易于维护:每个原子类只包含一个样式规则,修改和扩展样式更加清晰和方便。

尽管原子化 CSS 有一些好处,但也存在一些缺点和考虑因素:

  1. 学习曲线:原子化 CSS 需要掌握一套新的类命名规则和组合方式,这可能需要一定的学习曲线。特别是对于团队中新加入的开发人员,需要一定的时间来适应和理解原子化 CSS 的工作方式。
  2. 类名冲突:当原子类命名不规范或命名空间不清晰时,可能会导致类名冲突的问题。如果不小心定义了相同的类名,可能会造成样式的混乱和冲突。
  3. 管理成本:由于原子化 CSS 以小的样式单元为基础,可能会导致样式表的冗余和重复。在大型项目中,管理和维护大量的原子类可能会变得复杂,并需要额外的工具和规范来确保一致性和可维护性。
  4. 语义性问题:原子化 CSS 通常使用简短的类名来表示样式规则,这可能会导致语义性的丧失。开发人员和设计师可能需要花费额外的时间来理解每个类名的含义和作用。
  5. 深度嵌套的问题:原子化 CSS 通常倾向于使用多个类名来定义样式,这可能导致在 HTML 标记中出现深度嵌套的问题。这可能会增加 HTML 的复杂性,并可能使选择器的性能受到影响。

因此,在决定是否使用原子化CSS时,需要考虑项目的规模、团队的技术能力以及维护成本等因素,权衡好其中的利与弊。

Tailwind 是流行的原子化 css 框架。

原子化 CSS 最流行的框架应该就是 Tailwind CSS了。有多流行呢?

它现在有 68k star 了,要知道 express 才 60k。

原子化 css 它到底有啥好处呢? 它解决了什么问题?

Tailwind 实战!

  1. 我们通过 crerate-react-app 创建一个 react 项目:
1
npx create-react-app tailwind-test
  1. 然后进入 tailwind-test 目录,执行:
1
2
npm install -D tailwindcss
npx tailwindcss init
  1. 安装 tailwindcss 依赖,创建 tailwindcss 配置文件:

tailwind 实际上是一个 postcss 插件,因为 cra 内部已经做了 postcss 集成 tailwind 插件的配置,这一步就不用做了:

  1. 然后在入口 css 里加上这三行代码:

这三行分别是引入 tailwind 的基础样式、组件样式、工具样式的。

  1. 之后就可以在组件里用 tailwind 提供的 class 了:
1
2
3
4
5
6
7
8
9
import './App.css';

function App() {
return (
<div className='text-base p-1 border border-black border-solid'>guang</div>
);
}

export default App;
  1. 我们执行 npm run start 把开发服务跑起来。

可以看到,它正确的加上了样式:

用到的这些原子 class 就是 tailwind 提供的:

  1. 这里的 p-1padding:0.25rem,你也可以在配置文件里修改它的值:

tailwind.config.jstheme.extend 修改 p-1 的值,设置为 30px。

刷新页面,就可以看到 p-1 的样式变了:

  1. .text-base 是 font-size、line-height 两个样式,这种通过数组配置:

也就是说所有 tailwind 提供的所有内置原子 class 都可以配置。

  1. 但这些都是全局的更改,有的时候你想临时设置一些值,可以用 [] 语法。

比如 text-[14px],它就会生成 font-size:14px 的样式:

比如 aspect-[4/3],就是这样的样式:

我们平时经常指定 hover 时的样式,在 tailwind 里怎么指定呢?

很简单,这样写:

生成的就是带状态的 class:

  1. 此外,写响应式的页面的时候,我们要指定什么宽度的时候用什么样式,这个用 tailwind 怎么写呢?

也是一样的写法:

生成的是这样的代码:

  1. 这个断点位置自然也是可以配置的:

可以看到 md 断点对应的宽度变了:

光这些就很方便了。

之前要这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="aaa"></div>

.aaa {
background: red;
font-size: 16px;
}

.aaa:hover {
font-size: 30px;
}

@media(min-width:768px) {
.aaa {
background: blue;
}
}

现在只需要这样:

1
<div class="text-[14px] bg-red-500 hover:text-[30px] md:bg-blue-500"></div>

省去了很多样板代码,还省掉了 class 的命名。

并且这些 class 都可以通过配置来统一修改。

感受到原子化 css 的好处了么?

tailwind 文档提到了 3 个好处:

不用起 class 名字,这点简直太爽了,我就经常被起 class 名字折磨。

css 不会一直增长,因为如果你用之前的写法可能是这样的:

多个 class 里都包含了类似的样式,但你需要写多次,而如果用了原子 class,就只需要定义一次就好了。

css 没有模块作用域,所以可能你在这里加了一个样式,结果别的地方样式错乱了。

而用原子 class 就没这种问题,因为样式是只是作用在某个 html 标签的。

我觉得光这三点好处就能够说服我用它了,特别是不用起 class 名字这点。

当然,社区也有一些反对的声音,我们来看看他们是怎么说的:

一堆 class,可读性、可维护性太差了

真的么?

这种把 css 写在 html 里的方式应该是更高效才对。

想想为啥 vue 要创造个单文件组件的语法,把 js、css、template 放在一个文件里写,不就是为了紧凑么?

之前你要在 css、js 文件里反复跳来跳去的,查找某个 class 的样式是啥,现在不用这么跳了,直接在 html 里写原子样式,它不香么?

而且 tailwindcss 就前面提到的那么几个语法,没啥学习成本,很容易看懂才对。

但是还要每次去查文档哪些 class 对应什么样式呀?

vscode 插件

这个可以用 tailwind css 提供的 vscode 插件来解决。

安装这个 Tailwind CSS IntelliSense 之后的体验是这样的:

有智能提示,可以查看它对应的样式。

不需要记。

难以调试?

在 chrome devtools 里可以直接看到有啥样式,而且样式之间基本没有交叉,很容易调试:

相反,我倒是觉得之前那种写法容易多个 class 的样式相互覆盖,还要确定优先级和顺序,那个更难调试才对:

类型太长了而且重复多次

这种问题可以用 @layer @apply 指令来扩展:

前面讲过 @tailwind 是引入不同的样式的,而 @layer 就是在某一层样式做修改和扩充,里面可以用 @apply 应用其他样式。

效果是这样的:

内置 class 不能满足我的需求

其实上面那个 @layer 和 @apply 就能扩展内置原子 class。

想跨项目复用?

那可以开发个 tailwind 插件

1
2
3
4
5
6
7
8
9
10
11
12
13
const plugin = require('tailwindcss/plugin');

module.exports = plugin(function({ addUtilities }) {
addUtilities({
'.guang': {
background: 'blue',
color: 'yellow'
},
'.guangguang': {
'font-size': '70px'
}
})
})

在 tailwind.config.js 里引入:

这样就可以用这个新加的原子 class 了:

插件的方式或者 @layer 的方式都可以扩展。

tailwind 的 class 名和我已有的 class 冲突了咋办?

命名冲突问题

比如我本来有个 border 的 class:

而 tailwind 也有,不就冲突了么?

这个可以通过加 prefix 解决:

不过这样所有的原子 class 都得加 prefix 了:

知道了什么是原子 css 以及 tailwind 的用法之后,我们再来看看它的实现原理。

实现原理

tailwind 可以单独跑,也可以作为 postcss 插件来跑。这是因为如果单独跑的话,它也会跑起 postcss,然后应用 tailwind 的插件:

所以说,tailwind 本质上就是个 postcss 插件。

postcss 是一个 css 编译器,它是 parse、transform、generate 的流程。

astexplorer.net 可以看到 postcss 的 AST:

而 postcss 就是通过 AST 来拿到 @tailwind、@layer、@apply 这些它扩展的指令,分别作相应的处理,也就是对 AST 的增删改查。

那它是怎么扫描到 js、html 中的 className 的呢?

这是因为它有个 extractor 的东西,用来通过正则匹配文本中的 class,之后添加到 AST 中,最终生成代码。

extractor 的功能看下测试用例就明白了:

所以说,tailwind 就是基于 postcss 的 AST 实现的 css 代码生成工具,并且做了通过 extractor 提取 js、html 中 class 的功能。

tailwind 还有种叫 JIT 的编译方式,这个原理也容易理解,本来是全部引入原子 css 然后过滤掉没有用到的,而 JIT 的话就是根据提取到的 class 来动态引入原子 css,更高效一点。

为啥这个 css 框架叫 tailwind ?

因为作者喜欢叫做 kiteboarding 风筝冲浪的运动。

就是这样的,一个风筝,一个冲浪板:

这种运动在顺风 tailwind 和逆风 headwind 下有不同的技巧。而 tailwind 的时候明显更加省力。

所以就给这个 css 框架起名叫 tailwind 了。

确实,我也觉得用这种方式来写 css 更加省力、高效,不用写 class 名字了,代码更简洁了,还不容易样式冲突了。

总结

tailwind 是一个流行的原子化 css 框架。

传统 css 写法是定义 class,然后在 class 内部写样式,而原子化 css 是预定义一些细粒度 class,通过组合 class 的方式完成样式编写。

tailwind 用起来很简单:

所有预定义的 class 都可以通过配置文件修改值,也可以通过 aaa-[14px] 的方式定义任意值的 class。

所有 class 都可以通过 hover:xxx、md:xxx 的方式来添加某个状态下的样式,响应式的样式,相比传统的写法简洁太多了。

它的优点有很多,我个人最喜欢的就是不用起 class 的名字了,而且避免了同样的样式在多个 class 里定义多次导致代码重复,并且局部作用于某个标签,避免了全局污染。

它可以通过 @layer、@apply 或者插件的方式扩展原子 class,支持 prefix 来避免 class 名字冲突。

tailwind 本质上就是一个 postcss 插件,通过 AST 来分析 css 代码,对 css 做增删改,并且可以通过 extractor 提取 js、html 中的 class,之后基于这些来生成最终的 css 代码。

是否感受到了 tailwind 的简洁高效,易于扩展?就是这些原因让它成为了最流行的原子化 css 框架。