当 Sass 和新的 CSS 特性发生冲突

Avatar of Ana Tudor
Ana Tudor

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

最近,CSS 添加了许多很酷的新特性,例如 自定义属性 和新的 函数。虽然这些东西可以使我们的生活变得更容易,但它们也可能以有趣的方式与预处理器(如 Sass)发生交互。

因此,这将是一篇关于我遇到的问题、我如何解决这些问题以及为什么我今天仍然认为 Sass 是必要的文章。

错误

如果您使用过新的 min()max() 函数,当使用不同的单位时,您可能会遇到类似这样的错误消息:“不兼容的单位:vhem。”

Screenshot. Shows the `Incompatible units: 'em' and 'vh'` error when trying to set `width: min(20em, 50vh)`.
min()/ max() 函数中使用不同类型的单位时发生的错误

这是因为 **Sass 有自己的 min() 函数,并且忽略了 CSS min() 函数**。此外,Sass 无法使用两个单位之间没有固定关系的值进行任何类型的计算。

例如,cmin 单位之间存在固定关系,因此 Sass 可以计算出 min(20in, 50cm) 的结果,并且在我们在代码中尝试使用它时不会抛出错误。

其他单位也是如此。例如,角度单位之间都存在固定关系:1turn1rad1grad 始终计算为相同的 deg 值。同样,1s 始终为 1000ms1kHz 始终为 1000Hz1dppx 始终为 96dpi1in 始终为 96px。这就是为什么 Sass 可以 在它们之间进行转换 并在计算和函数(如它自己的 min() 函数)中混合它们。

但是当这些单位之间没有固定关系时(例如前面 emvh 单位的情况),就会出现问题。

而且不仅仅是不同的单位。尝试在 min() 中使用 calc() 也会导致错误。如果我尝试类似 calc(20em + 7px) 的操作,我得到的错误是,“calc(20em + 7px) 不是 min 的数字。”

Screenshot. Shows the `'calc(20em + 7px)' is not a number for 'min'` error when trying to set `width: min(calc(20em + 7px), 50vh)`.
min() 函数中嵌套使用 calc() 时,使用不同单位值发生的错误

当我们想要在 CSS 滤镜(如 invert())中使用 CSS 变量或数学 CSS 函数(如 calc()min()max())的结果时,就会出现另一个问题。

在这种情况下,我们会收到提示:“$color: 'var(--p, 0.85) 不是 invert 的颜色。”

Screenshot. Shows the `$color: 'var(--p, 0.85)' is not a color for 'invert'` error when trying to set `filter: invert(var(--p, .85))`.
var()filter: invert() 中发生的错误

grayscale() 也会发生同样的情况:“$color: ‘calc(.2 + var(--d, .3))‘ 不是 grayscale 的颜色。”

Screenshot. Shows the `$color: 'calc(.2 + var(--d, .3))' is not a color for 'grayscale'` error when trying to set `filter: grayscale(calc(.2 + var(--d, .3)))`.
calc()filter: grayscale() 中发生的错误

opacity() 也会导致同样的问题:“$color: ‘var(--p, 0.8)‘ 不是 opacity 的颜色。”

Screenshot. Shows the `$color: 'var(--p, 0.8)' is not a color for 'opacity'` error when trying to set `filter: opacity(var(--p, 0.8))`.
var()filter: opacity() 中发生的错误

但是,其他 filter 函数——包括 sepia()blur()drop-shadow()brightness()contrast()hue-rotate()——都可以与 CSS 变量一起正常工作!

事实证明,发生的情况与 min()max() 问题类似。Sass 没有内置的 sepia()blur()drop-shadow()brightness()contrast()hue-rotate() 函数,但它确实有自己的 grayscale()invert()opacity() 函数,并且它们的第一个参数是 $color 值。由于它没有找到该参数,因此会抛出错误。

出于同样的原因,当我们尝试使用列出至少两个 hsl()hsla() 值的 CSS 变量时,也会遇到麻烦。

Screenshot. Shows the `wrong number of arguments (2 for 3) for 'hsl'` error when trying to set `color: hsl(9, var(--sl, 95%, 65%))`.
var()color: hsl() 中发生的错误。

另一方面,color: hsl(9, var(--sl, 95%, 65%)) 是完全有效的 CSS,并且在没有 Sass 的情况下也能正常工作。

rgb()rgba() 函数也会发生完全相同的事情。

Screenshot. Shows the `$color: 'var(--rgb, 128, 64, 64)' is not a color for 'rgba'` error when trying to set `color: rgba(var(--rgb, 128, 64, 64), .7)`.
var()color: rgba() 中发生的错误。

此外,如果我们导入 Compass 并尝试在 linear-gradient()radial-gradient() 中使用 CSS 变量,我们会得到另一个错误,即使在 conic-gradient() 中使用变量也能正常工作(也就是说,如果浏览器支持它)。

Screenshot. Shows the At least two color stops are required for a linear-gradient error when trying to set background: linear-gradient(var(--c, pink), gold).
var()background: linear-gradient() 中发生的错误。

这是因为 Compass 带有 linear-gradient()radial-gradient() 函数,但从未添加过 conic-gradient() 函数。

在所有这些情况下,问题都源于 Sass 或 Compass 具有同名的函数,并假设这些是我们打算在代码中使用的函数。

哎呀!

解决方案

这里的技巧是记住 **Sass 区分大小写,但 CSS 不区分大小写。**

这意味着我们可以编写 Min(20em, 50vh),Sass 不会将其识别为它自己的 min() 函数。不会抛出错误,并且它仍然是有效的 CSS,可以按预期工作。类似地,编写 HSL()/ HSLA()/ RGB()/ RGBA()Invert() 可以让我们避免之前遇到的问题。

至于渐变,我通常更喜欢 linear-Gradient()radial-Gradient(),因为它更接近 SVG 版本,但在其中使用至少一个大写字母也可以正常工作。

但是为什么?

几乎每次我在 Twitter 上发布任何与 Sass 相关的内容时,都会有人告诉我现在我们有了 CSS 变量,就不应该使用 Sass 了。我想解决这个问题并解释为什么我不同意。

首先,虽然我发现 CSS 变量非常有用,并且在过去三年中几乎在所有地方都使用了它们,但需要注意的是,它们会带来性能成本,并且在 calc() 计算的迷宫中追踪错误的来源,使用我们当前的 DevTools 可能会很痛苦。我尽量避免过度使用它们,以免陷入使用它们的缺点大于好处的境地。

Screenshot. Shows how `calc()` expressions are presented in DevTools.
不容易弄清楚这些 calc() 表达式的结果是什么。

一般来说,如果它像常量一样工作,不会因元素或状态而改变(在这种情况下,自定义属性绝对是 最佳选择)或减少编译后的 CSS 的数量(解决前缀带来的重复问题),那么我就会使用 Sass 变量。

其次,变量一直是我使用 Sass 的原因中很小的一部分。当我从 2012 年底开始使用 Sass 时,主要是因为循环,这是 CSS 中仍然没有的功能。虽然我已经将部分循环转移到 HTML 预处理器(因为它减少了生成的代码,并且避免了以后需要同时修改 HTML 和 CSS),但我仍然在很多情况下使用 Sass 循环,例如生成值的列表、渐变函数中的停止列表、多边形函数中的点列表、变换列表等等。

举个例子。我以前使用预处理器生成 n 个 HTML 项目。预处理器的选择关系不大,但这里我将使用 Pug。

- let n = 12;

while n--
  .item

然后,我将 $n 变量设置到 Sass 中(它必须与 HTML 中的变量相等),并循环到该变量以生成定位每个项目的变换

$n: 12;
$ba: 360deg/$n;
$d: 2em;

.item {
  position: absolute;
  top: 50%; left: 50%;
  margin: -.5*$d;
  width: $d; height: $d;
  /* prettifying styles */

  @for $i from 0 to $n {
    &:nth-child(#{$i + 1}) {
      transform: rotate($i*$ba) translate(2*$d) rotate(-$i*$ba);
			
      &::before { content: '#{$i}' }
    }
  }
}

但是,这意味着在更改项目数量时,我必须同时更改 Pug 和 Sass,这使得生成的代码非常重复。

Screenshot. Shows the generated CSS, really verbose, almost completely identical transform declaration repeated for each item.
上面代码生成的 CSS

我现在改为让 Pug 生成索引作为自定义属性,然后在 transform 声明中使用这些属性。

- let n = 12;

body(style=`--n: ${n}`)
  - for(let i = 0; i < n; i++)
    .item(style=`--i: ${i}`)
$d: 2em;

.item {
  position: absolute;
  top: 50%;
  left: 50%;
  margin: -.5*$d;
  width: $d;
  height: $d;
  /* prettifying styles */
  --az: calc(var(--i)*1turn/var(--n));
  transform: rotate(var(--az)) translate(2*$d) rotate(calc(-1*var(--az)));
  counter-reset: i var(--i);
	
  &::before { content: counter(i) }
}

这大大减少了生成的代码。

Screenshot. Shows the generated CSS, much more compact, no having almost the exact same declaration set on every element separately.
上面代码生成的 CSS

但是,如果我想生成彩虹之类的效果,仍然需要在 Sass 中使用循环。

@function get-rainbow($n: 12, $sat: 90%, $lum: 65%) {
  $unit: 360/$n;
  $s-list: ();
	
  @for $i from 0 through $n {
    $s-list: $s-list, hsl($i*$unit, $sat, $lum)
  }
	
  @return $s-list
}

html { background: linear-gradient(90deg, get-rainbow()) }

当然,我可以从 Pug 生成它作为列表变量,但这并没有利用 CSS 变量的动态特性,也没有减少发送到浏览器的代码量,因此没有任何好处。

我使用 Sass(和 Compass)的另一个重要部分与内置的数学函数(如三角函数)相关,这些函数现在是 CSS 规范的一部分,但尚未在任何浏览器中实现。Sass 也没有这些函数,但 Compass 有,这就是为什么我经常需要使用 Compass 的原因。

当然,我可以在 Sass 中编写自己的这些函数。在 Compass 支持反三角函数之前,我确实求助于此。我真的很需要它们,所以我根据 泰勒级数 编写了自己的函数。但 Compass 现在提供了这些类型的函数,它们比我的函数更好,性能也更高。

数学函数对我来说极其重要,因为我是一名技术人员,而不是艺术家。我的 CSS 中的值通常是数学计算的结果。它们不是魔法数字,也不是纯粹为了美观而使用的数字。一个例子是生成创建规则或准规则多边形的剪辑路径点的列表。想想我们需要创建非矩形头像或贴纸的情况。

让我们考虑一个正多边形,其顶点位于一个圆上,该圆的半径是我们开始的正方形元素的50%。在以下演示中拖动滑块,我们可以看到对于不同数量的顶点,这些点是如何放置的。

将其转换为Sass代码,我们得到

@mixin reg-poly($n: 3) {
  $ba: 360deg/$n; // base angle
  $p: (); // point coords list, initially empty
	
  @for $i from 0 to $n {
    $ca: $i*$ba; // current angle
    $x: 50%*(1 + cos($ca)); // x coord of current point
    $y: 50%*(1 + sin($ca)); // y coord of current point
    $p: $p, $x $y // add current point coords to point coords list
  }
	
  clip-path: polygon($p) // set clip-path to list of points
}

请注意,在这里我们也使用了循环以及条件语句和取模运算,这些在不使用Sass的情况下使用CSS时非常麻烦。

这个代码的稍微更高级的版本可能涉及到通过向每个顶点的角度添加相同的偏移角($oa)来旋转多边形。这可以在以下演示中看到。此示例中加入了一个星形混合宏,其工作方式类似,只是我们始终具有偶数个顶点,并且每个奇数索引的顶点都位于较小半径($f*50%,其中$f小于1)的圆上。

我们也可以得到像这样的圆润的星星

或者带有有趣border图案的贴纸。在这个特定的演示中,每个贴纸都是用一个HTML元素创建的,border图案是用Sass中的clip-path、循环和数学运算创建的。事实上,其中包含相当多的数学运算。

另一个例子是这些卡片背景,其中循环、取模运算和指数函数协同工作以生成抖动像素背景层。

此演示碰巧也大量依赖于CSS变量。

然后还有使用混合宏来避免在为诸如范围输入之类的元素设置样式时一遍又一遍地编写完全相同的声明。不同的浏览器使用不同的伪元素来设置此类控件的组件样式,因此对于每个组件,我们都必须设置控制其外观的多个伪元素的样式。

遗憾的是,尽管将此放入我们的CSS中可能很诱人

input::-webkit-slider-runnable-track, 
input::-moz-range-track, 
input::-ms-track { /* common styles */ }

…但我们无法做到这一点,因为它不起作用!如果即使其中一个选择器不被识别,整个规则集也会被丢弃。由于没有任何浏览器识别以上所有三个选择器,因此样式在任何浏览器中都不会应用。

如果我们希望应用我们的样式,则需要类似这样的内容

input::-webkit-slider-runnable-track { /* common styles */ }
input::-moz-range-track { /* common styles */ }
input::-ms-track { /* common styles */ }

但这可能意味着许多相同的样式重复三次。如果我们想更改轨道background,我们需要在::-webkit-slider-runnable-track样式、::-moz-range-track样式和::-ms-track样式中更改它。

我们拥有的唯一合理的解决方案是使用混合宏。样式在编译后的代码中重复,因为它们必须在那里重复,但我们不再需要三次编写相同的内容了。

@mixin track() { /* common styles */ }

input {
  &::-webkit-slider-runnable-track { @include track }
  &::-moz-range-track { @include track }
  &::-ms-track { @include track }
}

底线是:是的,在2020年,Sass仍然非常必要。