在 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%;
}

   因此,有許多很棒的想法還不能實現。譬如色相與亮度以不同的速度變化,例如漸變背景的顏色控制等。期待未來的發展吧。