news 2026/9/13 6:13:19

UnoCSS transformer-directives 详解:@apply、@screen 与 theme()/icon() CSS 指令转换原理与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
UnoCSS transformer-directives 详解:@apply、@screen 与 theme()/icon() CSS 指令转换原理与实战

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@screentheme()icon()四类指令的完整用法、可配置选项(如applyVariablethrowOnMissing),以及该 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.tstransformers数组中注册:

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 类型定义 可以看到完整可配置项,比文档正文描述更完整:

选项类型默认值说明
applyVariablefalse \| string \| string[]['--at-apply', '--uno-apply', '--uno']把哪些 CSS 自定义属性当作@apply指令处理;传false关闭该特性
throwOnMissingbooleantruetheme()/icon()引用不存在的值时是否抛错
enforce'pre' \| 'post'-控制 transformer 的执行阶段(pre/post)

其中applyVariable的默认值在 transform.ts 的resolveApplyVariables()中解析:未显式配置时取上述三个别名,配置false时归一化为空数组。

转换的触发条件

在 index.ts 中,transformer 定义了两层过滤:

  • idFilter:使用@unocss/core导出的cssIdRE,只对 CSS 类文件生效;
  • codeFilter:代码中包含@apply@screentheme(icon(或任一applyVariable别名时才真正执行转换。

仓库测试 test/transformer-directives.test.ts 中的source filter用例验证了这一行为:普通样式.button { color: red }被过滤(返回false),而含@applytheme("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()中,可以归纳为五步:

  1. 提取工具类串:从@apply的 prelude 或自定义属性声明中取原始文本(css-tree 会对声明值做表达式解析,所以这里直接用code.original.slice()按 AST 位置截取原文),去引号、去注释后按空白拆分;
  2. 展开 variant 分组:先调用expandVariantGroup()展开v-hover:...之类的简写;
  3. 解析 token:对每个类名调用uno.parseToken(),把原子类解析为带位置信息的StringifiedUtil元组,再按来源位置排序,保证展开后的声明顺序稳定;
  4. 合并与落位:普通声明直接拼接到@apply语句之后;而带父选择器(parent,如hover:产生的嵌套)、带特殊选择器的工具类,则生成独立规则块插入当前规则之后;layer: 'properties'的产物(如 opacity 的@property注册)会统一插入到文件头部;
  5. 删除原指令:用 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-utilscalcMaxWidthBySize()计算(断点值减 0.1),与lt-变体生成的媒体查询保持一致。

@screen at-:精确落在断点区间

前缀at表示“恰好处于该断点区间”,生成min-widthmax-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)控制“找不到值时”是抛错还是静默保留;若设置了默认值,则即使throwOnMissingtrue也不会抛错,而是采用默认值。

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,复用其apiIconsOptions(包括scaleprefixcollectionscustomizations等);若找不到 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 的实现,整个转换流程是:

  1. 快速判定:先用字符串includes判断是否含@apply/@screen/theme(/icon(/自定义属性别名,不含则直接返回,零解析开销;
  2. AST 解析:用 css-tree 的parse()解析(开启parseCustomProperty,这正是--at-apply能被识别为声明的前提,同时关闭parseAtrulePrelude以避免@screen等 at-rule 的参数被误解析);
  3. 遍历处理walk()遍历 AST,Atrule.screen交给handleScreen()Function节点(theme/icon)交给handleFunction()Rule交给handleApply()
  4. 原地改写:全程通过 MagicString 的overwrite/remove/appendLeft等做增量改写,保留其余源码与映射信息,因此 sourcemap 可以正确回溯;
  5. 递归与清理handleApplyRaw节点(嵌套 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.ts
  • theme()/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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 6:12:47

dsPIC33CK SPI驱动MCP25625扩展CAN通道实战

简介:这是一份基于dsPIC33CK256MP506微控制器与MCP25625 CAN控制器的源码工程,面向嵌入式初学者、汽车电子开发者和需要快速验证CAN总线通信的工程师,提供通过SPI接口驱动MCP25625并完成初始化、消息收发与中断处理的完整示例。压缩包共67个文…

作者头像 李华
网站建设 2026/9/13 6:10:10

Linux不是操作系统,而是一套硬件调度协议

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 6:09:22

Odoo 怎么接入 Peppol 网络收发 BIS Billing 3.0 格式的发票

Odoo 怎么接入 Peppol 网络收发 BIS Billing 3.0 格式的发票 【免费下载链接】odoo Odoo. Open Source Apps To Grow Your Business. 项目地址: https://gitcode.com/GitHub_Trending/od/odoo 如果你的公司在 PEPPOL_LIST 列出的欧洲国家(奥地利、比利时、瑞…

作者头像 李华
网站建设 2026/9/13 6:08:15

Spring Boot事务实战:隔离级别、传播特性与失效排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 6:07:06

AI大模型岗位指南:从零基础到高薪就业

1. AI大模型岗位全景图:从技术栈到职业路径2023年成为AI大模型爆发的元年,行业对相关人才的需求呈现指数级增长。根据领英最新数据,全球AI大模型相关岗位同比增长320%,其中中国市场占比达45%。这个领域不再只是PhD们的游戏——经过…

作者头像 李华
网站建设 2026/9/13 6:06:45

CesiumForUnreal加载b3dm瓦片:tileset.json配置与地理坐标对齐指南

简介:本资源是一套专为Cesium for Unreal引擎适配的3D Tiles标准数据集,面向三维GIS开发工程师、Unreal引擎开发者及数字孪生项目实践者,解决在虚幻引擎中快速集成高精度地理三维模型的核心需求。压缩包共2000个文件,包含1543个JS…

作者头像 李华