跳到主要内容

Tailwindcss 原子类维护指南

前言

很多开发者,看到 Tailwindcss 的写法,或者初步使用它的时候,第一感觉可能就是 写是真的爽,维护火葬场

诚然,软件工程中没有银弹,原子化有原子化的问题,但这不意味着像 Tailwindcss/Unocss 这类工具背后的原子化 CSS 思想本身没有价值。

最起码,原子化 CSS 在一定程度上帮助我们解决了: 类的命名,复用,以及迁移的问题,甚至也能避免了一定程度上的样式污染。

但是这似乎也带来了代码冗余,可读性差的问题。所以接下来的内容就是来帮助大家更好的认识,和维护 Tailwindcss 原子类。

语义化 CSS

首先要声明的是,原子化 CSS 和内联 CSS 一点关系都没有!内联 CSS 的优先级更高,但是复用性,可维护性要差很多。

其次 原子化CSS 不是绝对意味着一个 class 对应着一条CSS声明(Declaration),比如 w-0 对应 width: 0px;

但是 line-clamp-2 (效果为文字超过2行显示 ... ) 这类一个就对应多个CSS 声明:

.line-clamp-2{
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}

所以原子化 CSS 中的原子化三个字,实际上指代的是 CSS 语义上 效果的原子化。

本质

就本质上而言,其实不论是 Tailwindcss 还是 Unocss,你都可以把它们想象成一个漏斗,它们从你编写的代码中,通过正则,匹配到符合规则的字符串,然后再生成对应的 CSS,就这么简单。

其中,Tailwindcss 大部分时候是作为 postcss 插件来使用的,它可以和众多 postcss 插件很好的配合起来使用。

Tailwindcss 中的 @tailwind 指令,本质上也就是把当前 @tailwind 所在文件所属的 Tailwindcss 上下文中的 base,components,utilities 这些 layer 依次展开罢了。

所以你在一个文件里这样写:

@tailwind base;
@tailwind components;
@tailwind utilities;
/* 再写一遍 @tailwind 会导致重复展开,造成大量的代码冗余 */
@tailwind base;
@tailwind components;
@tailwind utilities;

是没有意义的行为。

但是假如你通过 @config 指令,给不同CSS文件指定 tailwindcss 配置文件,类似于:

/* app.css 文件, 应用全局的 tailwind.config.js */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* prose.css 文件,应用当前文件下的 tailwind.prose.config.js */
@config "./tailwind.prose.config.js";
@tailwind base;
@tailwind components;
@tailwind utilities;

那么,这种一个项目,多 tailwindcss 配置,多 tailwindcss 上下文的方式,将给你带来很大的自由性。

类名冗余问题

类名冗余可能是我们使用 Tailwindcss 中经常遇到的问题,比如下面这段 HTML:

<div class="w-80 rounded-2xl bg-gray-100">
<div class="flex flex-col gap-2 p-8">
<input placeholder="Email" class="w-full rounded-lg border border-gray-300 bg-white px-4 py-3 focus:outline-none focus:ring-2 focus:ring-gray-700 focus:ring-offset-2 focus:ring-offset-gray-100" />
<label class="flex cursor-pointer items-center justify-between p-1">
Accept terms of use
<div class="relative inline-block">
<input type="checkbox" class="peer h-6 w-12 cursor-pointer appearance-none rounded-full border border-gray-300 bg-white checked:border-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-900 focus-visible:ring-offset-2" />
<span class="pointer-events-none absolute start-1 top-1 block h-4 w-4 rounded-full bg-gray-400 transition-all duration-200 peer-checked:start-7 peer-checked:bg-gray-900"></span>
</div>
</label>
<label class="flex cursor-pointer items-center justify-between p-1">
Submit to newsletter
<div class="relative inline-block">
<input type="checkbox" class="peer h-6 w-12 cursor-pointer appearance-none rounded-full border border-gray-300 bg-white checked:border-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-900 focus-visible:ring-offset-2" />
<span class="pointer-events-none absolute start-1 top-1 block h-4 w-4 rounded-full bg-gray-400 transition-all duration-200 peer-checked:start-7 peer-checked:bg-gray-900"></span>
</div>
</label>
<button class="inline-block cursor-pointer rounded-md bg-gray-700 px-4 py-3.5 text-center text-sm font-semibold uppercase text-white transition duration-200 ease-in-out hover:bg-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-700 focus-visible:ring-offset-2 active:scale-95">Save</button>
</div>
</div>

这段代码看着看着就晕了,虽然你可以通过 class 很直观的了解到了每个元素的样式,但是数量一多,理解成本还是会指数级上升的。

为了缓解这个问题,Windicss / Unocss 还设计了 Attributify 模式,使得它们可以依据属性进行分组归类,使得原子化类显得非常直观:

<button
bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"
text="sm white"
font="mono light"
p="y-2 x-4"
border="2 rounded blue-200"
>
Button
</button>

而且 Unocss 还利用它能够在编译时,改变用户代码的能力,构建了许多转化语法糖,比如 transformer-variant-group:

<div class="hover:(bg-gray-400 font-medium) font-(light mono)"/>

这些在一定程度上,都能缓解类名冗余的问题,但是无法解决这个问题。

那么,在 Tailwindcss 遇到这样的问题,应该如何处理呢?

最简单方式: @apply 提取

@applyTailwindcss 里的一个 CSS 指令,它可以把多个原子类给合并到一个你自定义的 CSS 节点中。

而且,写法上它也遵从 HTML 里的写法。你可以很容易的从 HTML 中复制你的原子类到 CSS 中,再把它们提取成一个单独的类。

@layer components{
.btn{
@apply inline-block cursor-pointer rounded-md bg-gray-700 px-4 py-3.5 text-center text-sm font-semibold uppercase text-white transition duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 active:scale-95;
}
/* more... */
}

这样,通过提取和组合,我们可以对原子类进行更高程度上的封装,值得一提的是 Tailwindcss 中最流行的 UI 框架: daisyUI 原理上也是类似的,不过它进行了进一步的处理和预提取,并最终把它们封装成了一个 Tailwindcss 插件罢了。

最终,我们把大量的原子化类,进行提取组合,并最终提炼出了原子化的CSS组件 card,label,btn,input组件,那么上面的 HTML 就被改造成了:

<div class="card bg-base-200 w-80">
<div class="card-body">
<input placeholder="Email" class="input input-bordered" />
<label class="label cursor-pointer">
Accept terms of use
<input type="checkbox" class="toggle" />
</label>
<label class="label cursor-pointer">
Submit to newsletter
<input type="checkbox" class="toggle" />
</label>
<button class="btn btn-neutral">Save</button>
</div>
</div>

此为最简单,最直接的方式去减小类名的冗余程度,但是这种方式也存在一定的缺陷。比如 @apply 这种本质上还是基于 CSS AST 的,用得多会有性能问题,另外智能提示也不友好。

所以刚开始可以这样使用,到出现性能问题的时候,我们就需要进行更高一部分的封装: 提炼成 Tailwindcss 插件

提炼成 Tailwindcss 插件

Tailwindcss 官方文档实际上希望我们把样式,提炼成 Tailwindcss Plugin

这样做有许多的好处,比如智能提示友好,性能也比 @apply 要高。

它的编写方式也非常简单:

const plugin = require('tailwindcss/plugin')

module.exports = {
plugins: [
plugin(function({ addUtilities, addComponents, e, config }) {
addUtilities({
'.content-auto': {
'content-visibility': 'auto',
},
'.content-hidden': {
'content-visibility': 'hidden',
},
'.content-visible': {
'content-visibility': 'visible',
},
})
}),
]
}

其中 addUtilities/addComponents/matchUtilities/matchComponents 这些函数都是用来添加对应的样式到 tailwindcss 中。

它们参数中,添加的 CSS 对象遵从 CSS-in-JS 语法

幸运的是,我们无需重新编写代码,便可以直接把之前 @apply 部分的代码,转化成 CSS-in-JS 对象。这一切都只需要我们用到 postcss-js 这个工具。

postcss-js 作为 postcss 生态的组成部分,它能够解析 CSS-in-JS 对象,同样它也能够把 postcss 解析的 AST 转化成 CSS-in-JS 对象。

所以它自然可以把 CSS 字符串,直接转化成 CSS-in-JS 对象。这正是我们想要达到的效果。大体的执行脚本如下:

const postcss = require('postcss')
const path = require('path')
const fs = require('fs')
const tailwindcss = require('tailwindcss')
const postcssJs = require('postcss-js')

async function main () {
const { root } = await postcss([
tailwindcss()
]).process('@tailwind components;' + `@layer components{
.btn{
@apply inline-block cursor-pointer rounded-md bg-gray-700 px-4 py-3.5 text-center text-sm font-semibold uppercase text-white transition duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 active:scale-95;
}
/* more... */
}`,
{
from: undefined
})
fs.writeFileSync(path.resolve(__dirname, './output.json'), JSON.stringify(postcssJs.objectify(root)), 'utf8')
}

main()

对应的 tailwind.config.js 添加 raw 来提取 btn 类。

/** @type {import('tailwindcss').Config} */
module.exports = {
content: [{
raw: 'btn'
}],
}

当然这是保存到本地磁盘的方式,接着我们编写的 Tailwindcss Plugin 只需要去引用这样的对象,把它们作为参数添加到 addUtilities/addComponents 中,即可正常使用。

例如这样的生成结果为:

{
".btn": {
"display": "inline-block",
"cursor": "pointer",
"borderRadius": "0.375rem",
"--tw-bg-opacity": "1",
"backgroundColor": "rgb(55 65 81 / var(--tw-bg-opacity))",
"paddingLeft": "1rem",
"paddingRight": "1rem",
"paddingTop": "0.875rem",
"paddingBottom": "0.875rem",
"textAlign": "center",
"fontSize": "0.875rem",
"lineHeight": "1.25rem",
"fontWeight": 600,
"textTransform": "uppercase",
"--tw-text-opacity": "1",
"color": "rgb(255 255 255 / var(--tw-text-opacity))",
"transitionProperty": "color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter",
"transitionDuration": "200ms",
"transitionTimingFunction": "cubic-bezier(0.4, 0, 0.2, 1)"
},
".btn:focus-visible": {
"outline": "2px solid transparent",
"outlineOffset": "2px",
"--tw-ring-offset-shadow": "var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)",
"--tw-ring-shadow": "var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)",
"boxShadow": "var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)",
"--tw-ring-offset-width": "2px"
},
".btn:active": {
"--tw-scale-x": ".95",
"--tw-scale-y": ".95",
"transform": "translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))"
}
}

不过这种方式其实也完全可以更进一步,接下来进入下一个章节: postcss 预生成产物。

postcss 预生成产物

其实在上一章中,我们已经使用到了一些预先生成的思想了。

毕竟 tailwindcss 本身不过是个 postcss 插件,我们自然可以通过编写脚本的方式,预先的把 CSS 先生成出来,直接交给项目进行使用的。

比如我们要从 utilities 提炼出一些 flex 相关的工具类出来,那么我们就可以写一段脚本:

const path = require('path')
const fs = require('fs')
const postcss = require('postcss')
const tailwindcss = require('tailwindcss')

async function main () {
const { css } = await postcss([
tailwindcss()
]).process('@tailwind utilities',
{
from: undefined
})
fs.writeFileSync(path.resolve(__dirname, './output.css'), css, 'utf8')
}

main()

对应的 tailwind.config.js 配置:

/** @type {import('tailwindcss').Config} */
module.exports = {
content: [{
raw: 'flex flex-1 flex-none flex-auto flex-initial'
}],
}

生成的 output.css 产物

.flex {
display: flex
}
.flex-1 {
flex: 1 1 0%
}
.flex-auto {
flex: 1 1 auto
}
.flex-initial {
flex: 0 1 auto
}
.flex-none {
flex: none
}

所以按照这种方式,我们也可以把之前那些 @apply 直接复制过来,进行处理,生成出 CSS,交给项目来进行使用。同样也能通过这种方式来提炼出 CSS 组件。或者把插件里面的 CSS 代码,比如 daisyUI 里面的样式给 “倒出来”。

Unocss 对比 Tailwindcss

Unocss 相比 Tailwindcss 强大之处,在于 Tailwindcss 仅仅只是一个 postcss 插件,而 Unocss 不是。

Tailwindcss 功能简单到,它只是从我们的源代码 (content) 中,提取到字符串,然后去生成 CSS 节点。

Unocss 更多时候作为一个打包插件去使用,它可以复用我们打包的产物,并从里面进行提取字符生成 CSS 节点,甚至它有能力去修改我们的代码 (transformer)。这样的能力 Tailwindcss 是不具备的,这也是 Unocss 快且功能丰富的原因。

不过单纯这样比较是没有意义的,就像名义上 Unocss 快是因为它不用解析 AST,但是你一旦想用到 @apply 这样的 directives 功能,Unocss 也不可避免的去使用 css-tree 去解析和操纵 AST。而这个功能是作为 postcss 插件的 Tailwindcss 内置的,而 Unocss 需要额外的包去实现。

所以比较公平的比较方式,应该是用 Unocsspostcss 插件来和 Tailwindcss 做比较。

不过,作为 Windi CSS 思路继承者的 UnoCSS,在原子化 CSS 各个方面都做的更加极致也是不争的事实。而且由于作者 antfu 在国内国外的高人气,也吸引了许多志同道合的开发者,积极的为 UnoCSS 做贡献,使得生态也生机勃勃,未来可期,真是羡煞旁人啊!

Tailwindcss 作为一个成功商业化的开源产品,虽然不如 UnoCSS 激进,但出的比较早,相对来说生态更丰富,使用人数更多一些,相对应的前人踩过的坑,解决方案也完备一些,而且有公司和资金支持,理论上不会烂尾。

所以希望 Unocss 多为我们探索更多原子化的极限,也希望 Tailwindcss 多争点气,虚心多学习学习别人的优势。

More

更多的方法论 Coming Soon...