前端开发者学堂 - fedev.cn

CSS条件变量

发布于 shadow walker

我将从这里说起: 在W3C标准规范中,使用CSS变量并没有任何条件。我认为这是规范文档中的一个很大的缺陷,CSS变量已经实现了很多我们之前无法实现的功能,并且我们未来可能大量使用,缺失文档描述着实让人沮丧。

但是如果现在我们需要这些对CSS变量的描述怎么办?好,借助其他的CSS知识,我们可以在一些实例中略窥一二。

问题定义

我们需要的是使用一个单一的CSS变量来给不同的CSS属性设置不同的值。这些值并不会基于CSS变量(因为这些值并不会由我们的变量计算得出)。我们需要的是条件。

使用二进制计算

长话短说, 我先列出一些解决策略,然后再详诉。

:root {
    --is-big: 0;
}

.is-big {
    --is-big: 1;
}

.block {
    padding: calc(
        25px * var(--is-big) +
        10px * (1 - var(--is-big))
    );
    border-width: calc(
        3px * var(--is-big) +
        1px * (1 - var(--is-big))
    );
}

在这个例子中,我们给所有.block元素设置 10px 内边距(padding)和 1pxborder-width) 的边框,条件是作用在那些元素上的 --is-big 变量不为1,否则,元素内边距和边框分别为 25px3px

CSS变量机制也很简单: 我们在calc()使用了一个CSS变量的两个可能值,--is-big 要么为0,要么为1。换句话说,.block 的内边距要么为 25px * 1 + 10px * 025px) 要么为 25px * 0 + 10px * 1(10px)。

更复杂的情况

我们可以用这种方法去选择CSS变量可能超过2个值。相应地,每增加一个可能值,表达式也就变的越复杂。选择3个值进看起来像这样。

.block {
    padding: calc(
        100px * (1 - var(--foo)) * (2 - var(--foo)) * 0.5 +
         20px * var(--foo) * (2 - var(--foo)) +
          3px * var(--foo) * (1 - var(--foo)) * -0.5
    );
}

--foo 的值可能是01或者2padding的值也相应的可能为100px200px或者3px

原理是相通的: 我们只需要将这个表达式和每一个可能值相乘就好了,结果为1的是我们需要的,其他则为0。组成这个表达式也很简单。我们只需要使另外的可能值生效就好了。设计好之后,我们需要添加一个触发值以此来调整结果以致于它等于1,就是这样了。

可能存在的陷阱

随着计算式越来越复杂,有可能会出错。为什么?规范中是这样描述的:

UAs 支持至少20calc() 表达式,number,dimension 或者percentage 都是表达式。如果calc()包含了它所支持以外的表达式,它会把这些表达式视作无效。

当然,我自己在浏览器测试了一下,并没有这样的问题。但是你写了非常复杂的代码仍然有这样的可能。或者在将来某些浏览器会有这样的问题,因此当使用非常复杂的计算式时,请小心为上。

根据条件获取颜色

正如你看到的,这些计算表达式只能用在那些可计算的属性上面,因此我们无法使用它改变display的值或者计算一些非数字的属性。但是,对于颜色(color)呢?事实上,我们能计算颜色的各个组成部分。不幸的是,现在只有webkit和blink 浏览器引擎能支持颜色的计算,因为火狐现在还不支持在calc()里使用rgba()和其他颜色函数。 但是当浏览器支持的时候(或者你在支持颜色计算的浏览器上想体验一下),我们可以这样做。

:root {
    --is-red: 0;
}

.block {
    background: rgba(
        calc(
            255*var(--is-red) +
            0*(1 - var(--is-red))
            ),
        calc(
            0*var(--is-red) +
            255*(1 - var(--is-red))
            ),
        0, 1);
}

这里我们把lime颜色设置成默认值,如果--is-red被设置成1,那么得到的颜色就会变成red(**注意:**如果我们完全忽略CSS表达式,颜色的各个部分则为0,这使我们的代码更简洁,在这里,我尽量让CSS变量计算变得更清楚)。

借此你对于可计算的属性可以做任何运算,甚至可能创造出任何颜色(包含渐变?你可以试试)。

规则中另一个陷阱

当我们在测试如何产生我们想要的颜色时,我发现了在规范文档中存在一个非常奇怪的现象,它被叫做"类型检查"。我现在很讨厌它。意味着属性值只接受整数值(<integer>),如果你在calc()里存在除法运算或一些非整数,即便结果是整数,"处理后的类型"也不会是整数,它可能是数字,意味着我们无法给属性设置我们写入的值。当这些计算式包含两个可能值的时候,我们需要一个非整数指示器。当我们使用颜色或者其他只允许整数的属性时(像z-index)时能够使表达式无效。比如:

calc(255 * (1 - var(--bar)) * (var(--bar) - 2) * -0.5)

rgba()里时使用这个calc()是无效的。最开始的时候我认为这是个bug,尤其是知道rgba()可以接受超过255的值(rgba(9001, +9001, -9001, 43)得到的是一个正常的黄色(yellow)),但是这种类型检查似乎对于浏览器来说太难了(因为乘了个0.5,非整数)。

能否解决?

这里有一种远不完美的解决策略。因为我们知道我们想要的值和非整数指示器,我们只要先计算出来然后四舍五入一下。嗯,这意味着结果在某些时候可能不太精确,但总好过什么都没有吧。

但是这里还有一种策略可以应对颜色,我们可以使用hsla,而不是rgba,因为hsla不接受非整数,而是接受数字和百分比。因此在类型检查中不存在冲突。但是对于特定的属性例如z-index,还是没辙。因为即便这样,如果从rgb转到hsl还是会存在精度的损失。但是这相对于前面的方法还是损失小些。

预处理

当存在分支时,我们仍然可以手写CSS。但是当我们遇到非常复杂的条件时,或者当我们要拿到颜色时,我们最好使用工具这样可以让书写变的更方便。幸运的是,我们正有这样的预处理器。

这是stylus里如何书写的例子:

conditional($var, $values...)
  $result = ''

  // If there is only an array passed, use its contents
  if length($values) == 1
    $values = $values[0]

  // Validating the values and check if we need to do anything at all
  $type = null
  $equal = true

  for $value, $i in $values
    if $i > 0 and $value != $values[0]
      $equal = false

    $value_type = typeof($value)
    $type = $type || $value_type
    if !($type == 'unit' or $type == 'rgba')
      error('Conditional function can accept only numbers or colors')

    if $type != $value_type
      error('Conditional function can accept only same type values')

  // If all the values are equal, just return one of them
  if $equal
    return $values[0]

  // Handling numbers
  if $type == 'unit'
    $result = 'calc('
    $i_count = 0
    for $value, $i in $values
      $multiplier = ''
      $modifier = 1
      $j_count = 0
      for $j in 0..(length($values) - 1)
        if $j != $i
          $j_count = $j_count + 1
          // We could use just the general multiplier,
          // but for 0 and 1 we can simplify it a bit.
          if $j == 0
            $modifier = $modifier * $i
            $multiplier = $multiplier + $var
          else if $j == 1
            $modifier = $modifier * ($j - $i)
            $multiplier = $multiplier + '(1 - ' + $var + ')'
          else
            $modifier = $modifier * ($i - $j)
            $multiplier = $multiplier + '(' + $var + ' - ' + $j + ')'

          if $j_count < length($values) - 1
            $multiplier = $multiplier + ' * '

      // If value is zero, just don't add it there lol
      if $value != 0
        if $modifier != 1
          $multiplier = $multiplier + ' * ' + (1 / $modifier)
        $result = $result + ($i_count > 0 ? ' + ' : '') + $value + ' * ' + $multiplier
        $i_count = $i_count + 1

    $result = $result + ')'

  // Handling colors
  if $type == 'rgba'
    $hues = ()
    $saturations = ()
    $lightnesses = ()
    $alphas = ()

    for $value in $values
      push($hues, unit(hue($value), ''))
      push($saturations, saturation($value))
      push($lightnesses, lightness($value))
      push($alphas, alpha($value))

    $result = 'hsla(' + conditional($var, $hues) + ', ' + conditional($var, $saturations) + ', ' + conditional($var, $lightnesses) + ', ' + conditional($var, $alphas) +  ')'

  return unquote($result)

是的,这里代码比较多,但是这个mixin可以在多个值的条件下产生number 和 colors 的色值。

使用也很简单:

border-width: conditional(var(--foo), 10px, 20px)

第一个参数变量是我们的CSS变量,第二个参数变量是当CSS变量为0时被应用,第三个参数变量是当CSS变量为1时被应用。

上面的调用会产生一个适当的条件:

border-width: calc(10px * (1 - var(--foo)) + 20px * var(--foo));

这里有一个更复杂的示例:

color: conditional(var(--bar), red, lime, rebeccapurple, orange)

可能生成你不想手写的代码:

color: hsla(calc(120 * var(--bar) * (var(--bar) - 2) * (var(--bar) - 3) * 0.5 + 270 * var(--bar) * (1 - var(--bar)) * (var(--bar) - 3) * 0.5 + 38.82352941176471 * var(--bar) * (1 - var(--bar)) * (var(--bar) - 2) * -0.16666666666666666), calc(100% * (1 - var(--bar)) * (var(--bar) - 2) * (var(--bar) - 3) * 0.16666666666666666 + 100% * var(--bar) * (var(--bar) - 2) * (var(--bar) - 3) * 0.5 + 49.99999999999999% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 3) * 0.5 + 100% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 2) * -0.16666666666666666), calc(50% * (1 - var(--bar)) * (var(--bar) - 2) * (var(--bar) - 3) * 0.16666666666666666 + 50% * var(--bar) * (var(--bar) - 2) * (var(--bar) - 3) * 0.5 + 40% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 3) * 0.5 + 50% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 2) * -0.16666666666666666), 1);

注意这里没有整数监测的属性,因此这里对于z-index等属性不会生效。但是它已经将颜色转化成了hsla,使它们变的有效(尽管只有这种转换只有我们需要的时候,它才有所优势)。另外在这个mixin中我们无法执行的是使用CSS变量作为属性值。这对于非整数的属性也是可以实现的,因为我们的条件表达式中是可以插入CSS变量的。也许,等我有空了,我会让这个mixin接受CSS变量。目前,用本文解释的算法也是可能实现的。

降级

当然,如果你打算真的使用它,你需要使用一个方式来降级。这很容易应对浏览器不支持CSS变量的时候:你可以在条件申明前使用回退值。

.block {
    padding: 100px; /* fallback */
    padding: calc(
        100px * ((1 - var(--foo)) * (2 - var(--foo)) / 2) +
         20px * (var(--foo) * (2 - var(--foo))) +
          3px * (var(--foo) * (1 - var(--foo)) / -2)
    );
}

但是当涉及到颜色时,我们会遇到一个问题:当浏览器支持CSS变量时,任何包含CSS变量的申明都会被认为有效。这意味着在CSS中,我们是不太可能在包含CSS变量的申明中做回退的。

在有效的CSS中,background会得到一个初始值(initial),不是在回退中我们提供的。因此我们需要提供在一些场景中使用降级处理——使用@support来测试除了有效的CSS变量其他值。

在我们的例子中,火狐浏览器中我们需要像这样处理:

.block {
    color: #f00;
}
@supports (color: rgb(0, calc(0), 0)) {
    .block {
        color: rgba(calc(255 * (1 - var(--foo))), calc(255 * var(--foo)), 0, 1);
  }
}

这里我们测试一下在color 函数中使用calc() 并且做为color的值。

这里也可能自动创建回退,但这里我并不推荐你使用预处理器处理回退,因为创建这些东西的复杂度超越了CSS预处理器的能力。

用例

我真的不喜欢为那种一目了然的东西提供使用场景。这里我就简单提一下吧。这里不仅仅是CSS变量的,还有一些生成条件,像calc()的结果;

  • CSS变量应用在那些主题分明的块上是绝佳的。你可能有很多的块,然后要把主题应用在这些块上,你只需要一个像--block--variant:1的CSS变量。当我们在不同的主题中想给不同的属性设置不同的值时,CSS变量可能是最好的做法了,否则的话,你可能需要给每一种主题制定一个变量。
  • 排版。如果我们在CSS变量中使用<,<=,>,>=等判断条件,那么可能会应用不同的规则采用不同同的字体,因此你可以设置不同的行高(line-height),字体粗细(font-weight)和基于字号(font-size)的其他属性。
  • 响应式设计。如果对表达式设置以条件,这不就变成媒体查询了嘛。你可以使用vw或百分比,决定具体条件下,使用哪个样式。

当然也会有其他的应用场景,发现后一定要告诉我。我很确信还有别的东西,但是我记性不太好,记不住所有曾经我想用CSS做的东西。

未来特性

我真的很希望在CSS规范文档中有具体的描述,而不用依赖calc hack 或使用一些特殊值来应对不能计算的属性值。现在我们不可能有除了严格相等其他之外的条件,因此没有"变量大于x"或者其他之类的。我不知道为什么我们我能使用这些条件,如果你认识文档开发商,帮我给他们提点意见。我希望他们别以“用JS”或者为什么这样不可能的理由来搪塞我。

本文根据@kizmarh的《Conditions for CSS Variables》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://kizu.ru/en/fun/conditions-for-css-variables/

Shadow Walker

一名在校研究生,热爱前端,热爱英语,热爱摄影,喜欢一切能表达情绪的东西,喜欢黑色,喜欢秋天,喜欢孤独。。

如需转载,烦请注明出处:https://www.fedev.cn/css/conditions-for-css-variables.htmljordan retro 11 mens boots