在 CSS 中使用变量,是长期以来所有前端伙伴们的急迫诉求。 通过构建工具,我们已经可以在 Sass 和 Less 中愉快地使用变量。 那么 CSS 变量,又能给我们带来什么呢?仅仅是跟着走 Sass/Less 所走过的路吗? 不,作为标准的亲儿子,CSS 变量有着无可比拟的优越性。

动态性

   Sass/Less 中的变量,在项目构建之后,被常量所替代。 即,我们不可能通过客户端 JS 去访问,或动态地修改这些变量。它们归根结底,是静态的。 而 CSS 变量,是动态的。@media 和 JS 可以动态地修改这些变量,而引用它们的地方,将同步改变。

更好的作用域规则

   CSS 变量,又称为“自定义属性”。从作用域的角度看,它不像 Sass/Less 的变量,而像普通的 CSS 属性。 Sass/Less 变量的作用域就是语法上的,也就是一对{}之间。 而 CSS 变量,则和一般属性一样,通过选择器,绑定到 DOM 之上,并在 DOM 节点和其字节点中有效。 Sass/Less 变量的作用域取决于你怎么写,CSS 变量作用域取决于最终的 DOM 树。 这有什么好处呢?别急,先来看一个例子。

组件化思想与自定义属性

button
{
	min-width: calc(var(--button-size, 1)*1rem);
	height: calc(var(--button-size, 1)*1rem);
}

body>main
{
	--button-size: 1.5;
}

   这里我们给 button 设置尺寸,依据是变量 --button-size 的值,若不存在,则取缺省值为 1。 这个规则,将应用于页面上所有的 <button> 上。 又在 body>main 上设置了 --button-size: 1.5。规则作用于 <body> 下,直属的 <main> 上。 容易预见,所有这个 <main> 中的 <button> 的高度为 1.5rem,而外部的 <button> 则高度为 1rem

写 C 的伙伴:不科学!变量怎么能使用在先,定义在后?怎么能取到别的作用域下的变量?

   让我们换个角度来理解问题,把 button 部分看作是函数,而将其中的 --button-size 看作参数(形参)。 这个“函数”将在 <button> 被渲染时调用。而此时,body>main 上所赋予的值,将作为参数(实参)传入。

   这个小例子体现出了封装,以及组件化的思想。 button 是这样一个组件,有着内部的样式,又向外暴露若干参数接口。可在各种场景中复用。 而外界不对这个组件的普通样式进行入侵,只修改其所暴露的参数。 此时,CSS 变量,就好似组件的自定义样式属性。

   至此,还不是真正的组件化,我们必须以规范来避免外界的样式入侵。一旦不小心破坏规则,一切将乱套。 所幸,我们已经有 Web Components。

结合 Web Components

   Custom Elements 中,组件里的样式表与外界几乎是隔离的。但 CSS 变量却可以穿透这个屏障。是十分便利的通信渠道。 这是一个自定义元素内部 CSS 的例子。

:host
{
	--button-size: 1.5;
}

button
{
	font-size: var(--font-size, 1rem);
	min-width: calc(var(--button-size)*1rem);
	height: calc(var(--button-size)*1rem);
}

   此例中,我们使用变量给自定义元素内部的 button 定义样式。与前面的例子是一样的。 值得注意的是,我们在 :host 上声明了 --button-size ,而没有声明 --font-size。 造成的区别就是:任何外层元素上定义的 --font-size 将对自定义元素内的 <button> 产生影响; 而外部要想设置 --button-size 的值,必须直接定义在这个自定义元素上。 两种方式,各有其适用的场景。 --button-size 的方式,适用于一般情况下组件接受外部参数。参数必须明确地直接传递给组件,避免混乱。 而 --font-size 的方式,适用于读取全局或局部的配置,不可滥用。

与 JS 的交互

   以下是 JS 操作样式的接口,可以操作普通的样式属性,以及自定义属性(CSS 变量)。

style instanceof CSSStyleDeclaration;
/*
 * style 可以是一个 DOM 的样式,例如:
 *   document.body.style
 *
 * 也可以是一个层叠样式表中的样式规则,例如:
 *   document.styleSheets[0].cssRules[0].style
 *
 * 还可以是一个CSS的层叠计算结果(只读),例如:
 *   getComputedStyle( element, )
 */

// 获取一个样式属性的值,(总是得到字符串,即便样式不存在)
const value= style.getPropertyValue( '--foo', );

// 设置一个样式属性,(接受的值也被转为字符串)
style.setProperty( '--foo', value, );

// 移除一个样式属性
style.removeProperty( '--foo', );

   即便是普通 CSS 属性,这也是更好的操作方式。推荐统一使用这套接口操作样式。

// good
style.setProperty( 'z-index', '1', );
style.setProperty( 'float', 'left', );

// bad
style.zIndex= '1';
style.cssFloat= 'left';

   有一种场景是,JS 捕获用户事件后,获取事件参数,例如鼠标的位置;并读取一些布局信息, 例如某个 DOM 的宽高;再经过一系列计算后,将几个计算结果设置到该 DOM 的 style 上。 这样,JS 就干涉了布局,与 CSS 强耦合,违背了关注点分离。 由于存在对布局信息的读取,会引起额外的重绘。

   使用 CSS 变量,则完美地解决了这个痛点。JS 中你只需将鼠标的位置传递给 CSS 变量, 而无需注意其它细节。在 CSS 中通过 calc() 完成计算和布局。 很好地遵循了关注点分离,也避免了额外的重绘。

数据类型

   在 CSS 中,基本数据类型有数量,字符串,关键字,颜色等。此外,还有序列。 这些类型的数据,都可以作为 CSS 变量的值。

.foo
{
	--an-int: 3;
	--a-float: 5.2;
	--with-unit: 80%;
	--a-color: white;
	--a-string: 'foobar';
	--a-keyword: left;
	--a-list: 1px 1px;
	--another-list: green , 1px;
	--more-another-list: 20% / 1em;
}

   使用时,可以自由组合,计算。计算时,要注意结果的单位。 而序列的组合,只要把 ,/ 等符号,视为与 1px 等普通值一样的序列元素,便能理解其行为了。

.foo
{
	/* 以上变量的基本用例 */
	z-index: var(--an-int);
	font-size: var(--a-float);
	width: var(--with-unit);
	color: var(--a-color);
	font-family: var(--a-string);
	float: var(--a-keyword);
	margin: var(--a-list);
	border-radius: var(--more-another-list);
	
	/* 一些略复杂的用例 */
	background-color: hsla(var(--an-int),var(--with-unit),calc(var(--an-int)*20%));
	box-shadow: var(--a-list) 5px var(--another-list) 2px 2px red;
	border-radius: 4px 4px 4px var(--more-another-list) 4px 4px 4px;
}

结合过渡与动画

   很遗憾,尚无浏览器支持 CSS 变量的过渡效果。只能对常规属性使用过渡效果或关键帧。

.foo
{
	--lightness: 80%;
	color: hsla(120,100%,var(--lightness));
	background-color: hsla(0,100%,var(--lightness));
	
	/* 尚不被支持 */
	transition:
		--lightness 500ms
	;
	
	/* 可行 */
	transition:
		color 500ms
		,
		background-color 500ms
	;
}

.foo:hover
{
	--lightness: 50%;
}

   因此,有许多很棒的想法还不能实现。譬如色相与亮度以不同的速度变化,例如渐变背景的颜色控制等。期待未来的发展吧。