Sass 提案解析:At-Rule 名称中的插值(Interpolation in At-Rule Names)
【免费下载链接】sassSass makes CSS fun!项目地址: https://gitcode.com/gh_mirrors/sa/sass
导读
本篇文章基于 Sass 官方提案 accepted/at-rule-interpolation.md(Draft 1,对应 Issue #429),完整解读"在 at-rule(@规则)名称中使用#{}插值"这一语法特性的设计动机、文法定义与运行时语义。读完本文,你将掌握:为什么插值名称的 at-rule 一律按未知规则(UnknownAtRule)处理、唯一的例外@keyframes如何被特殊照顾、该特性如何用于动态生成带厂商前缀的 at-rule,以及它与 Sass 现有语法(InterpolatedIdentifier、InterpolatedValue)之间的承接关系。
提案背景与目标
在 Sass 中,插值(interpolation)是#{}语法,允许把 SassScript 表达式的求值结果拼接到标识符、字符串、选择器等文本片段中。此前,插值可以出现在 at-rule 的值(value)部分,但 at-rule 的名称本身(即@之后的标识符)是固定的字面量。
本提案的目标是:允许任何 at-rule 在其名称中包含插值,例如:
@m#{$vendor}media { ... }其核心应用场景是动态生成带厂商前缀的 at-rule 名称。由于浏览器厂商常通过-webkit-、-moz-、-ms-、-o-等前缀扩展 CSS 特性,开发者希望用一段 Sass 代码统一输出多个前缀版本的规则,例如:
$prefixes: "-webkit-", "-moz-", ""; @each $prefix in $prefixes { @#{$prefix}keyframes fade-in { ... } }设计决策:插值名称统一按"未知 at-rule"处理
提案在 Design Decisions 一节明确了关键取舍:
任何 at-rule 都可以在名称中包含插值。插值会自动导致它被解析为未知 at-rule(unknown at-rule),即使插值解析后的名称是 Sass 已知的规则。唯一的例外是
@keyframes,它仍然允许包含10%之类的 keyframes 选择器子规则。
为什么不重新解析已知规则?
理论上,可以让 Sass 认识的 CSS at-rule(如@media、@supports)即使在名称由插值生成时也被特殊对待。但提案明确指出这会带来极大的实现复杂度:
- 实现需要具备在运行时重新解析这些 at-rule 值的能力;
- 收益不明确:at-rule 插值主要用于添加厂商前缀,而 Sass 有特殊运行时支持的
@media和@supports恰好不使用厂商前缀,因此没有为此付出复杂度的价值。
运行时行为
当一个 at-rule 被求值时:
- 先对其名称求值,产出一个不带引号的字符串(unquoted string),作为生成 at-rule 的名称;
- 再检查该生成名称是否是需要特殊运行时处理的 at-rule;
- 在运行时触发特殊处理的只有
@keyframes;@media、@supports等规则是在解析期(parse-time)被检测的。
由此得出一个直接推论:@m#{ed}ia会被当作未知 at-rule,而不会被当作媒体查询规则处理——因为@media的特殊语义绑定在解析期,而不是运行时。
语法:UnknownAtRule 产物的替换
本提案定义了UnknownAtRule产物的替换文法,与现有语法(见 spec/at-rules/unknown.md)相比,唯一区别是名称部分可以使用插值:
UnknownAtRule ::= '@' InterpolatedIdentifier InterpolatedValue? ('{' Statements '}')?文法要点:
@之后不允许出现空白;- 与所有语句一致,不带块(block)的 UnknownAtRule 必须用分号与其他语句分隔;
InterpolatedIdentifier与InterpolatedValue均为可插值产物(详见下文源码佐证)。
与现有 UnknownAtRule 文法的关系
在 spec/at-rules/unknown.md 中,当前规范对未知 at-rule 的文法定义为:
UnknownAtRule ::= '@' InterpolatedIdentifier¹ InterpolatedValue? Block? | `@function'² InterpolatedIdentifier³ InterpolatedValue? Block(其中 1:名称不得与 Sass 定义的其他 at-rule 重名;2:@function大小写不敏感匹配;3:必须以--开头。)
本提案即是在此基础上,明确"名称可以被插值",并进一步规定插值名称一律走未知规则路径。提案原文特别注明:"这与之前的语法相同,只是 at-rule 的名称可以被插值。"
InterpolatedIdentifier 与 InterpolatedValue
提案中引用的两个非终结符来自 Sass 核心语法(spec/syntax.md):
InterpolatedIdentifier ::= (<ident-token> | '-'? Interpolation) (Name | Interpolation)*其含义是:一个可插值标识符由一个 CSS ident-token(或可选的-加插值)开头,后跟任意多个 Name 片段或插值组成,各组成部分之间不允许出现空白。
InterpolatedValue对应 Sass 的InterpolatedAnyValue,与 CSS 的<any-value>一致,唯一区别是当解析到#{时,会解析一个必须紧跟}的 Expression。此外,spec/syntax.md 中的"Consuming an Interpolated Identifier"算法给出了解析器层面的具体步骤:从-#{、#{或普通标识符开始,随后循环消费#{、标识符码点或\转义,最终返回由字符串与表达式组成的组件序列。
运行时语义:求值、拼接与特殊分支
综合提案与 spec/at-rules/unknown.md 的语义描述,一个带插值名称的未知 at-rule 在运行时按以下流程执行:
- 求值名称:对
InterpolatedIdentifier求值,产出不带引号的字符串name; - 求值值:若存在
InterpolatedValue,对其求值得到value; - 构造 CSS 节点:创建一个名为
name、值为value的 CSS 未知 at-rule; - 定位父节点:取当前样式规则(current style rule)、keyframe 块或 at-rule 中最近的作为父节点;
- 有 Statements 时:
- 父节点不存在 → 追加到当前模块的 CSS 输出;
- 父节点是样式规则,且名称是
font-face或其去前缀名(unprefixed name)为keyframes→ 追加到当前模块的 CSS 输出; - 否则 → 先追加到父节点的父节点,再复制一个不含子节点的父节点副本追加到 css 中(该副本随后成为新的 current style rule);
- 然后逐个求值其子 Statements;
- 无 Statements 时:必要时先拆分当前父节点(splitting the current parent if necessary),再追加到父节点或当前模块的 CSS 输出。
其中关键的一步是第 5 条中的去前缀判断(unprefixed):它引用 spec/syntax.md 对 vendor prefix 的定义——前缀是-开头、后跟一个或多个非-码点、再跟-的子串,且-之后必须还有文本,剩余部分即"无前缀标识符"。
@keyframes:唯一的运行时特殊处理
为什么偏偏是@keyframes?因为它承载的子规则内容是选择器(如0%、100%、from、to),而这些内容只有在 keyframes 上下文里才合法。当@-webkit-keyframes之类的名称由插值生成后,运行时仍需将其内部内容按 keyframes 选择器解析。
在 spec/style-rules.md 的样式规则语义中可以看到这一分支:
- 若父节点是名称去掉厂商前缀后为
keyframes的未知 at-rule,则子选择器文本会按keyframe selector解析,追加一个 keyframe 块到父节点,并结束当前样式规则的求值; - 若父节点是 keyframe 块,则抛出错误(在 keyframes 内不允许再嵌套普通样式规则)。
这一机制保证了@#{$prefix}keyframes生成的@-webkit-keyframes、@-moz-keyframes等规则内部仍能正确识别10%、from这类 keyframes 选择器。
与 @media、@supports 的对比:解析期 vs 运行时
| 特性 | @media/@supports | 插值名称的未知 at-rule |
|---|---|---|
| 检测时机 | 解析期(parse-time) | 运行时(runtime) |
| 特殊运行时处理 | 有(如嵌套媒体查询提升到顶层) | 无(除@keyframes) |
| 厂商前缀 | 不使用 | 主要用途 |
| 插值名称结果 | @m#{ed}ia不被识别为媒体规则 | 一律按 UnknownAtRule 处理 |
以@media为例,Sass 为其提供了特殊支持:媒体查询会被解析两次(第一次在解析 Sass 样式表时允许包含 SassScript 与插值,第二次将求值结果作为纯 CSS 解析),以实现嵌套媒体查询向顶层提升等能力(见 spec/at-rules/media.md)。这些特殊支持全部建立在解析期识别@media的前提之上,因此@m#{ed}ia无法享受这些语义,只能落入未知 at-rule 的通用路径。
综合示例:动态生成厂商前缀规则
结合以上全部语义,一个完整的实战用例是把前缀抽成变量,循环输出多版本 at-rule:
$vendors: ("-webkit-", "-moz-", "-ms-", ""); @each $vendor in $vendors { @#{$vendor}keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } }编译行为分析:
- 每次循环,
@#{$vendor}keyframes的名称经插值求值,成为不带引号的字符串(如-webkit-keyframes); - 由于名称含插值,一律按未知 at-rule 处理;
- 运行时检查发现其去前缀名(
keyframes)命中唯一特殊分支,于是块内的from/to/10%等选择器按 keyframe selector 解析并正确生成 keyframe 块; - 最终输出四组独立的
@keyframes规则,分别带-webkit-、-moz-、-ms-前缀及无前缀版本。
再对比一个反例:若把@media写成@m#{ed}ia,由于媒体规则的特殊语义绑定在解析期,该写法只会被当作一个名为media的未知 at-rule 原样输出,不会触发嵌套媒体查询提升等 Sass 特有行为。
总结
本提案以最小文法改动(UnknownAtRule的名称支持InterpolatedIdentifier)为 Sass 解锁了 at-rule 名称插值能力,同时通过"插值名称一律视为未知 at-rule"的设计,将实现复杂度控制在可接受范围内:唯一的运行时特殊分支给了@keyframes,恰好覆盖了厂商前缀使用场景中最需要动态化的动画规则,而@media、@supports这类无需前缀的规则则完全不需要引入昂贵的运行时重解析机制。对于想要输出多前缀 CSS 的工具链与样式库作者而言,这是一个成本极低、收益明确的语法扩展。
延伸阅读(仓库内相关资料)
- 提案全文:accepted/at-rule-interpolation.md
- 未知 at-rule 的现行文法与语义:spec/at-rules/unknown.md
InterpolatedIdentifier文法与 vendor prefix 定义:spec/syntax.md- keyframes 特殊处理的运行时分支:spec/style-rules.md
@media的双重解析机制:spec/at-rules/media.md- 样式表拆分父节点的过程:spec/stylesheet.md
【免费下载链接】sassSass makes CSS fun!项目地址: https://gitcode.com/gh_mirrors/sa/sass
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考