css 原子化演进史

avatar

近几年来,CSS 及其社区已经从单纯的样式表发展成为一个完整的技术生态系统,如今的前端 Web 开发不再只涉及简单的 CSS 编写,还需要了解技术以及在任何给定场景中使用哪些技术。

最近两年原子 CSS 框架特别火,我们可以从近些年 CSS 编码史来看看,CSS 原子化出现的契机,解决了什么问题。

css 语义化

从我们一开始学习编写 CSS,关注点分离(Separation of Concerns)就是我们了解到的最佳实践,什么是关注点分离,MVC 就是关注点分离的一个体现,把业务逻辑、数据、界面分离。对应这里,HTML 中只应该包含有关内容的信息,而所有的样式都应该交给 CSS 来实现。

下面看一个例子:

<div class="text-center">
    hello world!
</div>

其中定义了类 text-center 表明文本居中的设计,这违反了关注点分离的思路,它将样式信息渗入到了 HTML 中,推荐的做法应该是根据文本内容来命名类名,比如 greeting。CSS 样式如下:

<style>
.greeting {
    text-align: center;
}
</style>

<div class="greeting">
    hello world!
</div>

再看下面的例子

<style>
.article {
    padding: 8px 12px;
    background: white;
    > h2 {
     	font-size: 20px;
    }
    > p {
			font-size: 16px;
    }
}
</style>

<div class="article">
    <h2>原子化 css</h2>
    <p>近几年来,CSS 及其社区已经从单纯的样式表发展成为一个完整的技术生态系统...</p>
</div>

上述例子已经分离了关注点,但是 CSS 的嵌套结构里掺杂了 HTML 标签,我的 HTML 确实不用关注样式了,但是我的 CSS 却不得不非常关心 HTML 的结构。

解决 CSS 样式嵌套的耦合,比较出名的是 BEM。使用这种方法,上面的例子可以改写为:

<style>
.article{
    padding: 8px 12px;
    background: white;
 
    &__name{
        font-size: 20px;
    }
    &__desc{
        font-size: 16px;
    }
}
</style>

<div class="article">
    <h2 class="article__name">原子化 css</h2>
    <p class="article__desc">近几年来,CSS 及其社区已经从单纯的样式表发展成为一个完整的技术生态系统...</p>
</div>

上述方案解决了 CSS 样式的耦合问题,跟 HTML 标记也没有了关联,CSS 类名也是语义化的。

新需求来了!

当我们想增加类似的样式,显示 author 的信息,同样是名称 + 描述的形式。article 类名应用到 author 信息,这违反了语义化的规则。不行,这绝对不行~

那么如果我们定义 author  类名,我们就要把具体的样式都复制或者重写一遍。也可以使用 @extend 来继承 article 的样式。

css 组件化

我们可以创建与内容无关的样式,虽然从语义的角度讲,作者简介和文章预览没有必然联系,但是从设计角度讲,他们有着共同的样式,可以定义 card 类。

<style>
.card{
    padding: 8px 12px;
    background: white;
 
    &__name{
        font-size: 20px;
    }
    &__desc{
        font-size: 16px;
    }
}
</style>

尽可能创建不依赖 HTML 内容的可复用的类名定义,这样的类名可能有:card  btn btn-primary, 常见的 UI 框架 Bootstrap 也采用这种思想 ,提供了包括:导航、标签、工具条、按钮等一系列组件,方便开发者调用。

我们可以把这些可复用的 css 样式,称为 css 组件。

新需求又来了!

我们想要改变作者信息的样式,而不改变文章的样式,刚才我们定义的 css 组件可能又得考虑重新规划,重新规范时,还需要考虑到最佳实际,避免耦合,具有语义化,一不小心可能就是一座山。

我们像是进入了一个圈,好想解脱,如果我们从沿着 css 复用的思路继续走下去,我们更进一步- css 原子化

css 原子化

CSS 原子化是指定义一组表示单一用途样式单元的类。

与 CSS 组件化,了解两者可参考文章 「CSS 思维」组件化 VS 原子化

功能类优先(utility-first)

我们“传统上”认为 CSS 类代表 UI 的不同元素,它们的主要目的是在我们的 CSS 文件中描述特定元素的所有样式。

功能类优先颠覆了这个想法,使用类不是定义不同的组件,而是为我们提供一个包含不同帮助类(utility)的工具箱,我们可以将它们混合在一起,通过将它们应用到组件的 HTML 元素来设置组件的样式。

比较下面的两个示例

.card{
    padding: 8px 12px;
    background: white;
 
    &__name{
        font-size: 20px;
    }
    &__desc{
        font-size: 16px;
    }
}
<div class="card">
    <h2 class="card__name"></h2>
    <p class="card__desc"></p>
</div>
<div class="px-8 py-12 bg-white">
    <h2 class="text-20"></h2>
    <p class="text-16"></p>
</div>

我们将使用非语义的方式来代替语义命名类。因为命名很难,也许这是编程中最难的事情。 要为组件找到合适的名称,其元素和修饰符很难。像上面提到的 BEM 这样的方法确实很有帮助,尤其是在大团队中,但它并没有完全解决问题。而且不是每个人都能成为 BEM 专家,所以团队的产出是模糊的。如果能有一套标准的原子功能类集合,一定会倍感欣慰,tailwind 应运而生!

先行者 - tailwindcss

tailwindcss,一个功能类优先的 CSS 框架,用于快速构建定制的用户界面

活跃度

github starts 数量达到 57.3k(2022-05-30)。

npm 月下载量达到了 12,269,397(2022.04-29-2022.05.30),同期 vue 的下载量为 13,762,183

ps:

活跃度更新:

github starts 数量达到 63.4k(2022-12-27 )。

tailwindcss 下载量: 98,966,274(2022-05-24 - 2022-11-24)链接

vue 下载量: 91,506,191(2022-05-24 - 2022-11-24)链接

tailwindcss 的下载量已经超过 vue 了,虽然不一定有可比性,但是能说明 tailwindcss 受欢迎的程度。

vue (2022-11-27 - 2022-12-22)期间有异常数据不做比较~

使用

通过命令 npx tailwindcss init 就可以生成一个初始配置文件 tailwindcss.config.js,默认配置如下:

// tailwind 2.0+ 版本
module.exports = {
    purge: [],
    darkMode: false, // or 'media' or 'class'
    theme: {
        extend: {},
    },
    variants: {
        extend: {},
    },
    plugins: [],
}

你只需指定要更改的内容即可,缺少的部分将会使用 TailwindCss 默认配置

特点

  • 体积

使用默认配置,tailwindcss 的开发版本(2.2.4)是 3566.2 KB 未压缩, 用 Gzip 进行压缩可到 289.2 KB,用 Brotli 进行压缩 71.3 KB。

UncompressedMinifiedGzipBrotli
3566.2 KB2872.2 KB289.2 KB71.3 KB

当构建生产时,您应该总是使用 Tailwind 的 purge 选项来 tree-shake 优化未使用的样式,并优化您的最终构建大小当使用 Tailwind 删除未使用的样式时,很难最终得到超过 10kb 的压缩 CSS。

  • 不错的语义化

使用 tailwindcss 你不用花精力来定义类名,你可以使用内置具有良好语义化的类名,实现样式效果。你也可以一定程度定义符合你自己规则的类名,例如加上统一的前缀。

tailwindcss 语义化也并不完美,默认的命名方案有一定的记忆成本。如何解决?

插件加持:

推荐使用官方为 Visual Studio Code 用户准备的 TailwindCSS 智能感知工具

Webstorm 只需要启用插件,可以参考文章

有了智能提示,常用的很难记不住

  • 约束性

使用 TailwindCss 功能类,是从预定义的设计系统中选择样式,这使得构建统一的 UI 变得更加容易。这也是 CSS 原子化与直接使用内联样式有着明显差异,TailwindCss 具有约束性。例如 ui 同学使用的间距  4,8,12,16 等,对于其他数值,预定义的样式是不会有相应的 css 类的,超出范围后,不会生效

  • 响应式

TailwindCss 中的每个功能类都可以有条件的应用于不同的断点(breakpoints),在不同分辨率设备上,可以轻松切换属性。内联样式中,无法使用媒体查询。

  • Hover, focus, 以及其它状态

TailwindCss 处理 响应式设计 类似,通过为功能类添加适当的状态变体前缀,可以对处于 hover 、focus 和其它状态的元素设置样式, 而内联样式无法设置 hover 或者 focus 这样的状态。

windicss 出现

Windi CSS 视为 Tailwind 的按需替代品,它提供更快的加载时间、与 Tailwind v2.0 的完全兼容性以及一系列额外的酷功能。

活跃度

github starts 数量达到 5.1k(2022-05-30)。

ps:

活跃度更新:

github starts 数量达到 6k(2022-12-27 )。

值得一说的按需生成

当我的项目变大,大约有几十个组件时,初始编译时间达到 3s,使用 Tailwind CSS 热更新时间超过 1s。- voorjaar(windi css 创建者)

通过扫描您的 HTML 和 CSS 并按需生成实用程序,Windi CSS 能够在开发中提供更快的加载时间和快速的 HMR,并且不需要在生产中进行清除。

"按需生成" 的想法引入了一种全新的思维方式。让我们先来对比下这些方案:

传统的方式不仅会消耗不必要的资源(生成了但未使用),甚至有时更是无法满足你的需求,因为总会有部分需求无法包含在内。

通过调换 "生成" 和 "扫描" 的顺序,"按需" 会为你节省浪费的计算开销和传输成本,同时可以灵活地实现预生成无法实现的动态需求。另外,这种方法可以同时在开发和生产中使用,提供了一致的开发体验,使得 HMR (Hot Module Replacement, 热更新) 更加高效。

为了实现这一点,Windi CSS 采用了预先扫描源代码的方式。下面是一个简单示例:

import glob from 'fast-glob'
import { promises as fs } from 'fs'

// 通常这个是可以配置的
const include = ['src/**/*.{jsx,tsx,vue,html}']

async function scan() {
  const files = await glob(include)

  for (const file of files) {
    const content = await fs.readFile(file, 'utf8')
    // 将文件内容传递给生成器并配对 class 的使用情况
  }
}

await scan()
// 扫描会在构建/服务器启动前完成
await buildOrStartDevServer()

为了在开发期间提供 HMR,通常会启动一个 文件系统监听器:

import chokidar from 'chokidar'

chokidar.watch(include).on('change', (event, path) => {
  // 重新读取文件
  const content = await fs.readFile(file, 'utf8')
  // 将新的内容重新传递给生成器
  // 清除 CSS 模块的缓存并触发 HMR 事件
})

因此,通过按需生成方式,Windi CSS 获得了比传统的 Tailwind CSS 快 100 倍左右 的性能。

tailwindcss 2.1

TailwindCss 2.1 版本推出了JIT 模式,这一次 tailwind 站在了 windicss 的肩上。官方介绍入口

官方对 JIT 模式的定义 -- The Next Generation of Tailwind CSS

  • JIT 模式

在传统模式下,初始编译需要 3-8s 的时间,在大型的 webpack 项目里可能达到 30–45s,而 JIT 编译在大型的项目里也只需要 800ms 左右,增量更新时仅需要几毫秒。

在传统模式下,我们使用 pureCss 来优化生产环境的包大小。

module.exports = {
    prefix: 'tw-',
    enabled: process.env.NODE_ENV === 'production',
    purge: {
        content: ['./src/**/*.vue'],
    },
    ...
}

而在 JIT 模式下,TailwindCss 只会打包你用到的 css,使用 JIT 模式,保证了生产和开发环境生成一致的 css

module.exports = {
    mode: 'jit',  // 3.0 默认启用 jit
    prefix: 'tw-',
    // purge: {
    //    content: ['./src/**/*.vue'],
    // },
    ...
}
  • 轻松使用任意变体

在传统模式下,基于文件大小的考虑,像 focus-visible, active, disabled,,这些变体是默认 disabled 的。而 JIT 模式,你可以轻松使用任意的变体不再需要去配置。

  • 任意值 class

JIT模式提供了一个支持 任意值 class 的方式,把想要指定的值用 [...] 框起来,比如 h-[100px] 就可以设定高度为 100px 。 不过要注意的是不能滥用,否则 TailwindCss 的设计系统就失去了原本的优势了。

  • 生产和开发环境打包的 css 统一

tailwindcss 3

不出所料,tailwindcss 3.0 把 jit 模式作为了默认的引擎,之前一些 变体类 考虑到体积,还需要手动决定是否引入,现在默认就是按需构建、可使用任意辅助类、开发和生产构建方式与产物统一,避免了不一致性、还获得了极大的性能优化。

不得不说,tailwind 3.0 版本 已经很好用了,windicss 优势也没了~

重新构想原子化 Unocss - antfu

unocss ,即时按需 Atomic CSS 引擎,没有解析,没有 AST,没有扫描,它是即时的(比 Windi CSS 或 Tailwind JIT 快 5 倍),antfu 总是能有不错的点子,并为之行动!

活跃度

github starts 数量达到 5k(2022-05-30)。

活跃度更新:

github starts 数量达到 5k(2022-12-27 )。

特点:

  • 跳过解析,不使用 AST

从内部实现上看,Tailwind 依赖于 PostCSS 的 AST 进行修改,而 Windi 则是编写了一个自定义解析器和 AST。考虑到在开发过程中,这些工具 CSS 的并不经常变化,UnoCSS 通过非常高效的字符串拼接来直接生成对应的 CSS 而非引入整个编译过程。同时,UnoCSS 对类名和生成的 CSS 字符串进行了缓存,当再次遇到相同的实用工具类时,它可以绕过整个匹配和生成的过程。

  • 与 vite 结合

UnoCSS 有意只提供了 Vite 插件(以后可能考虑其他的集成),这使得它能够专注于与 Vite 的最佳集成。由于 Vite 也会处理 HMR,并在文件变化时再次执行 transform 钩子,这使得 UnoCSS 可以在一次加载中就完成所有的工作,没有重复的文件 I/O 和文件系统监听器。此外,通过这种方式,扫描会依赖于模块图而非文件 glob。这意味着只有构建在你应用程序中的模块才会影响生成的 CSS,而并非你文件夹下的任何文件。

目前 UnoCSS 还处于实验阶段,UnoCSS 是 Windi CSS 团队的一个激进的实验,可能成为 Windi CSS v4 的新引擎。

  • 更多

完全可定制- 没有核心实用程序,所有功能都是通过预设提供的。

CSS-in-JS 运行时构建- 使用 UnoCSS 和一行 CDN 导入。

...

团队使用

在团队内,我们选用 tailwindcss 框架,结合 UI 规范,定义了一套预设配置。根据 figma 设计稿 token,到编辑器的智能提示,到对应页面上相应的 tailwindcss 类,整个过程挺丝滑的!从开发成本来看,我们不用去纠结命名,不用再考虑功能迭代后,对应 css 结构是否需要重新设计,投入更多时间到其他核心业务开发去。从维护成本上来看,我们的标准是统一的,命名清晰,删除代码也会随着 html 标签一并删除, tailwindcss 成本更小。

总结

原子 css,这种细粒度的css,从一起初就备受争议,对新的想法和技术保持开放的态度是必不可少的,特别是如果很多人喜欢它。不是每个人都能成为 css 专家,当团队有一套标准,它能给我们带来方便,执行起来还很轻松,这就是很好的选择!

这都快 2023 年了,没用的小伙伴,可以用起来了~

参考资料:

  1. 重新构想原子化 CSS
  2. 「CSS思维」组件化VS原子化
  3. Windi CSS and Tailwind JIT
  4. CSS Utility Classes and "Separation of Concerns"