CSS 工作组(WG)决定在 CSS 中添加一个内联的 if() 函数。本文解读 if() 函数的设计理念与应用场景,对比其与 style queries 的差异,展示在复杂条件处理上的独特优势。
CSS 工作组决定在 CSS 中添加一个内联的 if() 函数
上周,CSS 工作组(WG)决定在 CSS 中添加一个内联的 if() 函数。但这意味着什么,为什么它如此令人兴奋呢?
上周,我们在西班牙的拉科鲁尼亚举行了一次 CSS 工作组的面对面会议。这次会议中有一个决议我特别兴奋:一致同意在 CSS 中添加一个内联的 if() 函数。虽然我不是第一个提出内联条件语法的人,但我尝试将各种无休止的讨论缩减为一个可以快速实现的最小可行产品(MVP),与实现者讨论了想法,并最终发布了一个具体的提案并推动了小组决议。非常富有诗意的是,相关的讨论恰好发生在我的生日那天,所以从某种意义上说,我得到了 if() 作为最独特的生日礼物。😀
这也表明,提案被拒绝并不是某个功能的终结。事实上,一个功能在被接受之前被多次拒绝是相当常见的:CSS Nesting、:has()、容器查询(container queries)都是一系列被拒绝提案中的最后迭代。if() 本身在 2018 年似乎被拒绝了,当时我提出的语法与之非常相似。那么有什么不同呢?那时样式查询(style queries)已经发布,我们可以简单地引用相同的语法作为条件(再加上 Tab 的 @when 提案中的 media() 和 supports()),而在 2018 年的提案中,条件的工作方式在很大程度上是未定义的。
我在各种社交媒体上发布了这个消息,而开发者的反应非常积极:Twitter、LinkedIn、Mastodon。
我甚至有大公司的朋友写信告诉我,他们内部的 Slack 群聊对此事炸开了锅。这证实了我一直以来的怀疑,也是我向 CSS 工作组提出的一个理由:这是一个巨大的痛点。希望这些积极反应的数量和强度能帮助浏览器优先考虑这个功能,并将其尽早纳入他们的开发路线图。
在这些平台上,除了“我迫不及待地想要这个功能上线!”是最常见的情绪外,还有一些其他问题反复出现,以及相当程度的困惑,我觉得这些问题值得回答。
Q&A
if()
的用途是什么?它会取代样式查询(style queries)吗?
恰恰相反,if()
是对样式查询的补充。如果你能用样式查询完成某件事,那么请尽管使用样式查询——它们几乎肯定是更好的解决方案。但是有些事情是样式查询无法做到的。我来解释一下。
推动引入 if()
的用例是,组件(在广义上)经常需要定义更高级别的自定义属性,这些属性的值不仅直接在声明中使用,而且会在各种声明中设置不相关的值。
例如,考虑一个 --variant
自定义属性(受 Shoelace 的 variant 属性的启发)。它可能看起来像这样:
--variant: success | danger | warning | primary | none;
这个属性需要设置背景色、边框色、文本色、图标等。实际上,它的实际值在任何地方都不会被直接使用,它只用于设置其他值。
样式查询(style queries)让我们部分实现了这一功能:
.callout {@container (style(--variant: success)) { &::before { content: var(--icon-success); color: var(--color-success); }}/* (其他变体) */
}
但是,样式查询仅在后代(子)元素上工作。我们无法做到以下这点:
.callout { @container (style(--variant: success)) { border-color: var(--color-success-30); background-color: var(--color-success-95); &::before { content: var(--icon-success); color: var(--color-success-05); }} /* (其他变体) */
}
通常,我们需要在元素本身上设置的声明很少,有时甚至只有一个。然而,即使只有一个也是多余的,使得在许多(可能是大多数)高级别自定义属性用例中使用自定义属性变得不可行。因此,组件库最终会使用诸如 pill、outline、size 等呈现属性。
虽然呈现属性乍看之下可能没问题,甚至对开发者体验来说更好(字符更少——至少与每个元素设置一个变量相比),但它们存在几个可用性问题:
-
减少灵活性
它们不能基于选择器、媒体查询等条件来应用。更改它们需要更多的JavaScript。如果它们被用在另一个组件内部,你就束手无策了,而使用(可继承的)自定义属性,你可以在父组件上设置属性,它会向下继承。 -
冗长性
它们必须应用到单个实例上,并且不能继承。即使使用某种形式的模板或组件化来减少重复,在使用开发工具进行调试时,仍然需要遍历这些属性。 -
缺乏一致性
由于几乎每个成熟的组件也支持自定义属性,用户必须记住哪些样式是通过属性设置的,哪些是通过自定义属性设置的。这种区分往往是任意的,因为它不是由用例驱动的,而是由实现的便利性驱动的。
使用if(),上述示例变得可能:
.callout {border-color: if(style(--variant: success) ? var(--color-success-30) :style(--variant: danger) ? var(--color-danger-30) :var(--color-neutral-30));background-color: if(style(--variant: success) ? var(--color-success-95) :style(--variant: danger) ? var(--color-danger-95) :var(--color-neutral-95));@container (style(--variant: success)) {&::before {content: var(--icon-success);color: var(--color-success-05);}}
}
虽然这是主要的用例,但事实证明,将媒体查询和兼容性条件也作为if()的条件语法的一部分是非常容易的。由于它是一个函数,它的参数(包括条件!)可以存储在其他自定义属性中。这意味着你可以做像这样的事情:
:root {--xl: media(width > 1600px);--l: media (width > 1200px);--m: media (width > 800px);
}
然后定义如下值:
padding: if(var(--xl) ? var(--size-3) :var(--l) or var(--m) ? var(--size-2) :var(--size-1)
);
就像JavaScript中的三元运算符一样,当只有值的一小部分发生变化时,使用这种方式可能更易于使用和阅读:
animation: if(media(prefers-reduced-motion) ? 10s : 1s) rainbow infinite;
那么这个功能已经在浏览器里实现了吗?
信不信由你,这是我收到的一个真实问题😅。不,这个功能目前还没有在浏览器中实现,而且还需要一段时间。最乐观的估计也大约是2年左右,如果这个过程没有在任何时候停滞不前(这通常会发生)。
目前我们只是在该特性上达成了共识。接下来的步骤是:
-
就该特性的语法达成共识。语法辩论往往会持续很长时间,因为每个人在语法上都有自己的看法。目前的辩论主要集中在:
-
条件与分支之间应该使用什么分隔符?
-
如何表示没有值?我们是简单地允许像var()中那样的空值(例如,你可以写var(–foo,)),还是引入一个专门的语法来表示“空值”?
-
最后一个值是否应该是可选的?
-
-
为该特性编写规范。
-
获取第一个实现。这通常是最难的部分。一旦一个浏览器实现了,其他浏览器加入就会容易得多。
-
在所有主要浏览器中发布该功能。
我确实有一个页面,我在那里追踪我的一些标准提案,这应该有助于阐明这些步骤的时间表是什么样的。事实上,你也可以在那里追踪if()的特定进度。
这是CSS中的第一个条件语句吗?
许多回答都是类似“哇,CSS终于有条件语句了!”这样的。
朋友们……CSS从一开始就有条件语句。每个选择器本质上都是一个条件语句!
此外:
@media
和 @supports
规则也是条件语句。别忘了还有 @container
。
var(--foo, fallback)
是一种有限类型的条件语句(本质上类似于 if(style(--foo: initial) ? var(--foo) : fallback)
),这就是为什么它成为大多数模拟内联条件语句的解决方案的基础。
这会让CSS变成命令式的吗?
一个普遍的误解是非线性逻辑(条件语句、循环)使一种语言成为命令式语言。
声明式与命令式并不关乎逻辑,而是关于抽象层次。我们是在描述目标还是实现它的方法?用烹饪术语来说,食谱是命令式的,而餐厅菜单是声明式的。
条件逻辑实际上可以使一种语言更加声明式,如果它有助于更好地描述意图的话。
考虑以下两段CSS代码:
正常写法
button {border-radius: calc(.2em + var(--pill, 999em));
}.fancy.button {/* Turn pill on */--pill: initial;
}
使用if() 写法
button {border-radius: if(style(--shape: pill) ? 999em : .2em);
}.fancy.button {--shape: pill;
}
我会说后者是更加声明式的,即它更接近于指定目标而不是如何实现它。通过使用--shape
自定义属性,我们是在声明一个按钮应该具有什么形状,而不是直接指定它应该如何根据条件改变其border-radius
。这种方法使代码更加清晰和易于理解,因为我们是在定义目标样式,而不是实现细节。
这会让CSS成为一种编程语言吗?
一种非常常见的回应是CSS现在是否是一种编程语言(要么是询问它是否是,要么是断言它现在是)。要回答这个问题,首先需要回答什么是编程语言。
如果图灵完备性让一种语言成为编程语言,那么CSS十多年前就已经是编程语言了。但话说回来,Excel或Minecraft也是。那么这究竟意味着什么呢?
如果是因为命令式特性,那么不,CSS不是一种编程语言。但许多实际的编程语言也不是!
但一个更深层次的问题是,这重要吗?是因为它让选择专注于CSS变得合法化了吗?是因为即使你只写HTML和CSS也可以被认为是程序员吗?如果这只是为了面子问题,那么我们应该从核心上解决这个问题,并努力让CSS的专业知识合法化,无论CSS是否是一种编程语言。毕竟,任何了解几种备受尊敬的编程语言和CSS的人都可以证明,CSS更难掌握。