Raven 技术:距离容器查询更近一步

Avatar of Mathias Hülsbusch
Mathias Hülsbusch

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 免费积分!

再次强调:我们需要 CSS 中的容器查询!并且,看起来我们正朝着那个方向前进.

在构建网站组件时,您并不总是知道该组件将如何使用。也许它将呈现为与浏览器窗口一样宽。也许两个组件将并排放置。也许它将位于某个狭窄的列中。的宽度并不总是与浏览器窗口的宽度相关联。

通常会达到这样一种情况,即为组件的 CSS 提供基于容器的查询会非常方便。如果您在网上搜索此问题的解决方案,您可能会发现一些基于 JavaScript 的解决方案。但是这些解决方案会带来一些成本:额外的依赖项、需要 JavaScript 的样式以及污染的应用程序逻辑和设计逻辑。

我坚信关注点分离,而布局是 CSS 的关注点。例如,尽管IntersectionObserver 是一个很好的 API,但我希望在 CSS 中看到类似:in-viewport 的东西!因此,我继续寻找仅使用 CSS 的解决方案,我遇到了 Heydon Pickering 的Flexbox 神圣信天翁。对于列来说,这是一个不错的解决方案,但我想要更多。原始信天翁有一些改进(比如不神圣的信天翁),但它们仍然有点 hacky,而且所有正在发生的事情只是行到列的切换。

我仍然想要更多!我想要更接近真正的容器查询!那么,CSS 提供了什么我可以利用的东西呢?我拥有数学背景,因此,我喜欢并理解calc()min()max()clamp() 之类的函数。

下一步:用它们构建类似容器查询的解决方案。

想要在继续阅读之前了解一下可能性吗?这里有一个 CodePen 集合 展示了本文中讨论的想法可以实现什么。


为什么是“Raven”?

这项工作灵感来自 Heydon 的信天翁,但该技术可以实现更多技巧,因此我选择了乌鸦,因为乌鸦是非常聪明的鸟类。

回顾:CSS 中的数学函数

calc() 函数 允许在 CSS 中进行数学运算。作为奖励,可以组合单位,因此可以使用诸如calc(100vw - 300px) 之类的内容。

min()max() 函数接受两个或多个参数,并返回最小或最大参数(分别)。

clamp() 函数类似于min()max() 的组合,非常有用。函数clamp(a, x, b) 将返回

  • a 如果 x 小于 a
  • b 如果 x 大于 b 且
  • x 如果 x 介于 a 和 b 之间

因此,它有点类似于clamp(最小值, 相对值, 最大值)。可以将其视为min(max(a,x),b) 的简写。如果您想了解更多信息,请阅读有关所有内容的更多信息

我们还将在本文中大量使用另一个 CSS 工具:CSS 自定义属性。它们是类似--color: red;--distance: 20px 的东西。本质上是变量。我们将使用它们来使 CSS 更简洁,例如避免过多重复。

让我们开始使用 Raven 技术。

步骤 1:创建配置变量

让我们创建一些 CSS 自定义属性来进行设置。

我们希望查询基于什么基本大小?由于我们追求的是容器查询行为,因此这将是100%——使用100vw 将使其表现得像媒体查询,因为那是浏览器窗口的宽度,而不是容器的宽度!

--base_size: 100%;

现在,我们考虑断点。从字面上看,就是容器宽度,当达到这些宽度时,我们希望产生断点以应用新的样式。

--breakpoint_wide: 1500px; 
/* Wider than 1500px will be considered wide */
--breakpoint_medium: 800px;
/* From 801px to 1500px will be considered medium */
/* Smaller than or exact 800px will be small */

在运行示例中,我们将使用三个区间,但此技术没有限制。

现在,让我们定义一些(CSS 长度)值,我们希望这些值在断点定义的区间内返回。这些是字面量值。

--length_4_small: calc((100% / 1) - 10px); /* Change to your needs */
--length_4_medium: calc((100% / 2) - 10px); /* Change to your needs */
--length_4_wide: calc((100% / 3) - 10px); /* Change to your needs */

这是配置。让我们使用它!

步骤 2:创建指示器变量

我们将为区间创建一些指示器变量。它们的行为有点像布尔值,但使用长度单位(0px1px)。如果我们将这些长度作为最小值和最大值进行夹紧,那么它们就会充当“true”和“false”指示器。

因此,当且仅当--base_size 大于--breakpoint_wide 时,我们希望该变量为1px。否则,我们希望它是0px。这可以使用clamp() 来完成。

--is_wide: clamp(0px,
  var(--base_size) - var(--breakpoint_wide),
  1px
);

如果var(--base_size) - var(--breakpoint_wide) 为负值,则--base_size 小于--breakpoint_wide,因此在这种情况下,clamp() 将返回0px

相反,如果--base_size 大于--breakpoint_wide,则计算将得出正长度,该长度大于或等于1px。这意味着clamp() 将返回1px

Bingo!我们得到了“wide”的指示器变量。

让我们为“medium”区间执行此操作。

--is_medium: clamp(0px,
  var(--base_size) - var(--breakpoint_medium),
  1px
); /*  DO NOT USE, SEE BELOW! */

这将为小型区间提供0px,但为中型和大型区间提供1px。但是,我们想要的是大型区间为0px,而中型区间1px

我们可以通过减去--is_wide 值来解决此问题。在大尺寸区间,1px - 1px0px;在中尺寸区间,1px - 0px1px;在小尺寸区间,0px - 0px0px。完美。

因此,我们得到

--is_medium: calc(
  clamp(0px, 
  var(--base_size) - var(--breakpoint_medium), 
  1px) 
  - var(--is_wide)
); 

明白了吗?要计算指示器变量,请使用clamp(),将0px1px 作为边界,并将--base_width--breakpoint_whatever 的差作为夹紧值。然后减去所有较大区间指示器的总和。此逻辑将为最小区间指示器产生以下结果。

--is_small: calc(
  clamp(0px,
    (var(--base_size) - 0px,
    1px)
  - (var(--is_medium) + var(--is_wide))
); 

我们可以跳过这里的夹紧,因为小尺寸的断点为0px,而--base_size 为正值,因此--base_size - 0px 始终大于1px,而clamp() 将始终返回1px。因此,--is_small 的计算可以简化为

--is_small: calc(1px - (var(--is_medium) + var(--is_wide))); 

步骤 3:使用指示器变量选择区间值

现在,我们需要将这些“指示器变量”转换为有用的东西。假设我们使用基于像素的布局。不要惊慌,我们将在后面处理其他单位。

这里有一个问题。这将返回什么?

calc(var(--is_small) * 100);

如果--is_small1px,它将返回100px,如果--is_small0px,它将返回0px

这有什么用?请看这里。

calc(
  (var(--is_small) * 100) 
  +
  (var(--is_medium) * 200) 
);

这将在小尺寸区间返回100px + 0px = 100px(其中--is_small1px,而--is_medium0px)。在中尺寸区间(其中--is_medium1px,而--is_small0px),它将返回0px + 200px = 200px

你明白了吗?看看 Roman Komarov 的 文章,了解更多关于这里发生的事情,因为它可能很难理解。

你将一个像素值(不带单位)乘以相应的指示器变量,然后将所有这些项加起来。因此,对于基于像素的布局,像这样的代码就足够了

width: calc(
    (var(--is_small)  * 100) 
  + (var(--is_medium) * 200) 
  + (var(--is_wide)   * 500) 
  );

但大多数情况下,我们并不想要基于像素的值。我们想要概念,比如“全宽”或“三分之一宽”,甚至其他单位,比如 2rem65ch 等等。为了实现这些,我们必须继续深入。

步骤 4:使用 min() 和一个极大的整数来选择任意长度的值

在第一步中,我们定义了类似于这样的代码,而不是一个静态像素值

--length_4_medium: calc((100% / 2) - 10px);

那么我们如何使用它们呢?min() 函数来救援!

让我们定义一个辅助变量

--very_big_int: 9999; 
/* Pure, unitless number. Must be bigger than any length appearing elsewhere. */

将此值乘以指示器变量将得到 0px9999px。此值的大小取决于你的浏览器。Chrome 会接受 999999,但 Firefox 不会接受如此大的数字,所以 9999 是一个在两者中都能正常工作的数值。很少有视口大于 9999px,因此我们应该没问题。

那么,当我们用任何小于 9999px 但大于 0px 的值与之 min() 时会发生什么?

min(
  var(--length_4_small), 
  var(--is_small) * var(--very_big_int) 
);

当且仅当 --is_small0px 时,它将返回 0px。如果 --is_small1px,乘法将返回 9999px(这比 --length_4_small 大),min 将返回:--length_4_small

这就是我们如何根据指示器变量选择任何长度(小于 9999px 但大于 0px)。

如果你处理的视口大于 9999px,则需要调整 --very_big_int 变量。这有点难看,但一旦纯 CSS 可以 在值上删除单位,我们就可以解决这个问题,从而消除指示器变量的单位(并直接将其乘以任何长度)。目前,这种方法有效。

现在我们将把所有部分组合起来,让乌鸦飞起来!

步骤 5:将所有部分组合起来

现在,我们可以像这样计算出我们动态的基于容器宽度的、断点驱动的值

--dyn_length: calc(
    min(var(--is_wide)   * var(--very_big_int), var(--length_4_wide)) 
  + min(var(--is_medium) * var(--very_big_int), var(--length_4_medium))
  + min(var(--is_small)  * var(--very_big_int), var(--length_4_small))
);

每行都是步骤 4 中的 min()。所有行都像步骤 3 一样加起来,指示器变量来自步骤 2,所有这一切都基于我们在步骤 1 中的配置——它们在一个大的公式中协同工作!

想试试吗?这里有一个 Pen 可以用来玩(查看 CSS 中的注释)。

这个 Pen 不使用 flexbox、grid 或浮动。只是一些 div。这样做是为了表明在这样的布局中,辅助程序是没必要的。但是,请随意在这些布局中使用乌鸦,因为它会帮助你实现更复杂的布局。

还有其他事情吗?

到目前为止,我们一直在使用固定的像素值作为断点,但也许我们想改变布局,如果容器大于或小于视口的一半,减去 10px?没问题

--breakpoint_wide: calc(50vw - 10px);

这就可以了!其他公式也同样有效。为了避免奇怪的行为,我们想要使用类似于这样的代码

--breakpoint_medium: min(var(--breakpoint_wide), 500px);

…在 500px 宽度处设置第二个断点。步骤 2 中的计算依赖于 --breakpoint_wide 不小于 --breakpoint_medium 的事实。只要保持断点顺序正确:min() 和/或 max() 在这里非常有用!

高度呢?

所有计算的评估都是懒惰的。也就是说,当将 --dyn_length 赋值给任何属性时,计算将基于 --base_size 在此处的评估结果。因此,如果 --base_size100%,则设置高度将基于 100% 的高度。

我还没有(还没有)找到一种方法可以根据容器的宽度来设置高度。因此,你可以使用 padding-top,因为 100% 会评估为填充的宽度。

显示和隐藏东西呢?

使用乌鸦显示和隐藏东西最简单的方法是在适当的指示器变量处将宽度设置为 100px(或任何其他合适的宽度)

.show_if_small {
  width: calc(var(--is_small) * 100);
}
.show_if_medium {
  width: calc(var(--is_medium) * 100);
}
.show_if_wide {
  width: calc(var(--is_wide) * 100);
}

你需要设置

overflow: hidden;
display: inline-block; /* to avoid ugly empty lines */

…或其他一些方法来隐藏 width: 0px 框内的内容。完全隐藏框需要设置额外的盒模型属性,包括 marginpaddingborder-width,为 0px。乌鸦可以为某些属性做到这一点,但将它们固定为 0px 也是一样有效。

另一个选择是使用 position: absolute; 并通过 left: calc(var(--is_???) * 9999); 将元素绘制到屏幕外。

要点

我们可能根本不需要 JavaScript,即使是容器查询行为!当然,我们希望,如果我们在 CSS 语法中真正实现了容器查询,它将更容易使用和理解——但这也很酷,因为今天在 CSS 中已经可以实现这些东西。

在开发这个过程中,我对 CSS 中可以使用的其他一些东西形成了自己的看法

  • 基于容器的单位,比如 conWconH,用于根据宽度设置高度。这些单位可以基于当前堆叠上下文的根元素。
  • 某种“评估为值”函数,以克服延迟评估问题。这将与在渲染时工作的“剥离单位”函数一起很好地工作。

注意:在早期版本中,我使用了 cwch 作为单位,但有人指出它们很容易与同名 CSS 单位混淆。感谢 Mikko Tapionlinna 和 Gilson Nunes Filho 在评论中提供的建议!)

如果我们有了第二个函数,它将允许我们用乌鸦设置颜色(以一种干净的方式)、边框、box-shadowflex-growbackground-positionz-indexscale(),以及其他一些东西。

再加上基于组件的单位,甚至可以将子元素的尺寸设置为与父元素相同的纵横比。除以带单位的值是不可能的;否则,--indicator / 1px 将作为乌鸦的“剥离单位”有效。

附加内容:布尔逻辑

指示器变量看起来像布尔值,对吧?唯一的区别是它们有一个“px”单位。那些的逻辑组合呢?想象一下,比如“容器宽度大于屏幕的一半” **并且** “布局是两列模式”。CSS 函数再次来救援!

对于 OR 运算符,我们可以在所有指示器上使用 max()

--a_OR_b: max( var(--indicator_a) , var(--indicator_b) );

对于 NOT 运算符,我们可以从 1px 中减去指示器

--NOT_a: calc(1px - var(--indicator_a));

逻辑纯粹主义者可能会止步于此,因为 NOR(a,b) = NOT(OR(a,b)) 是完整的布尔代数。但,嘿,仅仅是为了好玩,这里还有一些其他的

AND:

--a_AND_b: min(var(--indicator_a), var(--indicator_b)); 

当且仅当两个指示器都为 1px 时,它才会评估为 1px

注意,min()max() 可以接受两个以上的参数。对于(两个以上的)指示器变量,它们仍然可以作为 ANDOR 工作。

XOR:

--a_XOR_b: max(
  var(--indicator_a) - var(--indicator_b), 
  var(--indicator_b) - var(--indicator_a)
);

当且仅当两个指示器具有相同的值时,两个差值都为 0pxmax() 将返回此值。如果指示器具有不同的值,一个项将得到 -1px,另一个将得到 1px。在这种情况下,max() 将返回 1px

如果有人对两个指示器相等的情况感兴趣,请使用以下代码

--a_EQ_b: calc(1px - 
  max(
    var(--indicator_a) - var(--indicator_b), 
    var(--indicator_b) - var(--indicator_a)
  )
);

是的,这是 NOT(a XOR b)我无法找到一个更“优雅”的解决方案。

相等性可能对 CSS 长度变量本身很有趣,而不仅仅是用于指示器变量。再次使用 clamp(),这可能会有所帮助

--a_EQUALS_b_general: calc(
  1px -
  clamp(0px,
        max(
          var(--var_a) - var(--var_b),
          var(--var_b) - var(--var_a)
        ),
        1px)
  );

删除 px 单位以获得无单位变量(整数)的一般相等性。

我认为这对大多数布局来说已经足够布尔逻辑了!

额外 2:在网格布局中设置列数

由于 Raven 仅限于返回 CSS 长度值,它无法直接选择网格的列数(因为这是一个没有单位的值)。但有一种方法可以使其正常工作(假设我们像上面一样声明了指示器变量)

--number_of_cols_4_wide: 4;
--number_of_cols_4_medium: 2;
--number_of_cols_4_small: 1;
--grid_gap: 0px;

--grid_columns_width_4_wide: calc(
(100% - (var(--number_of_cols_4_wide) - 1) * var(--grid_gap) ) / var(--number_of_cols_4_wide));
--grid_columns_width_4_medium: calc(
(100% - (var(--number_of_cols_4_medium) - 1) * var(--grid_gap) ) / var(--number_of_cols_4_medium));
--grid_columns_width_4_small: calc(
(100% - (var(--number_of_cols_4_small) - 1) * var(--grid_gap) ) / var(--number_of_cols_4_small));

--raven_grid_columns_width: calc( /*  use the Raven to combine the values  */
  min(var(--is_wide) * var(--very_big_int),var(--grid_columns_width_4_wide)) 
  + min(var(--is_medium) * var(--very_big_int),var(--grid_columns_width_4_medium))
  + min(var(--is_small) * var(--very_big_int),var(--grid_columns_width_4_small))
  );

并使用以下内容设置您的网格:

.grid_container{
  display: grid;
  grid-template-columns: repeat(auto-fit, var(--raven_grid_columns_width));
  gap: var(--grid_gap)
};

这是如何工作的?

  1. 定义我们想要的每个区间(第 1、2、3 行)的列数。
  2. 计算每个区间(第 5、6、7 行)列的完美宽度。

    这里发生了什么?

    首先,我们计算可用于列的空间。 这是 100%,减去间隙占用的空间。 对于 n 列,有 (n-1) 个间隙。 然后将此空间除以我们想要的列数。

  3. 使用 Raven 计算实际 --base_size 的正确列宽。

在网格容器中,这行

grid-template-columns: repeat(auto-fit, var(--raven_grid_columns_width));

… 然后选择适合 Raven 提供的值的列数(这将导致我们上面的 --number_of_cols_4_??? 变量)。

Raven 可能无法直接给出列数,但它可以提供一个长度,以便 repeatautofit 为我们计算我们想要的数字。

但是 auto-fitminmax() 做的是同一件事,对吗? 不!上面的解决方案永远不会得到三列(或五列),而且列数不需要随着容器宽度的增加而增加。 尝试设置以下值 在这个 Pen 中 看看 Raven 如何完全发挥作用

--number_of_cols_4_wide: 1;
--number_of_cols_4_medium: 2;
--number_of_cols_4_small: 4;

额外 3:使用 linear-gradient() 更改 background-color

这一个有点令人费解。 Raven 都是关于长度值的,那么我们如何从这些值中得到颜色呢? 好吧,线性渐变 处理两者。 它们在由长度值定义的特定区域定义颜色。 在深入代码之前,让我们更详细地了解一下这个概念。

为了解决实际的渐变问题,一个众所周知的技术是 重复颜色停顿,有效地使渐变部分发生在 0px 内。 看一下这段代码,了解如何做到这一点

background-image:linear-gradient(
  to right,
  red 0%,
  red 50%,
  blue 50%,
  blue 100%
);

这将使您的背景在左侧一半为红色,在右侧为蓝色。 注意第一个参数“to right”。 这意味着百分比值是水平计算的,从左到右。

通过 Raven 变量控制 50% 的值允许随意移动颜色停顿。 我们可以添加更多颜色停顿。 在运行示例中,我们需要三种颜色,导致两个(加倍的)内部颜色停顿。

添加一些颜色和颜色停顿的变量,这就是我们得到的结果

background-image: linear-gradient(
  to right,
  var(--color_small) 0px,
  var(--color_small) var(--first_lgbreak_value),
  var(--color_medium) var(--first_lgbreak_value),
  var(--color_medium) var(--second_lgbreak_value),
  var(--color_wide) var(--second_lgbreak_value),
  var(--color_wide) 100%
);

但是我们如何计算 --first_lgbreak_value--second_lgbreak_value 的值? 让我们来看看。

第一个值控制 --color_small 在哪里可见。 在小间隔上,它应该是 100%,在其他间隔上是 0px。 我们已经了解了如何使用 Raven 来做到这一点。 第二个变量控制 --color_medium 的可见性。 它应该在小间隔上为 100%,在中间隔上为 100%,但在宽间隔上为 0px。 如果容器宽度位于小间隔或中间隔内,则相应的指示器必须为 1px

由于我们可以对指示器进行布尔逻辑运算,因此它是

max(--is_small, --is_medium)

… 以获得正确的指示器。 这给出

--first_lgbreak_value: min(var(--is_small) * var(--very_big_int), 100%);
--second_lgbreak_value: min(
  max(var(--is_small), var(--is_medium)) * var(--very_big_int), 100%);

将所有内容放在一起,得到以下 CSS 代码,以根据宽度更改 background-color(如上所示计算间隔指示器)

--first_lgbreak_value: min(
      var(--is_small) * var(--very_big_int), 100%);
--second_lgbreak_value: min(
    max(var(--is_small), var(--is_medium)) * var(--very_big_int), 100%);

--color_wide: red;/* change to your needs*/
--color_medium: green;/* change to your needs*/
--color_small: lightblue;/* change to your needs*/

background-image: linear-gradient(
  to right,
  var(--color_small) 0px,
  var(--color_small) var(--first_lgbreak_value),
  var(--color_medium) var(--first_lgbreak_value),
  var(--color_medium) var(--second_lgbreak_value),
  var(--color_wide) var(--second_lgbreak_value),
  var(--color_wide) 100%
);

这里有一个 Pen 来查看它的实际效果。

额外 4:摆脱嵌套变量

在使用 Raven 的过程中,我遇到了一个奇怪的问题:calc() 中可使用的嵌套变量数量有限。 当使用太多断点时,这会导致一些问题。 就我所知,此限制是为了防止在计算样式时页面阻塞,并允许更快地进行循环引用检查。

在我看来,类似于 *评估为值* 将是一种克服此问题的绝佳方法。 然而,这个限制在突破 CSS 限制时会让你头疼。 希望这个问题将在未来得到解决。

有一种方法可以在无需(深度)嵌套变量的情况下计算 Raven 的指示器变量。 让我们看一下 --is_medium 值的原始计算

--is_medium:calc(
  clamp(0px, 
        var(--base_size) - var(--breakpoint_medium), 
        1px) 
        - var(--is_wide)
); 

问题出现在 --is_wide 的减法中。 这会导致 CSS 解析器粘贴 --is_wide 的完整公式的定义。 --is_small 的计算甚至有更多这类引用。(--is_wide 的定义甚至会被粘贴两次,因为它隐藏在 --is_medium 的定义中,并且也被直接使用。)

幸运的是,有一种方法可以在不引用更大断点的指示器的情况下计算指示器。

当且仅当 --base_size 大于区间的较低断点且小于或等于区间的较高断点时,指示器为真。 此定义为我们提供了以下代码

--is_medium: 
  min(
    clamp(0px, var(--base_size) - var(--breakpoint_medium), 1px),
    clamp(0px, 1px + var(--breakpoint_wide) - var(--base_size), 1px)
  );
  • min() 用作逻辑 AND 运算符
  • 第一个 clamp() 是“--base_size 大于 --breakpoint_medium
  • 第二个 clamp() 意味着“--base_size 小于或等于 --breakpoint_wide。”
  • 添加 1px 将从“小于”切换到“小于 **或等于**”。 这是有效的,因为我们处理的是完整的(像素)数字(对于完整数字,a <= b 意味着 a < (b+1))。

可以用这种方式完成指示器变量的完整计算

--is_wide: clamp(0px, var(--base_size) - var(--breakpoint_wide), 1px);

--is_medium: min(clamp(0px, var(--base_size) - var(--breakpoint_medium), 1px),
                 clamp(0px, 1px + var(--breakpoint_wide) - var(--base_size), 1px)
             );

--is_small: clamp(0px,1px + var(--breakpoint_medium) - var(--base_size), 1px);

--is_wide--is_small 的计算更简单,因为每个只需要检查一个给定的断点。

这对我们目前所看到的所有内容都有效。 这里有一个 Pen 组合了示例。

最后的想法

Raven 无法完成媒体查询可以做到的所有事情。 但是我们不需要它那样做,因为我们在 CSS 中有媒体查询。 使用它们进行“大型”设计更改(例如侧边栏的位置或菜单的重新配置)就可以了。 这些事情发生在整个视窗(浏览器窗口的大小)的上下文中。

但是对于组件来说,媒体查询有点 *错误*,因为我们永远不知道组件将如何调整大小。

Heydon Pickering 用这张图片展示了这个问题

Three boxes representing browsers from left-to-right. The first is a wide viewport with three boxes in a single row. The second is a narrow viewport with the boxes stacked vertically. The third is a wide viewport, but with a dashed vertical line down the middle representing a container and the three boxes are to the right of it in a single row.

我希望 Raven 能帮助你克服为组件创建响应式布局的问题,并将“CSS 可以做什么”的限制推进一步。

通过展示今天可能实现的功能,也许可以通过添加一些语法糖和一些非常小的新函数(如 conWconH、“strip-unit” 或“evaluate-to-pixels”)来实现“真正的”容器查询。 如果 CSS 中有一个函数可以将“1px”重写为空格,并将“0px”重写为“initial”,那么 Raven 可以与 自定义属性切换技巧 相结合,改变所有 CSS 属性,而不仅仅是长度值。

通过避免使用 JavaScript 来实现这一点,您的布局将渲染得更快,因为它不依赖于 JavaScript 的下载或运行。 即使 JavaScript 被禁用,它也不重要。 这些计算不会阻塞您的主线程,您的应用程序逻辑也不会被设计逻辑所阻塞。


感谢 ChrisAndrés GalanteCathy DuttonMarko IlicDavid Atanda 的精彩 CSS-Tricks 文章。 他们确实帮助我探索了 Raven 的可能性。