Appearance
核心概念:规则、预设与变体
上一章我们完成了 UnoCSS 的工程接入,写了一个简单的 uno.config.ts,引入了 presetUno() 预设。但这只是"能用"——要"用好",必须理解 UnoCSS 底层的三个核心抽象。
关于 UnoCSS,首先要问一个问题:当你写下 hover:text-red-500 这个类名时,UnoCSS 内部发生了什么?
要回答这个问题,我们需要理解三个概念:规则(Rules)定义类名如何映射为 CSS,预设(Presets)定义规则、变体、主题如何组织与复用,变体(Variants)定义 hover:、md: 这些前缀如何修饰生成的 CSS。
本章的目标是从抽象模型层面讲清楚这三者的机制与关系,为后续章节的深度实践打下基础。
1. 规则(Rules):类名到 CSS 的映射
规则是 UnoCSS 的最小构建单元。
1.1 规则的本质是什么?
思考一下,当你在 HTML 中写 <div class="flex">,UnoCSS 怎么知道要生成 display: flex 这段 CSS?
答案是:规则。
一条规则本质上回答一个问题:当我在代码里写了某个类名,UnoCSS 应该生成什么 CSS?用伪代码表示就是 Rule: className → CSSDeclaration。比如 flex 对应 display: flex;,text-center 对应 text-align: center;,p-4 对应 padding: 1rem;。
UnoCSS 的核心工作就是维护一组规则,当扫描到类名时,依次尝试匹配规则,命中则生成对应 CSS。
1.2 静态规则与动态规则
现在要问第二个问题:规则只能一一对应吗?如果我想支持 m-1、m-2、m-100 这种无限多的类名呢?
规则分为两类。
静态规则(Static Rules)是类名固定字符串,CSS 输出也是固定的:
ts
rules: [
['flex', { display: 'flex' }],
['hidden', { display: 'none' }],
]flex 就是 flex,一一对应,简单直接。
动态规则(Dynamic Rules)是类名包含可变部分,需要通过正则或函数提取参数,再动态生成 CSS:
ts
rules: [
// 匹配 m-1, m-2, m-4 等
[/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 0.25}rem` })],
]有没有感觉到这个设计的精妙之处? 一条规则就能覆盖 m-1 到 m-100(甚至更多),不需要穷举定义。
动态规则的能力在于它可以用一条规则覆盖一整类类名,而且在生成 CSS 时可以访问 theme 配置,做到设计 token 与规则联动。后续章节"深入规则"会展开更多写法与技巧。
1.3 规则匹配的优先级
现在要问第三个问题:如果多条规则都能匹配同一个类名,谁说了算?
答案是:先定义的规则先尝试,一旦命中,后续规则不再尝试。
这意味着如果你想覆盖某预设中的规则,需要在 rules 数组中靠前位置定义你自己的规则。预设内部的规则顺序也会影响最终行为。
了解这一点很重要,它能帮你调试"为什么某个类名生成了不符合预期的 CSS"。
2. 预设(Presets):规则的组织与复用单元
规则讲清楚了,现在要问下一个问题:规则从哪来?
你当然可以自己一条条写规则,但如果每个项目都要从零开始定义 flex、p-4、text-red-500,那也太累了。
这就是预设的用途。
2.1 预设是什么?
预设是 UnoCSS 的能力集合。它将一组相关的规则、变体、主题配置、快捷方式等打包在一起,供使用者按需引入。
一个预设本质上是一个函数,返回一个包含 name、rules、variants、theme、shortcuts 等字段的对象。
以 presetUno 为例,它内部包含数百条规则覆盖 flex、grid、spacing、colors、typography 等,若干变体支持 hover:、focus:、md:、dark: 等,以及主题配置定义默认的 colors、spacing、breakpoints 等。
当你在 uno.config.ts 中写 presets: [presetUno()],实际上是把 presetUno 返回的所有规则、变体、主题等"合并"到 UnoCSS 的运行时配置中。
一行代码,获得数百个工具类的能力。这就是预设的价值。
2.2 多预设叠加
presets 是一个数组,意味着你可以同时启用多个预设:
ts
presets: [
presetUno(),
presetAttributify(),
presetIcons(),
]多个预设的规则、变体、主题会按顺序合并。后面预设的规则追加到前面预设的规则列表末尾,主题配置进行深度合并(后者覆盖前者同名字段),变体同样追加合并。
这种设计让你可以像搭积木一样组合能力:从一个"基础预设"出发(如 presetUno),叠加"功能预设"(如 presetIcons、presetAttributify),最后叠加"项目级预设"(自己封装的业务规则)。
2.3 预设的工程价值
从工程角度看,预设解决了几个问题。
首先是能力模块化。不同功能放在不同预设中,按需启用。不用图标?不装 presetIcons。
其次是配置复用。团队可以封装内部预设,在多个项目中共享。统一的设计 token、统一的快捷方式,一处定义,多处复用。
最后是版本管理。预设可以独立发布为 npm 包,随时升级或回退。设计系统改了颜色?升级预设版本就行。
后续章节"玩转预设"和"创建你自己的预设"会深入展开预设的选择与编写。
3. 变体(Variants):类名的语义修饰层
规则和预设讲完了,现在要问一个新问题:hover:bg-blue-600 里的 hover: 是怎么工作的?
这就是变体的作用。
3.1 变体做了什么?
当我们写 hover:bg-blue-600 时,实际上 hover: 是变体前缀,bg-blue-600 是原始类名。
变体的作用分三步:首先识别并剥离前缀(hover:),然后将原始类名(bg-blue-600)交给规则匹配生成基础 CSS,最后在生成的 CSS 外层包裹相应的选择器或条件。
例如:
css
/* bg-blue-600 的原始 CSS */
.bg-blue-600 { background-color: #2563eb; }
/* hover:bg-blue-600 经过变体处理后 */
.hover\:bg-blue-600:hover { background-color: #2563eb; }有没有感觉到这个设计的巧妙? 变体不改变规则本身,只是给规则的输出"套了一层"。
3.2 常见变体类型
UnoCSS 的变体大致分为几类。
伪类变体包括 hover:、focus:、active:、disabled: 等,生成 :hover、:focus 等伪类选择器。
伪元素变体包括 before:、after:,生成 ::before、::after 伪元素选择器。
响应式变体包括 sm:、md:、lg:、xl:、2xl:,生成对应断点的媒体查询。
暗色模式变体就是 dark:,生成 .dark 类选择器或 @media (prefers-color-scheme: dark) 媒体查询。
父/兄弟选择器变体包括 group-hover:、peer-focus: 等,生成依赖父元素或兄弟元素状态的选择器。
3.3 变体可以组合
这里有一个强大的特性:变体可以链式组合。
html
<button class="md:hover:bg-blue-600">
...
</button>这表示在 md 断点及以上(≥768px),当元素处于 hover 状态时,应用 bg-blue-600 的背景色。生成的 CSS 类似:
css
@media (min-width: 768px) {
.md\:hover\:bg-blue-600:hover {
background-color: #2563eb;
}
}变体组合的顺序通常是响应式在前,然后是状态(hover/focus 等),最后是其他修饰。UnoCSS 会按照变体在类名中出现的顺序,依次"剥离"并"包裹",最终生成嵌套的选择器或媒体查询。
3.4 变体的实现机制
从实现角度看,变体是一个转换函数。它的 match 函数接收类名字符串,尝试识别并剥离变体前缀。如果命中,返回剩余类名加上如何包裹选择器的信息;如果未命中,返回 undefined,继续尝试下一个变体。
这种设计让变体的扩展变得非常灵活:你可以编写自定义变体支持项目特有的状态或条件,也可以修改现有变体的行为(例如调整 dark: 的实现方式)。
后续章节"精通变体"会展开变体的高级用法与自定义。
4. 三者的协作关系
现在我们把规则、预设、变体串起来,看看它们是如何协作的。
4.1 处理流程
当 UnoCSS 扫描到源码中的一个类名(如 md:hover:text-red-500)时,处理流程大致如下:
首先是变体解析阶段。识别 md: 后剥离并记录响应式断点,然后识别 hover: 后剥离并记录伪类,剩余部分是 text-red-500。
然后是规则匹配阶段。用 text-red-500 遍历所有规则,命中动态规则后生成 { color: #ef4444 }。
接着是变体包裹阶段。先包裹 :hover 伪类,再包裹 @media (min-width: 768px)。
最后输出 CSS,大致是 @media (min-width: 768px) { .md\:hover\:text-red-500:hover { color: #ef4444; } }。
这个流程说明了一件重要的事:变体和规则是正交的。变体负责"在什么条件下应用",规则负责"应用什么样式"。两者各司其职,互不干扰。
4.2 配置优先级
在 uno.config.ts 中,你可以同时在顶层和预设中定义规则、变体、主题。合并优先级是顶层配置优先级最高,多个预设按数组顺序后者覆盖前者。
这意味着如果你想覆盖某预设的规则,在顶层 rules 中定义即可。如果你想调整主题的某个 token,在顶层 theme 中覆盖。
4.3 调试技巧
当类名行为不符合预期时,排查思路如下。
首先确认变体是否被正确识别。检查变体前缀拼写(hover: 而不是 Hover:),检查变体是否在当前预设中启用。
其次确认规则是否命中。使用 UnoCSS Inspector 查看某个类名匹配到了哪条规则,检查规则的正则是否覆盖了当前类名模式。
最后确认主题配置。某些动态规则依赖 theme 中的值(如 text-red-500 依赖 theme.colors.red.500),如果主题中缺少对应值,规则可能不生成任何 CSS。
Inspector 工具会在后续"生态与工具链"部分详细介绍。
5. 实例:从类名到 CSS 的完整路径
我们通过一个具体例子,把本章所有概念串联起来。
5.1 场景
假设在组件中使用以下类名:
html
<div class="sm:dark:hover:bg-gray-800/50">
...
</div>这个类名看起来很复杂,但按照我们学到的知识,可以一层层拆解。
5.2 分解
这个类名包含 sm: 是响应式变体(断点 640px),dark: 是暗色模式变体,hover: 是伪类变体,bg-gray-800/50 是原始类名(背景色 gray-800,透明度 50%)。
5.3 处理过程
在变体解析阶段,从 sm:dark:hover:bg-gray-800/50 开始,剥离 sm: 后记录 @media (min-width: 640px),剥离 dark: 后记录 .dark 选择器,剥离 hover: 后记录 :hover 伪类,剩余 bg-gray-800/50。
在规则匹配阶段,bg-gray-800/50 命中动态规则,从主题中获取 gray-800 对应的颜色值 #1f2937,然后生成带透明度的背景色 { background-color: rgb(31 41 55 / 50%); }。
在变体包裹阶段,生成最终的 CSS:
css
@media (min-width: 640px) {
.dark .sm\:dark\:hover\:bg-gray-800\/50:hover {
background-color: rgb(31 41 55 / 50%);
}
}5.4 这个例子说明了什么?
一个类名,多重条件:响应式 + 暗色模式 + 交互状态,全部压缩在一个类名中。
声明式描述:开发者只需声明"我要什么效果",UnoCSS 负责生成正确的 CSS。
零冗余:只有这一个类名被使用时,才会生成这一段 CSS。
有没有感觉到这种设计的表达力? 用传统方式实现同样效果,你可能需要写好几行 CSS,还要注意媒体查询的嵌套和选择器的正确性。
6. 小结:建立心智模型
本章我们系统梳理了 UnoCSS 的三个核心抽象。
规则是最小构建单元,定义类名到 CSS 的映射。它分为静态规则(一一对应)和动态规则(正则匹配加参数提取)。
预设是规则、变体、主题的集合,实现能力的模块化与复用。多预设可以叠加组合,顶层配置优先级最高。
变体是类名前缀的解析与选择器包裹,支持条件样式。变体可以链式组合,与规则正交工作。
这三者的协作关系是:类名进入后先经过变体解析,然后进行规则匹配,再进行变体包裹,最后输出 CSS。
建立这个心智模型后,后续章节的内容会更容易理解:深入规则章节会讲如何编写静态和动态规则、如何访问主题;玩转预设章节会讲 preset-uno、preset-wind 等预设的差异与选择;精通变体章节会讲响应式、伪类、逻辑组合等变体的高级用法。
下一章,我们将深入规则系统,学习如何编写自己的规则,以及如何利用规则系统解决实际工程问题。