【译】使用 CSS 变量组合不可组合的内容

avatar
Advanced web developers @AOTHHOME

许多 CSS 属性是一组其他属性的简写,例如,该 margin 属性是同时设置 margin-top 、 margin-right 、和 margin-bottom margin-left 全部的简写。 因为该 margin 属性分解为这四个独立的属性,所以它可以很好地转换为像 Tailwind CSS 这样的实用程序类系统,我们可以为每个属性创建单独的实用程序类,然后在 HTML 中任意组合它们:

<style>
  .mt-2 {
    margin-top: 0.5rem;
  }
  /* ... */
  .mr-6 {
    margin-right: 1.5rem;
  }
  /* ... */
  .mb-8 {
    margin-bottom: 2rem;
  }
  /* ... */
  .ml-4 {
    margin-left: 1rem;
  }
</style>

<div class="mt-2 mr-6 mb-8 ml-4">
  <!-- ... -->
</div>

如果没有这种可组合性,基本上不可能在纯CSS中完成所有可以做的事情,因为必须存在组合爆炸才能支持每个值组合的类。

例如:

.mt-2_mr-0_mb-0_ml-0 {
  margin: 0.5rem 0 0 0;
}
.mt-2_mr-0_mb-0_ml-1 {
  margin: 0.5rem 0 0 0.25rem;
}
.mt-2_mr-0_mb-0_ml-2 {
  margin: 0.5rem 0 0 0.5rem;
}
/* ... */
.mt-2_mr-0_mb-1_ml-0 {
  margin: 0.5rem 0 0.25rem 0rem;
}
.mt-2_mr-0_mb-1_ml-1 {
  margin: 0.5rem 0 0.25rem 0.25rem;
}
.mt-2_mr-0_mb-1_ml-2 {
  margin: 0.5rem 0 0.25rem 0.5rem;
}
/* ... */
.mt-48_mr-48_mb-48_ml-48 {
  margin: 12rem 12rem 12rem 12rem;
}

如果我们必须在 Tailwind 中执行此操作,则开发版本将类似于 4gb(而不是精益求精的 3mb,哈哈)。

可悲的是,一些 CSS 属性不会分解成单独的属性。一个例子是 transform (尽管有人提议将其拆分!

.awesomeify {
  transform: translateX(3rem) rotate(90deg) scale(1.5);
}

如果我们想按 X 平移一个元素,用 Y 旋转它,然后用 Z 缩放它,则无法使用三个单独的实用程序类来做到这一点。或许有?

CSS变量简介

CSS 自定义属性(通常称为 CSS 变量)允许您为值创建单一事实来源,并在 CSS 的其他部分引用它:

:root {
  --color-brand: #0d84ff;
}

.btn-primary {
  background-color: var(--color-brand);
}

.link {
  color: var(--color-brand);
}

CSS 变量很酷的地方在于,与 Sass/Less/Stylus/potato 不同,它们是在运行时而不是在构建时计算的,因此它们可以更改。

例如,默认情况下,您可以将标题颜色设置为黑色:

:root {
  --headline-color: #000;
}

.headline {
  color: var(--headline-color);
}

...然后,每当它位于父级中时,它就会神奇地将其覆盖为白色 dark :

.dark {
  --headline-color: #fff;
}
<h1 class="heading">My text is black</h1>

<div class="dark">
  <h1 class="heading">My text is white</h1>
</div>

对部分值使用 CSS 变量

这本身就很酷,但更酷的是,CSS变量不必表示一个完整的值——它们也可以用于部分值。

例如,您可以仅将颜色的 RGB 通道存储在变量中,并将其与不透明度值一起粘贴到函数中 rgba() :

:root {
  --rgb-brand: 13, 132, 255;
}

.btn-primary--faded {
  background-color: rgba(var(--rgb-brand), 0.75);
}

这就是 Tailwind 的文本不透明度实用程序的工作方式。

每个文本颜色实用程序都将一个 --text-opacity 变量设置为 1 ,然后为 rgba 值的 alpha 通道引用它:

.text-blue-300 {
  --text-opacity: 1;
  color: rgba(144, 205, 244, var(--text-opacity));
}

Then the actual text opacity utilities simply change that variable to another value:
然后,实际的文本不透明度实用程序只需将该变量更改为另一个值:

.text-opacity-50 {
  --text-opacity: 0.5;
}

由于我们在文本颜色实用程序之后定义了文本不透明度实用程序,因此文本不透明度实用程序中的变量值始终优先于文本颜色实用程序中设置的值:

<h1 class="text-blue-300 text-opacity-50">
  I'm blue at 50% opacity.
</h1>

如果没有这种技术,就不可能使用类独立地控制颜色和不透明度,因为所有工作都需要在一个CSS属性中完成。

使用 CSS 变量填充“插槽”

那么,我们最初的问题,试图使 transform 属性可组合呢?

我们在 Tailwind 中的做法是为各种变换特征创建变量,并将它们组合在一起以创建整个变换值。

下面是一个稍微简化的版本(我们还允许您独立缩放 X 和 Y,以及倾斜):

.transform {
  --transform-translate-x: 0;
  --transform-translate-y: 0;
  --transform-rotate: 0;
  --transform-scale: 1;
  transform: translateX(var(--transform-translate-x)) translateY(var(--transform-translate-y)) rotate(var(--transform-rotate)) scale(var(--transform-scale));
}

您将看到,我们在这里所做的是在转换值中创建“槽”,每个槽对应一个 translateX :、 translateY 、 rotate scale 和 。

默认情况下,我们用“无操作”值填充每个插槽,例如旋转 0 度或缩放 1 倍。

然后,我们定义仅操作 CSS 变量的其他类:

.translate-x-2 {
  --transform-translate-x: 0.5rem
}
.translate-y-3 {
  --transform-translate-x: 1.5rem
}
.rotate-45 {
  --transform-rotate: 45deg
}
.scale-150 {
  --transform-scale: 1.5;
}

同样,由于这些实用程序是在 transform 实用程序之后定义的,因此它们会覆盖变量,而不会真正破坏 transform 属性本身:

<div class="transform translate-y-3 rotate-45 scale-150">
  <!-- ... -->
</div>

transform 类“启用”转换,每个变量的类只操作单个转换“槽”中的一个。

这需要传统上不可组合的属性,并使其可以使用 HTML 中的多个类完美组合。

使用空变量连接可选值

这就是事情变得非常疯狂的地方。

假设您正在尝试使 font-variant-numeric 类似属性的东西可组合。

它由一堆不同的可选块组成:

font-variant-numeric: {ordinal?} {slashed-zero?} {figure-value?} {spacing-value?} {fraction-value?}

所以所有这些都是有效的:

.my-class {
  font-variant-numeric: ordinal;
  font-variant-numeric: slashed-zero;
  font-variant-numeric: ordinal slashed-zero;
  font-variant-numeric: ordinal tabular-nums;
  font-variant-numeric: slashed-zero lining-nums;
  font-variant-numeric: slashed-zero diagonal-fractions;
  font-variant-numeric: ordinal tabular-nums diagonal-fractions;
  font-variant-numeric: ordinal slashed-zero oldstyle-nums tabular-nums diagonal-fractions;
  /* Etc. */
}

根据你上面学到的知识,你可能会想尝试这样的事情:

.variant-numeric {
  font-variant-numeric: var(--variant-ordinal) var(--variant-slashed-zero) var(--variant-figure) var(--variant-spacing) var(--variant-fractions);
}
.ordinal {
  --variant-ordinal: ordinal;
}
.slashed-zero {
  --variant-slashed-zero: slashed-zero;
}
/* ... */
.stacked-fractions {
  --variant-fractions: stacked-fractions;
}
.diagonal-fractions {
  --variant-fractions: diagonal-fractions;
}

问题在于,以这种方式做事,我们的变量 like --variant-ordinal 没有默认值,这意味着当尝试解析时 var(--variant-ordinal) ,它将是规范所说的无效值,这实际上使整个规则无效,因此根本没有应用 CSS。

所以你可能会想,也许我们可以默认为空字符串?

--variant-ordinal: '';

问题是这实际上根本不是一个空字符串——在 CSS 中,这是字面值 '' 。请记住,CSS 值不带引号。

那么我们能做些什么呢?事实证明空格是一个有效的CSS值。

--variant-ordinal: ;

是的,这不是语法错误,这是设置为文字空格字符的完全有效的 CSS --variant-ordinal 。

现在,当解析时 var(--variant-ordinal) ,我们将得到一个空格,这在值列表中是完全允许的。

以下是工作解决方案的样子:

.variant-numeric {
  --variant-ordinal: ;
  --variant-slashed-zero: ;
  --variant-figure: ;
  --variant-spacing: ;
  --variant-fractions: ;
  font-variant-numeric: var(--variant-ordinal) var(--variant-slashed-zero) var(--variant-figure) var(--variant-spacing) var(--variant-fractions);
}
.ordinal {
  --variant-ordinal: ordinal;
}
.slashed-zero {
  --variant-slashed-zero: slashed-zero;
}
/* ... */
.stacked-fractions {
  --variant-fractions: stacked-fractions;
}
.diagonal-fractions {
  --variant-fractions: diagonal-fractions;
}

唯一的问题是它可能在您的生产版本中不起作用。

如果你使用任何类型的CSS压缩器,几乎可以保证在我的测试中去除这些变量。事实上,这是唯一真正有效的方法:

.variant-numeric {
  --variant-ordinal: var(--not-a-real-variable,/*!*/ /*!*/);
  /* ... */
}

搞什么?是的,我同意。基本上,我们在这里所做的是欺骗缩小器保留出现在两个注释之间的空间。我们必须输入 ! 注释来告诉缩小器而不是剥离它们,并且我们必须将所有这些作为不存在的变量的默认值来执行,否则缩小器将看到该值为空并剥离整个声明。

好的部分是,当 PostCSS 8 获得大规模采用时,这实际上应该被修复,所以我们将能够回到这个看起来不那么奇怪的语法:

.variant-numeric {
  --variant-ordinal: ;
  /* ... */
}

CSS,真是一场骚乱。

参考文献

adamwathan.me/composing-t…