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
提取
@apply
是 Tailwindcss
里的一个 CSS
指令,它可以把多个原子类给合并到一个你自定义的 CSS
节点中。
而且,写法上它也遵从 HTML
里的写法。你可以很容易的从 HTML
中复制你的原子类到 CSS
中,再把它们提取成一个单独的类。
@layer components {
/* 使用 utilities 里的 inline-flex-center */
.btn {
@apply inline-flex-center font-bold py-2 px-4 rounded cursor-pointer;
}
/* 使用 components 里的 btn */
.btn-pink {
@apply btn bg-pink-600 hover:bg-pink-900 text-white;
}
}
@layer utilities {
.inline-flex-center {
@apply inline-flex items-center justify-center;
}
}
效果如下所示:
这样,通过提取和组合,我们可以对原子类进行更高程度上的封装,值得一提的是 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
需要额外的包去实现。
所以比较公平的比较方式,应该是用 Unocss
的 postcss
插件来和 Tailwindcss
做比较。
不过,作为 Windi CSS
思路继承者的 UnoCSS
,在原子化 CSS
各个方面都做的更加极致也是不争的事实。而且由于作者 antfu
在国内国外的高人气,也吸引了许多志同道合的开发者,积极的为 UnoCSS
做贡献,使得生态也生机勃勃,未来可期,真是羡煞旁人啊!
而 Tailwindcss
作为一个成功商业化的开源产品,虽然不如 UnoCSS
激进,但出的比较早,相对来说生态更丰富,使用人数更多一些,相对应的前人踩过的坑,解决方案也完备一些,而且有公司和资金支持,理论上不会烂尾。
所以希望 Unocss
多为我们探索更多原子化的极限,也希望 Tailwindcss
多争点气,虚心多学习学习别人的优势。
More
更多的方法论 Coming Soon...