airi 项目中的 UnoCSS Rules 完全指南:从静态工具类到符号级自定义规则
UnoCSS 是 airi 全栈仓库中几乎所有前端应用(stage-web、stage-pocket、stage-tamagotchi、stage-ui 组件库等)所依赖的即时原子化 CSS 引擎,而 Rules(规则)正是其生成一切 CSS 工具类的核心机制:它定义了 m-1、bg-dotted-[neutral-200/60] 这类类名与最终 CSS 之间的映射。本文以官方技能文档 core-rules.md 为骨架,结合 airi 仓库根目录 uno.config.ts 与各应用的配置源码,完整讲解静态规则、动态规则、CSS 回退值、特殊符号、多选择器规则、完全控制规则、规则排序与合并等全部主题。读完后,你将掌握如何阅读、扩展 airi 中现有的 UnoCSS 规则体系,并能独立为项目编写符合最佳实践的自定义规则。
Rules 在 UnoCSS 中的位置:一切工具类的来源
UnoCSS 的设计哲学是「核心无观点(un-opinionated)」,它本身不内置任何工具类,所有 CSS 工具类都由 presets(预设) 提供,而预设的本质正是批量注册规则。在 airi 根目录 uno.config.ts 的 sharedUnoConfig() 中可以看到这一架构的直接体现:
presetWind3()注册了 Tailwind CSS v3 / Windi CSS 兼容的整套内置规则(m-*、p-*、bg-*等);presetAttributify()、presetTypography()、presetIcons()、presetScrollbar()、presetChromatic()继续追加各自的规则;- 配置底部的
rules: [...]数组则是为 airi 项目定制化新增的规则。
因此理解 Rules 的两种形态(静态与动态)与三条输出通路(CSS 对象、2D 数组、模板字符串),就等于掌握了 UnoCSS 可扩展性的全部底层原理。
静态规则:最简单的类名到 CSS 映射
静态规则是「类名 → CSS 声明对象」的简单映射,直接以元组形式写在 rules 数组中:
rules: [
['m-1', { margin: '0.25rem' }],
['font-bold', { 'font-weight': 700 }],
]
在模板中使用 <div class="m-1"> 时,UnoCSS 会生成:
.m-1 { margin: 0.25rem; }
airi 仓库中就有这样真实的静态规则案例。在 apps/stage-web/uno.config.ts 中,stage-web 应用定义了一个用于关闭过渡动画的静态规则:
rules: [
['transition-colors-none', {
'transition-property': 'color, background-color, border-color, text-color',
'transition-duration': '0s',
}],
],
关键注意事项(原文档重点强调):CSS 属性名必须使用连字符语法(font-weight 而非 fontWeight),且含连字符的属性名在对象字面量中必须加引号,否则会被 JavaScript 解析为减法表达式。这一约定在 airi 的配置中也被严格遵守,例如根配置中的 '-webkit-mask-image' 同样带引号。
动态规则:用正则表达式批量生成工具类
静态规则无法覆盖无限变化的取值(如 m-1 到 m-100)。动态规则用 RegExp 匹配器 + 函数体 解决这个问题:
rules: [
// 匹配 m-1、m-2、m-100 等
[/^m-(\d+)$/, ([, d]) => ({ margin: `${d / 4}rem` })],
// 可访问 theme 与上下文
[/^p-(\d+)$/, (match, ctx) => ({ padding: `${match[1] / 4}rem` })],
]
函数接收两个参数:
- RegExp 匹配结果(通过解构
[, d]快速取出捕获组); - 上下文对象(ctx),包含
theme、symbols等属性,用于读取设计令牌或访问符号。
airi 中的真实动态规则:mask、bg-dotted 与 drag-region
airi 根目录 uno.config.ts 中注册了三个极具代表性的动态规则,是理解动态规则的最佳实战素材:
rules: [
// 1. 从带下划线的参数生成 mask-image,如 mask-[linear-gradient(...)]
[/^mask-\[(.*)\]$/, ([, suffix]) => ({ '-webkit-mask-image': suffix.replace(/_/g, ' ') })],
// 2. 生成点阵背景,依赖 theme 与颜色解析工具
[/^bg-dotted-\[(.*)\]$/, ([, color], { theme }) => {
const parsedColor = parseColor(color, theme)
return {
'background-image': `radial-gradient(circle at 1px 1px, ${colorToString(parsedColor?.cssColor ?? parsedColor?.color ?? color, 'var(--un-background-opacity)')} 1px, transparent 0)`,
'--un-background-opacity': parsedColor?.cssColor?.alpha ?? parsedColor?.alpha ?? 1,
}
}],
// 3. Electron 窗口拖拽区域
[/drag-region/, () => ({ 'app-region': 'drag' })],
],
这三个规则分别演示了动态规则的不同能力维度:
mask-[...]展示了 参数捕获与字符串变换:将类名中的_还原为空格,用于构造 mask-image;bg-dotted-[...]展示了 与 theme 的深度联动:通过parseColor(color, theme)解析形如neutral-200/60的颜色参数(含透明度),再配合colorToString生成radial-gradient点阵背景,并把解析出的透明度写入 CSS 自定义属性--un-background-opacity。这正是动态规则可以「读取设计系统、输出复杂 CSS」的最佳例证;drag-region是一个无捕获组的正则规则,无论类名是drag-region还是drag-region-anything都会命中,为 Electron 应用输出app-region: drag,让该元素可作为窗口拖拽手柄。
动态规则在组件中的实际使用
这些自定义规则并非摆设,它们在 stage-ui 组件库中被真实消费。例如 packages/stage-ui/src/components/menu/icon-item.vue 中通过 --at-apply 指令使用了点阵背景规则:
--at-apply: 'bg-dotted-[neutral-200/60] hover:bg-dotted-[primary-300/50] dark:bg-dotted-[neutral-700/25] dark:hover:bg-dotted-[primary-200/20]';
可以看到 bg-dotted-[...] 不仅能与 hover:、dark: 变体自由组合,还能接受 neutral-200/60 这类「带透明度的语义色」参数——动态规则与 变体(Variants) 的协同能力在这里体现得淋漓尽致。
CSS 回退值:用 2D 数组保证浏览器兼容
当同一属性需要输出多个值做浏览器回退时,规则函数可以返回 2D 数组:
rules: [
[/^h-(\d+)dvh$/, ([_, d]) => [
['height', `${d}vh`],
['height', `${d}dvh`],
]],
]
生成的 CSS 会按数组顺序依次输出,让不支持 dvh 的旧浏览器回退到 vh:
.h-100dvh { height: 100vh; height: 100dvh; }
这种模式适用于所有「新单位/新特性 + 旧回退」的场景,例如 svh/lvh、color-mix() 等渐进增强型 CSS 特性。
特殊符号(symbols):从 @unocss/core 精细控制 CSS 输出
当规则需要突破「对象键值」的局限时,可以引入 @unocss/core 的 symbols 常量。最常见的用途是把 @supports 之类的包裹器挂到属性上:
import { symbols } from '@unocss/core'
rules: [
['grid', {
[symbols.parent]: '@supports (display: grid)',
display: 'grid',
}],
]
可用符号一览(完整表格)
| Symbol | 作用说明 |
|---|---|
symbols.parent |
父级包裹器(如 @supports、@media) |
symbols.selector |
修改选择器的函数 |
symbols.layer |
设置该规则输出的 UnoCSS 层 |
symbols.variants |
变体处理器数组 |
symbols.shortcutsNoMerge |
在 shortcuts 中禁用合并 |
symbols.noMerge |
禁用规则合并 |
symbols.sort |
覆盖排序顺序 |
symbols.body |
完全掌控 CSS body 输出 |
这些符号与 层(Layers) 概念紧密结合:UnoCSS 默认有 shortcuts、utilities 等层,symbols.layer 可以像 rules 元组第三项 { layer: 'utilities' } 一样把规则定向到指定层,从而精确控制输出顺序。
多选择器规则:用生成器函数产出多条 CSS
当一条工具类需要生成多条(通常是不同选择器下的)CSS 规则时,把规则函数写成 generator(生成器函数),用 yield 逐条产出:
rules: [
[/^button-(.*)$/, function* ([, color], { symbols }) {
yield { background: color }
yield {
[symbols.selector]: selector => `${selector}:hover`,
background: `color-mix(in srgb, ${color} 90%, black)`
}
}],
]
使用 button-red 会同时生成两条规则:
.button-red { background: red; }
.button-red:hover { background: color-mix(in srgb, red 90%, black); }
这一写法非常适用于「一个类名 + 一组配套伪类/后代选择器」的场景,例如按钮、卡片等需要常态与悬停态同时定义的组件类。注意这里用到了 color-mix() 现代 CSS 特性,与上文的回退值技巧配合可以获得更稳的兼容性。
完全控制规则:直接返回 CSS 字符串
当对象表达力不足(例如需要输出 ::after 伪元素、@media 嵌套),可以返回原始 CSS 字符串,获得对输出的完全控制:
import { defineConfig, toEscapedSelector as e } from 'unocss'
rules: [
[/^custom-(.+)$/, ([, name], { rawSelector, theme }) => {
const selector = e(rawSelector)
return `
${selector} { font-size: ${theme.fontSize.sm}; }
${selector}::after { content: 'after'; }
@media (min-width: ${theme.breakpoints.sm}) {
${selector} { font-size: ${theme.fontSize.lg}; }
}
`
}],
]
这里有两个关键细节:
- 必须用
toEscapedSelector(即e)处理rawSelector,因为工具类名中可能含有:、/、[等需要 CSS 转义的字符(如custom-hover:会生成hover\:选择器); - 可以自由读取
theme中的设计令牌(theme.fontSize.sm、theme.breakpoints.sm),实现「响应式 + 语义化」的输出。
⚠️ 原文档明确警告:完全控制规则不兼容变体(如 hover:custom-red 不会生效),因为变体需要修改选择器,而返回字符串时选择器已被写死。
symbols.body:在保留变体支持的同时输出自定义 CSS
若既要自定义 CSS 结构、又希望变体仍能工作,应改用 symbols.body 而非字符串返回。body 中可以用 & 引用当前选择器:
rules: [
['custom-red', {
[symbols.body]: `
font-size: 1rem;
&::after { content: 'after'; }
& > .bar { color: red; }
`,
[symbols.selector]: selector => `:is(${selector})`,
}]
]
symbols.selector 负责把变体产生的选择器包装进 :is(),symbols.body 则用 & 作为当前选择器的占位符。这样 hover:custom-red 这类变体组合依旧可用——这是「完全控制规则」与「变体支持」二者不可兼得时推荐的替代方案。
规则排序与优先级
- 后声明的规则优先级更高:UnoCSS 按
rules数组顺序处理,后面的规则会覆盖前面规则的同名属性输出; - 动态规则在组内按字母序输出:同一组动态规则匹配出的多个类,最终 CSS 会按工具类名字母序排列。
结合 core-layers.md 可知,跨层级的顺序由 layers 配置决定(如 components: -1、default: 1、utilities: 2),而层内排序遵循上述规则。airi 根配置将自定义规则放在 presets 之后声明,正是为了确保 mask-[...]、bg-dotted-[...]、drag-region 这些定制类拥有覆盖预设规则同属性输出的更高优先级。
规则合并:相同 CSS body 的去重与 noMerge
UnoCSS 会自动合并拥有相同 CSS body 的规则。以下标记:
<div class="m-2 hover:m2">
会合并生成一条选择器列表:
.hover\:m2:hover, .m-2 { margin: 0.5rem; }
这一机制显著减小了最终 CSS 体积。若某些规则不希望被合并(例如依赖声明顺序才能生效的规则),用 symbols.noMerge 显式关闭合并。与之相关的还有 shortcuts 中的 symbols.shortcutsNoMerge——短横线(Shortcuts) 展开的多个工具类默认参与合并,可用该符号在特定 shortcut 上禁用。需要提醒的是,所有规则的合并/去重都发生在构建时,而非运行时,因此不会产生任何额外的浏览器开销。
延伸:与 Shortcuts、Variants 的协同
Rules 是 UnoCSS 的最小单元,但它通常与仓库技能文档中的另外两个核心概念搭配使用:
- Shortcuts:把多条规则组合成单个语义化类名(如
btn: 'py-2 px-4 font-semibold rounded-lg shadow-md'),并支持正则动态 shortcut([/^btn-(.*)$/, ([, c]) => \bg-${c}-400 ...`])。Shortcuts 在构建时展开为普通规则,因此天然兼容全部变体(hover:btn、dark:btn`); - Variants:变体是规则的「前置处理器」,把
hover:m-2逐步拆解后交给规则匹配,再把变体转换应用到最终选择器上。上文bg-dotted-[...]与hover:、dark:的自由组合正是这一协同机制的实战证明。
三者组合起来,可以在 airi 的每个应用(stage-web、stage-pocket、stage-tamagotchi)中构建出「语义化类名 + 无限取值 + 状态响应」的完整工具类体系。
小结:在 airi 中实践规则开发
回顾 airi 仓库中的真实证据,可以总结出在项目中编写规则的实用清单:
- 静态规则适用于固定取值,写法最简,见 apps/stage-web/uno.config.ts 的
transition-colors-none; - 动态规则适用于无限取值与 theme 联动,见 uno.config.ts 的
mask-[...]、bg-dotted-[...]、drag-region; - 需要
@media/@supports包裹时用symbols.parent;需要多条选择器时用生成器函数 +symbols.selector; - 需要完全自定义 CSS 时返回字符串,但注意其与变体不兼容;需要变体兼容则改用
symbols.body; - 始终遵守「连字符属性名 + 加引号」的书写约定,动态规则中处理颜色等主题令牌时,可参考
parseColor+colorToString(来自unocss/preset-mini)的标准解析链路; - 依赖 CSS 属性书写顺序生效的规则,记得用
symbols.noMerge关闭合并。
更多周边主题可继续阅读同目录下的 core-config.md(配置总览)、core-theme.md(设计令牌)、core-safelist.md(强制包含类)与 preset-mini.md(最小预设内置规则),形成对 UnoCSS 规则体系的完整认知。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00