巧妙的 CSS 变量
在 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%;
}
因此,有许多很棒的想法还不能实现。譬如色相与亮度以不同的速度变化,例如渐变背景的颜色控制等。期待未来的发展吧。