Sass 媒体查询逻辑(Media Logic)提案解读:全面支持 Media Queries Level 4 的 `and`、`or`、`not` 布尔语法
Sass 媒体查询逻辑(Media Logic)提案解读:全面支持 Media Queries Level 4 的 and、or、not 布尔语法
本篇技术指南基于 Sass 官方仓库中已接受的规范提案 accepted/media-logic.md(Draft 1.1)展开,系统讲解 Sass 如何为 @media 规则引入 Media Queries Level 4 的任意布尔逻辑语法,包括 and、or、not 组合的媒体条件、完整的 MediaQuery 与 CssMediaQuery 语法产生式、由此带来的破坏性变更,以及官方的弃用(deprecation)过渡策略。读完本文,你将理解 Sass 为何是极少数“完全解析媒体查询”的语言扩展、新语法与旧 SassScript 表达式的歧义边界在哪里,以及该提案如何与 spec/at-rules/media.md 中的正式规范、accepted/media-ranges.md 中的范围上下文语法相互衔接。
背景:为什么 Sass 必须跟进媒体查询新语法
本节对应原提案的 Background 部分,属于非规范性说明(non-normative)。
Sass 对 @media 规则的处理与其他 at-rule 有一个根本性差异:出于历史原因,Sass 完全解析媒体查询,并允许 SassScript 直接内嵌其中,例如 @media ($query: $value)。而绝大多数其他 at-rule(如 @supports、@keyframes)中,SassScript 只能通过插值(interpolation,即 #{})注入。
这一设计带来的直接后果是:每当 CSS 新增媒体查询语法,Sass 就必须同步更新自身的规范来兼容它——因为媒体查询在 Sass 源码里不是“原样透传”的文本,而是被语法树真实解析过的结构。
Media Queries Level 4(即 MQ4)为媒体查询引入了任意布尔逻辑能力,例如:
@media ((width >= 100px) and (width <= 800px)) or (grid) { ... }
这种查询不再只是简单的“媒体类型 + and 连接的特性列表”,而是可以嵌套括号、使用 or 表达“任一满足”、用 not 表达否定。Sass 的旧有语法(见 accepted/media-ranges.md Draft 3.1,仅支持 MediaType ('and' MediaFeature)* 形式的线性组合)无法表达这种嵌套布尔结构,因此必须修订语法规范。本文所述的 Media Logic 提案(对应 GitHub Issue #2538)正是为此而生。
提案总览:在语法层面引入任意 <media-condition>
本节对应原提案的 Summary 部分,属于非规范性说明。
该提案的目标相对直接:把 MQ4 的新语法加入 Sass 的文法。它允许任意一个 <media-condition> 出现在 <media-in-parens>(括号内的媒体条件)中,从而支持任意深度的布尔组合。
但需要注意的是,这带来了若干破坏性变更。提案明确承认这些变更对真实世界样式表的影响面很小,但仍值得强调:
- 以
(not开头的查询 - 以
((开头的查询
这两类写法在历史上会被解析为 SassScript 表达式,而在新语法下必须被解析为嵌套媒体查询。为了平滑过渡,Sass 会对这些 SassScript 用法发出弃用警告,推荐用户改用插值表达,随后移除支持并正式按媒体查询解析,以对齐 CSS 兼容性。
换句话说,这次变更的本质是:把媒体查询的“表达能力”从 SassScript 侧移交回媒体查询文法侧,让 Sass 语法与浏览器端 CSS 解析结果保持一致。
Sass 侧语法:MediaQuery 产生式详解
本节内容来自原提案 Syntax 部分的
MediaQuery小节,已同步进入正式规范 spec/at-rules/media.md#sass,所有标识符均不区分大小写匹配。
提案要求用以下产生式替换既有的 MediaQuery 定义:
MediaQuery ::= MediaNot
| MediaInParens (MediaAnd* | MediaOr*)
| MediaType ('and' MediaNot | MediaAnd*)
MediaType ::= InterpolatedIdentifier InterpolatedIdentifier¹?
MediaNot² ::= 'not' MediaOrInterp
MediaAnd² ::= 'and' MediaOrInterp
MediaOr² ::= 'or' MediaOrInterp
MediaOrInterp ::= Interpolation | MediaInParens
MediaInParens ::= '(' Expression³ ')'
| '(' Expression³ <mf-comparison> Expression³ ')'
| '(' Expression³ <mf-lt> Expression³ <mf-lt> Expression³ ')'
| '(' Expression³ <mf-gt> Expression³ <mf-gt> Expression³ ')'
| '(' MediaNot ')'
| '(' MediaInParens (MediaAnd* | MediaOr*) ')'
逐条解读这个文法:
- 三种顶层形态:一条媒体查询要么以
not开头(MediaNot),要么以括号内的媒体条件开头并跟随任意多个and/or子句,要么以媒体类型开头(后面可跟and加条件,或直接跟and列表)。 MediaType:由一个或两个InterpolatedIdentifier(即 spec/syntax.md#interpolatedidentifier 中定义的、允许插值的标识符)构成。注意脚注 1:第二个标识符不得是"and",否则"screen and ..."这种写法会被错误切分。MediaNot/MediaAnd/MediaOr:脚注 2 强调,关键字与后面的MediaOrInterp之间不允许有任何空白。这是为了避免与媒体类型解析产生歧义——例如not (width > 100px)是合法的MediaNot,但not (width > 100px)若中间插了空白就违反了文法。MediaOrInterp:可以是插值(Interpolation),也可以是括号内媒体条件(MediaInParens)。这是 SassScript 在媒体查询中唯一仍被允许出现的“裸”位置。MediaInParens的六种形态:普通特性((feature))、特性值比较((feature <mf-comparison> value))、以及 MQ4 的两种区间写法——<mf-lt>连用(如(400px < width < 1000px))和<mf-gt>连用(如(1000px > width > 400px))。<mf-comparison>、<mf-lt>、<mf-gt>均引用 MQ4 规范定义。最后两种形态正是本次新增的递归结构:(not ...)与(MediaInParens (MediaAnd* | MediaOr*)),它们使任意布尔嵌套成为可能。- 脚注 3 对
Expression的三条限制(这是整个提案中最容易踩坑的部分):- 表达式不得包含二元运算符
=、>、>=、<、<=构成的运算,除非这些运算出现在括号(包括函数调用与 map 字面量)或方括号内部。原因很直观:>、>=、<、<=正是媒体查询区间语法的运算符,Sass 必须把优先级让给媒体查询文法,否则(width > 100px)会先被 SassScript 解析成布尔比较表达式。 - 表达式不得以不区分大小写的标识符
"not"开头。 - 表达式不得以字符
"("开头。
- 表达式不得包含二元运算符
后两条限制直接服务于前文提到的歧义消解:(not ...) 和 ((...)) 在旧语法里是合法的 SassScript 表达式,在新语法里则必须交给媒体查询文法处理。
与正式规范的关系
原提案(Draft 1.1)中的这套 MediaQuery 产生式已经原样落入正式规范 spec/at-rules/media.md#sass。在正式规范中还可以看到完整的 MediaQueryList ::= MediaQuery (',' MediaQuery)* 顶层产生式,说明逗号分隔的多媒体查询列表不受影响。此外,该规范还说明:@media 虽然是普通 CSS 规则,但 Sass 对其做了双重解析——第一次在解析 Sass 样式表时进行(此时允许 SassScript 表达式与插值),第二次在 SassScript 求值完成后,把结果作为纯 CSS 重新解析。这也是为什么本文下面要单独定义一套 CSS 侧文法。
CSS 侧语法:CssMediaQuery 产生式详解
本节内容来自原提案 Syntax 部分的
CssMediaQuery小节,已同步进入正式规范 spec/at-rules/media.md#css。
SassScript 求值完毕后,媒体查询需要以纯 CSS 身份重新解析。提案要求用以下产生式替换既有的 CssMediaQuery 定义(所有标识符不区分大小写):
CssMediaQuery ::= CssMediaCondition
| CssMediaType ('and' CssMediaNot | CssMediaAnd*)
CssMediaType ::= <ident-token> <ident-token>¹?
CssMediaCondition ::= CssMediaNot | CssMediaInParens (CssMediaAnd* | CssMediaOr*)
CssMediaNot ::= 'not' CssMediaInParens
CssMediaAnd ::= 'and' CssMediaInParens
CssMediaOr ::= 'or' CssMediaInParens
CssMediaInParens ::= '(' <declaration-value> ')'
要点解读:
CssMediaType使用 CSS 语法规范的<ident-token>(与 Sass 侧的InterpolatedIdentifier相对应),同样受脚注 1 限制:第二个<ident-token>不得为"and"。CssMediaCondition是核心新增结构,允许CssMediaNot或CssMediaInParens后跟任意多个CssMediaAnd*/CssMediaOr*——即 CSS 输出端的任意布尔逻辑。CssMediaInParens直接使用 CSS 语法规范的<declaration-value>兜底,也就是说括号内的内容在 CSS 阶段是宽容解析的:只要括号平衡、token 合法,就整体接受。这与 accepted/media-ranges.md 中“CSS 阶段沿用<declaration-value>解析、无需任何行为变更”的设计一脉相承——因为区间形式(range form)的查询在 CSS 阶段天然被<declaration-value>覆盖。
从源码结构看,CSS 侧文法的定位是“结果校验与兜底”:Sass 侧文法负责承载 SassScript 能力与插值,CSS 侧文法负责保证求值后的产物能被标准 CSS 解析器接受。两者分工明确,这也是 Sass 对媒体查询“解析两次”设计的落点。
破坏性变更与歧义边界
新语法最值得警惕的不是“多了什么”,而是“什么变了”。汇总如下:
| 历史写法 | 旧解析行为 | 新解析行为 |
|---|---|---|
@media (not $x) |
not 被当作 SassScript 一元否定,$x 作为布尔变量 |
解析为 MediaInParens 中的 MediaNot(嵌套媒体条件) |
@media ((1 + 2)) |
(1 + 2) 被当作 SassScript 括号表达式 |
解析为嵌套的 MediaInParens (MediaAnd* | MediaOr*) |
@media (width > 100px) |
历史上由 SassScript 比较表达式处理(受 accepted/media-ranges.md 规范约束) | 由媒体查询文法中的 <mf-comparison> / <mf-lt> / <mf-gt> 处理 |
@media (screen and $x) 等含比较运算符的裸表达式 |
可被 SassScript 运算 | 除括号/方括号内部外,禁止此类二元运算 |
其中前两类就是提案明确点名的破坏性变更来源:以 (not 或 (( 开头的查询。如果你的样式表恰好用 SassScript 写了这样的表达式,升级后会得到不同的解析结果。
规避建议:任何需要 SassScript 参与的媒体查询值,都应改用插值(#{})显式注入,例如 @media (min-width: #{$width}),而不是依赖 SassScript 直接在媒体查询中裸写表达式。这也是提案在弃用期推荐的迁移方向。
弃用过程(Deprecation Process)
本节对应原提案的 Deprecation Process 部分。
正式规范不会一步到位强制实施,而是先以“受限模式”运行,分两个层面过渡:
- 语法规格降级:在弃用期内,
MediaInParens不允许使用两种递归产生式——'(' MediaNot ')'与'(' MediaInParens (MediaAnd* | MediaOr*) ')'。也就是说,过渡期仍不支持任意嵌套布尔逻辑,仅允许 MQ4 的区间/比较语法先行落地。 - 弃用警告:如果某个
MediaInParens产生式中的第一个Expression以不区分大小写的标识符"not"或以字符"("开头,则发出弃用警告(deprecation warning),提醒用户这些写法将从 SassScript 表达式改为媒体查询解析,应尽早迁移到插值形式。
完成过渡后,两种递归产生式正式启用,上述写法彻底按媒体查询解析,Sass 与 CSS 的解析结果完全对齐。对使用者而言,弃用期相当于一个“双保险”窗口:既给了生态迁移时间,又让实现方(如 Dart Sass、LibSass)可以分阶段落地文法。
配套规范与提案之间的关系
MediaQuery / CssMediaQuery 文法并非孤立存在,它与仓库中的其他规范/提案紧密咬合:
- spec/at-rules/media.md:
@media的正式规范,本文所述两套产生式已合并于此,并补充了MediaQueryList顶层产生式与“解析两次”的总体流程说明。 - accepted/media-ranges.md:范围上下文媒体特性提案(Draft 3.1),对应 Issue #1864。它定义了
(width > 100px)、(400px < width < 1000px)这类区间写法的 Sass 解析规则,其MediaFeature产生式中的<mf-comparison>/<mf-lt>/<mf-gt>被 Media Logic 提案直接复用;且它明确指出“当时 Sass 尚不支持完整的 MQ4 媒体条件,详见 sass/sass#2538”——这正是 Media Logic 提案要补上的最后一块拼图。 - spec/at-rules/import.md:
@import的ImportMedia产生式直接引用 MediaQueryList(见该文件第 22 行ImportMedia ::= MediaFeatureInParens (',' MediaQueryList)*及第 32 行的链接定义)。这意味着媒体查询文法的升级会自动传导到@import的媒体查询子句,保证两处解析行为一致。 - accepted/at-rule-interpolation.md:该提案说明
@media、@supports是在解析期被检测的特殊 at-rule;若 at-rule 名经过插值(如@m#{ed}ia),则被当作未知 at-rule 处理。这与 Media Logic 的“解析期歧义消解”思路互为印证:Sass 倾向于在文法层面明确边界,而非在运行时兜底。
版本变更记录
从 accepted/media-logic.changes.md 可以看到该提案的演进:
- Draft 1:初始草案。
- Draft 1.1(即当前版本)的三处修订:
MediaQuery产生式中,不允许Interpolation后跟(MediaAnd* | MediaOr*),因为Interpolation与MediaType存在歧义;- 禁止
MediaNot、MediaAnd、MediaOr产生式中关键字与后续部分之间存在空白(即前文脚注 2 的由来); - 修正了
CssMediaQuery的规范链接。
这三处修订体现了提案打磨期对歧义问题的精细处理:插值作为 MediaOrInterp 的合法成员本身已足够表达动态部分,若再允许插值后接布尔链,就会与“插值即媒体类型”的解析路径冲突,因此被明确禁止。
结语与适用前提
Media Logic 提案(Draft 1.1)是 Sass 对齐 Media Queries Level 4 的关键一步:它在 Sass 文法中引入 MediaNot/MediaAnd/MediaOr 与递归的 MediaInParens,让 @media ((width >= 100px) and (width <= 800px)) or (grid) 这类任意布尔条件成为一等公民;同时通过 CSS 侧 CssMediaCondition 文法与 <declaration-value> 兜底,保证求值产物可被标准 CSS 解析。它带来的破坏性变更集中在 (not 、(( 开头的历史 SassScript 写法上,弃用期通过“禁用递归产生式 + 发出弃用警告”两步过渡。
需要说明的适用前提:本仓库 README.md 明确指出,该仓库本身不是 Sass 的实现(实现位于 dart-sass 与 libsass),而是语言特性规范与提案的载体——spec/ 存放正式规范,accepted/ 存放已被接受、正在实现中的提案(本文主角即属此类),proposal/ 存放进行中的草案。因此,本文讨论的是语言规范层面的语法契约;具体某版本编译器的解析行为是否已完整落地 Media Logic,应以对应实现(如 Dart Sass)的发布说明为准。若你的样式表大量使用区间媒体特性,建议同时关注 accepted/media-ranges.md 中“区间特性仅以 and 列表方式合并、不做智能折叠”的设计决策——Sass 刻意保持对 CSS 值类型的最小知识面,这正是媒体查询文法保持“语法先行、语义克制”的原因。