UnoCSS transformer-directives 详解:@apply、@screen 与 theme()/icon() CSS 指令转换原理与实战
【免费下载链接】unocssThe instant on-demand atomic CSS engine.项目地址: https://gitcode.com/GitHub_Trending/un/unocss
本文基于 UnoCSS 官方文档 docs/transformers/directives.md 及@unocss/transformer-directives包的源码实现编写。读完本文后,你将掌握在 CSS 文件中使用@apply、@screen、theme()和icon()四类指令的完整用法、可配置选项(如applyVariable、throwOnMissing),以及该 transformer 基于 css-tree AST 与 MagicString 的底层转换机制。
UnoCSS 的核心用法是在模板中写原子类,但很多场景下我们仍然需要在 CSS 文件里维护自定义样式。@unocss/transformer-directives就是在构建期介入 CSS 源码、把类似 Tailwind 语法的指令“编译”成真实 CSS 的源码转换器:@apply展开原子类、@screen把断点名转成媒体查询、theme()读取主题配置值、icon()把图标名转成内联 SVG data URI。
安装与启用
按包管理器选择安装命令,将其作为开发依赖加入项目:
pnpm add -D @unocss/transformer-directives # 或 yarn add -D @unocss/transformer-directives npm install -D @unocss/transformer-directives bun add -D @unocss/transformer-directives然后在uno.config.ts的transformers数组中注册:
import transformerDirectives from '@unocss/transformer-directives' import { defineConfig } from 'unocss' export default defineConfig({ // ... transformers: [ transformerDirectives(), ], })该 transformer 也内置在聚合包unocss中,可以直接从那里导入,入口导出 中有一行export { default as transformerDirectives } from '@unocss/transformer-directives':
import { transformerDirectives } from 'unocss'选项一览
从源码中的 TransformerDirectivesOptions 类型定义 可以看到完整可配置项,比文档正文描述更完整:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
applyVariable | false \| string \| string[] | ['--at-apply', '--uno-apply', '--uno'] | 把哪些 CSS 自定义属性当作@apply指令处理;传false关闭该特性 |
throwOnMissing | boolean | true | theme()/icon()引用不存在的值时是否抛错 |
enforce | 'pre' \| 'post' | - | 控制 transformer 的执行阶段(pre/post) |
其中applyVariable的默认值在 transform.ts 的resolveApplyVariables()中解析:未显式配置时取上述三个别名,配置false时归一化为空数组。
转换的触发条件
在 index.ts 中,transformer 定义了两层过滤:
idFilter:使用@unocss/core导出的cssIdRE,只对 CSS 类文件生效;codeFilter:代码中包含@apply、@screen、theme(、icon(或任一applyVariable别名时才真正执行转换。
仓库测试 test/transformer-directives.test.ts 中的source filter用例验证了这一行为:普通样式.button { color: red }被过滤(返回false),而含@apply或theme("colors.red")的代码会命中;自定义别名applyVariable: '--custom-apply'同样能被codeFilter检测。这种“先粗筛再解析”的设计保证了不相关文件的 CSS 不会付出 AST 解析的成本。
@apply:在 CSS 中展开原子类
最基础的用法是在任意选择器内用@apply引用 UnoCSS 工具类:
.custom-div { @apply text-center my-0 font-medium; }构建后会展开为真实 CSS 声明:
.custom-div { margin-top: 0rem; margin-bottom: 0rem; text-align: center; font-weight: 500; }--at-apply:兼容原生 CSS 语法的变体
@apply是 at-rule,部分严格的 CSS 工具链(校验器、格式化器)可能不认可它。为此 UnoCSS 提供“把 CSS 自定义属性当作 apply 指令”的替代写法:
.custom-div { --at-apply: text-center my-0 font-medium; }该特性默认开启,且默认识别三个别名:--at-apply、--uno-apply、--uno(这也是文档中@screen示例大量使用--uno: grid-cols-2的原因)。可通过选项定制或关闭:
transformerDirectives({ // 默认值 applyVariable: ['--at-apply', '--uno-apply', '--uno'], // 或者完全关闭: // applyVariable: false })需要包含:的规则必须加引号
当要应用的工具类包含变体(冒号)时,需要用引号包住整个值:
.custom-div { --at-apply: 'hover:text-red hover:font-bold'; /* 或者 */ @apply 'hover:text-red hover:font-bold'; }对于@apply,引号是可选的——这是为了兼容部分格式化器会自动加/去引号的格式行为。从 apply.ts 的removeQuotes()可以看到,无论输入是否带引号,解析前都会先剥离成对的首尾引号,因此两种写法等价。
源码视角:@apply 是如何展开的
@apply的核心逻辑在 apply.ts 的parseApply()中,可以归纳为五步:
- 提取工具类串:从
@apply的 prelude 或自定义属性声明中取原始文本(css-tree 会对声明值做表达式解析,所以这里直接用code.original.slice()按 AST 位置截取原文),去引号、去注释后按空白拆分; - 展开 variant 分组:先调用
expandVariantGroup()展开v-hover:...之类的简写; - 解析 token:对每个类名调用
uno.parseToken(),把原子类解析为带位置信息的StringifiedUtil元组,再按来源位置排序,保证展开后的声明顺序稳定; - 合并与落位:普通声明直接拼接到
@apply语句之后;而带父选择器(parent,如hover:产生的嵌套)、带特殊选择器的工具类,则生成独立规则块插入当前规则之后;layer: 'properties'的产物(如 opacity 的@property注册)会统一插入到文件头部; - 删除原指令:用 MagicString 的
code.remove()移除@apply语句本身,并清理因此产生的空规则块(见 transform.ts 末尾的正则清理逻辑)。
另外值得注意的是handleApply()对Raw子节点会递归调用transformDirectives,意味着@media、@supports等嵌套容器内的@apply也能被正确处理——测试用例中body { @apply sm:lg:md:xs:w-[40em]; }展开为多层嵌套@media正是这条递归路径的验证。
@screen:按断点名称生成媒体查询
@screen允许用断点名称代替手写媒体查询,断点来自 theme.breakpoints 配置:
.grid { --uno: grid grid-cols-2; } @screen xs { .grid { --uno: grid-cols-1; } } @screen sm { .grid { --uno: grid-cols-3; } } /* ... */转换结果:
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } @media (min-width: 320px) { .grid { grid-template-columns: repeat(1, minmax(0, 1fr)); } } @media (min-width: 640px) { .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } } /* ... */如果断点名在 theme 中不存在,screen.ts 会直接抛出breakpoint xxx not found错误,属于快速失败设计,方便在开发期暴露拼写错误。
@screen lt-:小于断点
前缀lt表示“小于该断点”,生成的媒体查询使用max-width:
.grid { --uno: grid grid-cols-2; } @screen lt-xs { .grid { --uno: grid-cols-1; } } @screen lt-sm { .grid { --uno: grid-cols-3; } }.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } @media (max-width: 319.9px) { .grid { grid-template-columns: repeat(1, minmax(0, 1fr)); } } @media (max-width: 639.9px) { .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }319.9px这样的上界由@unocss/rule-utils的calcMaxWidthBySize()计算(断点值减 0.1),与lt-变体生成的媒体查询保持一致。
@screen at-:精确落在断点区间
前缀at表示“恰好处于该断点区间”,生成min-width与max-width组合查询,上界取下一个断点减 0.1:
.grid { --uno: grid grid-cols-2; } @screen at-xs { .grid { --uno: grid-cols-1; } } @screen at-xl { .grid { --uno: grid-cols-3; } } @screen at-xxl { .grid { --uno: grid-cols-4; } }.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } @media (min-width: 320px) and (max-width: 639.9px) { .grid { grid-template-columns: repeat(1, minmax(0, 1fr)); } } @media (min-width: 1280px) and (max-width: 1535.9px) { .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } } @media (min-width: 1536px) { .grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }注意最后一段at-xxl没有上界——当该断点是 theme 中最后一个时,源码中variantEntries[idx + 1]为空,就只输出min-width,行为上与裸@screen xxl等价。
从源码结构看,screen.ts 中还有一个版本适配细节:它会检测 presets 中是否包含@unocss/preset-wind4,是则从theme.breakpoint(单数)读取断点,否则从theme.breakpoints(复数)读取。也就是说如果你切换了 preset 大版本,需要确认断点配置键名匹配,否则@screen会因找不到断点而报错。
theme():用点语法读取主题配置
theme()函数让你以点语法(dot notation)访问theme配置中的任意值:
.btn-blue { background-color: theme('colors.blue.500'); }编译结果(以当前仓库 preset 的调色板为准):
.btn-blue { background-color: #3b82f6; }带默认值的 theme()
文档正文没有展开,但源码 functions.ts 与测试用例明确支持第二个参数作为回退值:当主题键不存在时,使用逗号之后的默认值而不是抛错:
.btn { color: theme('not.exists.color', #fff); font-family: theme('not.exists.font', 'ui-sans-serif', 'system-ui'); }会分别输出color: #fff;与font-family: "ui-sans-serif", "system-ui";。相对地,缺键且无默认值(theme('not.exists'))、空默认值(theme('not.exists', ))、或两个参数间缺少逗号都会抛出错误——这些行为都有对应的测试断言,见 test/transformer-directives.test.ts 的theme() with defaults用例。
throwOnMissing(默认true)控制“找不到值时”是抛错还是静默保留;若设置了默认值,则即使throwOnMissing为true也不会抛错,而是采用默认值。
icon():把图标工具类编译为 SVG data URI
icon()把图标名称转成具体的 SVG 图标,用于background-image等场景:
.icon { background-image: icon('i-carbon-sun'); }编译后是一个内联的 data URI:
.icon { background-image: url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 32 32' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M16 12.005a4 4 0 1 1-4 4a4.005 4.005 0 0 1 4-4m0-2a6 6 0 1 0 6 6a6 6 0 0 0-6-6M5.394 6.813L6.81 5.399l3.505 3.506L8.9 10.319zM2 15.005h5v2H2zm3.394 10.193L8.9 21.692l1.414 1.414l-3.505 3.506zM15 25.005h2v5h-2zm6.687-1.9l1.414-1.414l3.506 3.506l-1.414 1.414zm3.313-8.1h5v2h-5zm-3.313-6.101l3.506-3.506l1.414 1.414l-3.506 3.506zM15 2.005h2v5h-2z'/%3E%3C/svg%3E"); }前提依赖:icon()依赖@unocss/preset-icons,会读取该 preset 的配置(前缀、集合、缩放等),请确保已添加该 preset。从 icon.ts 的实现看,它会从当前 Uno 配置的 presets 中查找@unocss/preset-icons,复用其api与IconsOptions(包括scale、prefix、collections、customizations等);若找不到 preset,则打印警告 “@unocss/preset-icons not found, icon() directive will be keep as-is” 并保留原样,不会报错——这与throwOnMissing的严格模式不同,属于“优雅降级”行为。
自定义图标颜色
图标默认使用currentColor作为填充色。第二个参数可以指定自定义颜色,颜色值本身还支持theme()引用:
.icon { background-image: icon('i-carbon-moon', '#fff'); background-image: icon('i-carbon-moon', 'theme("colors.red.500")'); /* 使用主题色 */ }编译后currentColor会被替换为对应颜色(URL 编码后):
.icon { background-image: url("data:image/svg+xml;utf8,...%3Cpath fill='%23fff' d='M13.503 5.414...'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml;utf8,...%3Cpath fill='%23ef4444' d='M13.503 5.414...'/%3E%3C/svg%3E"); }实现上,functions.ts 的icon分支会先对第二个参数执行transformThemeFn()(因此theme("colors.red.500")这种嵌套字符串也能解析成真实色值),再encodeURIComponent后传给transformIconString(),后者在编码出的 SVG 中全局替换currentColor。
底层机制小结
综合 transform.ts 的实现,整个转换流程是:
- 快速判定:先用字符串
includes判断是否含@apply/@screen/theme(/icon(/自定义属性别名,不含则直接返回,零解析开销; - AST 解析:用 css-tree 的
parse()解析(开启parseCustomProperty,这正是--at-apply能被识别为声明的前提,同时关闭parseAtrulePrelude以避免@screen等 at-rule 的参数被误解析); - 遍历处理:
walk()遍历 AST,Atrule.screen交给handleScreen(),Function节点(theme/icon)交给handleFunction(),Rule交给handleApply(); - 原地改写:全程通过 MagicString 的
overwrite/remove/appendLeft等做增量改写,保留其余源码与映射信息,因此 sourcemap 可以正确回溯; - 递归与清理:
handleApply对Raw节点(嵌套 at-rule 内部)递归调用自身,最后一轮再清除因指令移除而产生的空规则块。
这种“AST 定位 + MagicString 增量改写”的组合,使得@apply展开、@screen替换、theme()值替换可以混合出现在同一份 CSS 中并正确共存,同时输出仍是可被任意 CSS 消费方理解的普通 CSS。
参考路径
- 官方文档:docs/transformers/directives.md
- 包入口与过滤器:packages-presets/transformer-directives/src/index.ts
- 选项类型定义:packages-presets/transformer-directives/src/types.ts
- 主转换流程:packages-presets/transformer-directives/src/transform.ts
@apply展开:packages-presets/transformer-directives/src/apply.ts@screen处理:packages-presets/transformer-directives/src/screen.tstheme()/icon()处理:packages-presets/transformer-directives/src/functions.ts、packages-presets/transformer-directives/src/icon.ts- 行为测试(含断点、theme 默认值、codeFilter):test/transformer-directives.test.ts
- 主题/断点配置:docs/config/theme.md
【免费下载链接】unocssThe instant on-demand atomic CSS engine.项目地址: https://gitcode.com/GitHub_Trending/un/unocss
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考