Sass 媒体查询逻辑(Media Logic)提案解读:全面支持 Media Queries Level 4 的 `and`、`or`、`not` 布尔语法

原创2026-09-21 19:04:451,133 阅读
文章标签:前端

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*) ')'

逐条解读这个文法:

  1. 三种顶层形态:一条媒体查询要么以 not 开头(MediaNot),要么以括号内的媒体条件开头并跟随任意多个 and/or 子句,要么以媒体类型开头(后面可跟 and 加条件,或直接跟 and 列表)。
  2. MediaType:由一个或两个 InterpolatedIdentifier(即 spec/syntax.md#interpolatedidentifier 中定义的、允许插值的标识符)构成。注意脚注 1:第二个标识符不得是 "and",否则 "screen and ..." 这种写法会被错误切分。
  3. MediaNot/MediaAnd/MediaOr:脚注 2 强调,关键字与后面的 MediaOrInterp 之间不允许有任何空白。这是为了避免与媒体类型解析产生歧义——例如 not (width > 100px) 是合法的 MediaNot,但 not (width > 100px) 若中间插了空白就违反了文法。
  4. MediaOrInterp:可以是插值(Interpolation),也可以是括号内媒体条件(MediaInParens)。这是 SassScript 在媒体查询中唯一仍被允许出现的“裸”位置。
  5. 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*)),它们使任意布尔嵌套成为可能。
  6. 脚注 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 部分。

正式规范不会一步到位强制实施,而是先以“受限模式”运行,分两个层面过渡:

  1. 语法规格降级:在弃用期内,MediaInParens 不允许使用两种递归产生式——'(' MediaNot ')' 与 '(' MediaInParens (MediaAnd* | MediaOr*) ')'。也就是说,过渡期仍不支持任意嵌套布尔逻辑,仅允许 MQ4 的区间/比较语法先行落地。
  2. 弃用警告:如果某个 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 值类型的最小知识面,这正是媒体查询文法保持“语法先行、语义克制”的原因。

登录后查看全文
sass