近几年来,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。
Uncompressed | Minified | Gzip | Brotli |
---|---|---|---|
3566.2 KB | 2872.2 KB | 289.2 KB | 71.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 年了,没用的小伙伴,可以用起来了~
参考资料: