進(jìn)階:玩轉(zhuǎn) CSS 變量,讓你的 CSS 變得更心動(dòng)!
作者:wsafight
如果當(dāng)年的 CSS 預(yù)處理器變量對(duì)于初入前端的我來(lái)說(shuō)是開啟了新世界的大門,那么 CSS 變量對(duì)于我來(lái)說(shuō)無(wú)疑就是晴天霹靂。其功能不但可以優(yōu)雅的處理之前 js 不好處理或不適合的業(yè)務(wù)需求。更在創(chuàng)造力無(wú)窮的前端開發(fā)者手中大放異彩。
基礎(chǔ)用法
在前端的領(lǐng)域中,標(biāo)準(zhǔn)的實(shí)現(xiàn)總是比社區(qū)的約定要慢的多,前端框架最喜歡的 $ 被 Sass 變量用掉了。而最常用的 @ 也被 Less 用掉了。官方為了讓 CSS 變量也能夠在 Sass 及 Less 中使用,無(wú)奈只能妥協(xié)的使用 --。
<style>
/* 在 body 選擇器中聲明了兩個(gè)變量 */
body {
--primary-color: red;
/* 變量名大小寫敏感,--primary-color 和 --PRIMARY-COLOR 是兩個(gè)不同變量 */
--PRIMARY-COLOR: initial;
}
/** 同一個(gè) CSS 變量,可以在多個(gè)選擇器內(nèi)聲明。優(yōu)先級(jí)高的會(huì)替換優(yōu)先級(jí)低的 */
main {
--primary-color: blue;
}
/** 使用 CSS 變量 */
.text-primary {
/* var() 函數(shù)用于讀取變量。 */
color: var(--primary-color)
}
<style>
<!-- 呈現(xiàn)紅色字體,body 選擇器的顏色 -->
<div class="text-primary">red</div>
<!-- 呈現(xiàn)藍(lán)色字體,main 選擇器定義的顏色 -->
<main class="text-primary">blue</main>
<!-- 呈現(xiàn)紫色字體,當(dāng)前內(nèi)聯(lián)樣式表的定義 -->
<div style='--primary-color: purple" class="text-primary">purple</main>
這里我們可以看到針對(duì)同一個(gè) CSS 變量,可以在多個(gè)選擇器內(nèi)聲明。讀取的時(shí)候,優(yōu)先級(jí)最高的聲明生效。這與 CSS 的"層疊"(cascade)規(guī)則是一致的。
由于這個(gè)原因,全局的變量通常放在根元素:root里面,確保任何選擇器都可以讀取它們。
:root {
--primary-color: #06c;
}
同時(shí), CSS 變量提供了 JavaScript 與 CSS 通信的方法。就是利用 js 操作 css 變量。我們可以使用:
<style>
/* ...和上面 CSS 一致 */
</style>
<!-- 呈現(xiàn)黃色字體 -->
<div class="text-primary">red</div>
<!-- 呈現(xiàn)藍(lán)色字體,main 選擇器定義的顏色 -->
<main id='primary' class="text-primary">blue</main>
<!-- 呈現(xiàn)紫色字體,當(dāng)前內(nèi)聯(lián)樣式表的定義 -->
<div id="secondary" style='--primary-color: purple" class="text-primary">purple</main>
<script>
// 設(shè)置變量
document.body.style.setProperty('--primary-color', 'yellow');
// 設(shè)置變量,js DOM 元素 ID 就是全局變量,所以直接設(shè)置 main 即可
// 變?yōu)?nbsp;紅色
primary.style.setProperty('--primary-color', 'red');
// 變?yōu)?nbsp;黃色,因?yàn)楫?dāng)前樣式被移除了,使用 body 上面樣式
secondary.style.removeProperty('--primary-color');
// 通過(guò)動(dòng)態(tài)計(jì)算獲取變量值
getComputedStyle(document.body).getPropertyValue('--primary-color')
</script>
我們可以在業(yè)務(wù)項(xiàng)目中定義以及替換 CSS 變量,大家可以參考 mvp.css[1]。該庫(kù)大量使用了 CSS 變量并且讓你去根據(jù)自己需求修改它。
:root {
--border-radius: 5px;
--box-shadow: 2px 2px 10px;
--color: #118bee;
--color-accent: #118bee0b;
--color-bg: #fff;
--color-bg-secondary: #e9e9e9;
--color-secondary: #920de9;
--color-secondary-accent: #920de90b;
--color-shadow: #f4f4f4;
--color-text: #000;
--color-text-secondary: #999;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
--hover-brightness: 1.2;
--justify-important: center;
--justify-normal: left;
--line-height: 150%;
--width-card: 285px;
--width-card-medium: 460px;
--width-card-wide: 800px;
--width-content: 1080px;
}
我們可以看到基于 CSS 變量,可以更友好的和設(shè)計(jì)師的設(shè)計(jì)意圖結(jié)合在一起。也易于修改,在業(yè)務(wù)項(xiàng)目中合理使用無(wú)疑可以事半功倍。
實(shí)現(xiàn)默認(rèn)配置
如果讓我來(lái)思考,我肯定無(wú)法想象出結(jié)合 CSS 預(yù)處理器 + CSS 變量便可以實(shí)現(xiàn)組件樣式的默認(rèn)配置。這里我先介紹兩個(gè)關(guān)于該功能的前置知識(shí)點(diǎn):
事實(shí)上,CSS 變量的 var() 函數(shù)還可以使用第二個(gè)參數(shù),表示變量的默認(rèn)值。如果該變量此前沒(méi)有定義或者是無(wú)效值,就會(huì)使用這個(gè)默認(rèn)值。
/* 沒(méi)有設(shè)置過(guò) --primary-color,顏色默認(rèn)使用 #7F583F */
color: var(--primary-color, #7F583F);
雖然目前 CSS 變量不是新的屬性,但終究不是所有的瀏覽器都支持 CSS 變量的,這里我們還是要考慮一下優(yōu)雅降級(jí)。
/* 對(duì)于不支持 CSS 變量的瀏覽器,可以采用下面的寫法。*/
a {
/* 顏色默認(rèn)值 */
color: #7F583F;
/* 不支持則不識(shí)別該條規(guī)則 */
color: var(--primary);
}
結(jié)合 CSS 處理器 + CSS 變量便可以實(shí)現(xiàn)組件樣式的默認(rèn)配置。這里參考了有贊的 Vant Weapp[2] 的做法。有贊代碼 **theme.less**[3] 如下所示:
// 先導(dǎo)入所有 Less 變量
@import (reference) './var.less';
// 利用正則去替換 Less 變量 為 CSS 變量
.theme(@property, @imp) {
@{property}: e(replace(@imp, '@([^() ]+)', '@{$1}', 'ig'));
@{property}: e(replace(@imp, '@([^() ]+)', 'var(--$1, @{$1})', 'ig'));
}
函數(shù)效果如下所示:
@import '../common/style/theme.less';
.van-button {
// ... 其他省略
.theme(height, '@button-default-height');
.theme(line-height, '@button-line-height');
.theme(font-size, '@button-default-font-size');
}
// => less 編譯之后生成
.van-button{
// ... 其他省略
height:44px;
height:var(--button-default-height,44px);
line-height:20px;
line-height:var(--button-line-height,20px);
font-size:16px;
font-size:var(--button-default-font-size,16px);
}
我們可以看到每調(diào)用一次 Less 函數(shù)將會(huì)被編譯成兩個(gè)屬性。第一個(gè)屬性的設(shè)定對(duì)于不支持 CSS 變量的設(shè)備可以直接使用,如果當(dāng)前設(shè)備支持 CSS 變量,則會(huì)使用 CSS 變量,但是由于當(dāng)前 CSS 變量未定義,就會(huì)使用變量的默認(rèn)值。雖然 '@button-default-height 雖然也是一個(gè)變量,但是該變量?jī)H僅只是 less 變量,最終生成的代碼中并沒(méi)有 --button-default-height 這樣的變量。此時(shí)我們就可以在使用樣式的位置或者 :root 中添加變量 --button-default-height。
這種方式更適合組件開發(fā),因?yàn)樵摲桨覆宦暶魅魏?css 變量,只是預(yù)留的 css 變量名稱和默認(rèn)屬性。這樣的話,無(wú)論開發(fā)者的選擇器優(yōu)先度有多低,代碼都可以很容易的覆蓋默認(rèn)屬性。因?yàn)槲覀儍H僅使用 css 的默認(rèn)值。
大家可能有時(shí)候會(huì)想,這樣的話,我們不是有更多的代碼了嗎?其實(shí)未必,事實(shí)上我們可以直接直接在頁(yè)面內(nèi)部定義變量樣式。其他組件直接通過(guò) style 去使用頁(yè)面內(nèi)的變量。當(dāng)然了,事實(shí)上書寫的代碼多少,重點(diǎn)在于想要控制默認(rèn)樣式的粒度大小。粒度越小,則需要在各個(gè)組件內(nèi)部書寫的變量越多,粒度大,我們也就不必考慮太多。
Space Toggle 邏輯切換
CSS 沒(méi)有邏輯切換似乎是一種共識(shí),但是我們可以利用選框(單選與多選)和 CSS 變量來(lái)實(shí)現(xiàn)判斷邏輯。我們先來(lái)看看如何使用 CSS 變量。
<style>
.red-box {
--toggler: ;
--red-if-toggler: var(--toggler) red;
background: var(--red-if-toggler, green); /* will be red! */
}
.green-box {
--toggler: initial;
--red-if-toggler: var(--toggler) red;
background: var(--red-if-toggler, green); /* will be green! */
}
</style>
<!-- 寬度高度為 100px 的 紅色盒子 -->
<div
style="height: 100px; width: 100px"
class="red-box"
></div>
<!-- 寬度高度為 100px 的 綠色盒子 -->
<div
style="height: 100px; width: 100px"
class="green-box"
></div>
這里因?yàn)橐粋€(gè)變量 --toggler 使用空格 或者 initial 而產(chǎn)生了不同的結(jié)果,基于這樣的結(jié)果我們不難想象我們可以觸發(fā)變量的修改而產(chǎn)生不同的結(jié)果。
他不是一個(gè) bug,也不是一個(gè) hack。他的原理完完全全的在 CSS Custom Properties 規(guī)范[4] 中。
This value serializes as the empty string, but actually writing an empty value into a custom property, like --foo: ;, is a valid (empty) value, not the guaranteed-invalid value. If, for whatever reason, one wants to manually reset a variable to the guaranteed-invalid value, using the keyword initial will do this.
解釋如下,事實(shí)上 -foo: ; 這個(gè)變量并不是一個(gè)無(wú)效值,它是一個(gè)空值。initial 才是 CSS 變量的無(wú)效值。其實(shí)這也可以理解,css 沒(méi)有所謂的空字符串,空白也不代表著無(wú)效,只能使用特定值來(lái)表示該變量無(wú)效。這個(gè)時(shí)候,我們?cè)倩仡^來(lái)看原來(lái)的 CSS 代碼。
.red-box {
/* 當(dāng)前為空值 */
--toggler: ;
/* 因?yàn)?nbsp;var(--toggler) 得到了空,所以得到結(jié)果 為 --red-if-toggler: red */
--red-if-toggler: var(--toggler) red;
/** 變量是 red, 不會(huì)使用 green */
background: var(--red-if-toggler, green); /* will be red! */
}
.green-box {
/** 當(dāng)前為無(wú)效值 */
--toggler: initial;
/** 仍舊無(wú)效數(shù)據(jù),因?yàn)?nbsp;var 只會(huì)在參數(shù)不是 initial 時(shí)候進(jìn)行替換 */
--red-if-toggler: var(--toggler) red;
/** 最終無(wú)效值沒(méi)用,得到綠色 */
background: var(--red-if-toggler, green); /* will be green! */
/* 根據(jù)當(dāng)前的功能,我們甚至可以做到 and 和 or 的邏輯
* --tog1 --tog2 --tog3 同時(shí)為 空值時(shí)是 紅色
*/
--red-if-togglersalltrue: var(--tog1) var(--tog2) var(--tog3) red;
/*
* --tog1 --tog2 --tog3 任意為 空值時(shí)是 紅色
*/
--red-if-anytogglertrue: var(--tog1, var(--tog2, var(--tog3))) red;
}
新式媒體查詢
當(dāng)我們需要開發(fā)響應(yīng)式網(wǎng)站的時(shí)候,我們必須要使用媒體查詢 @media。先看一下用傳統(tǒng)的方式編寫這個(gè)基本的響應(yīng)式 CSS:
.breakpoints-demo > * {
width: 100%;
background: red;
}
@media (min-width: 37.5em) and (max-width: 56.249em) {
.breakpoints-demo > * {
width: 49%;
}
}
@media (min-width: 56.25em) and (max-width: 74.99em) {
.breakpoints-demo > * {
width: 32%;
}
}
@media (min-width: 56.25em) {
.breakpoints-demo > * {
background: green;
}
}
@media (min-width: 75em) {
.breakpoints-demo > * {
width: 24%;
}
}
同樣,我們可以利用 css 變量來(lái)優(yōu)化代碼結(jié)構(gòu),我們可以寫出這樣的代碼:
/** 移動(dòng)優(yōu)先的樣式規(guī)則 */
.breakpoints-demo > * {
/** 小于 37.5em, 寬度 100% */
--xs-width: var(--media-xs) 100%;
/** 小于 56.249em, 寬度 49% */
--sm-width: var(--media-sm) 49%;
--md-width: var(--media-md) 32%;
--lg-width: var(--media-gte-lg) 24%;
width: var(--xs-width, var(--sm-width, var(--md-width, var(--lg-width))));
--sm-and-down-bg: var(--media-lte-sm) red;
--md-and-up-bg: var(--media-gte-md) green;
background: var(--sm-and-down-bg, var(--md-and-up-bg));
}
可以看出,第二種 CSS 代碼非常清晰,數(shù)據(jù)和邏輯保持在一個(gè) CSS 規(guī)則中,而不是被 @media 切割到多個(gè)區(qū)塊中。這樣,不但更容易編寫,也更加容易開發(fā)者讀。詳情可以參考 css-media-vars[5]。該代碼庫(kù)僅僅只有 3kb 大小,但是卻是把整個(gè)編寫代碼的風(fēng)格修改的完全不同。原理如下所示:
/**
* css-media-vars
* BSD 2-Clause License
* Copyright (c) James0x57, PropJockey, 2020
*/
html {
--media-print: initial;
--media-screen: initial;
--media-speech: initial;
--media-xs: initial;
--media-sm: initial;
--media-md: initial;
--media-lg: initial;
--media-xl: initial;
/* ... */
--media-pointer-fine: initial;
--media-pointer-none: initial;
}
/* 把當(dāng)前變量變?yōu)榭罩?nbsp;*/
@media print {
html { --media-print: ; }
}
@media screen {
html { --media-screen: ; }
}
@media speech {
html { --media-speech: ; }
}
/* 把當(dāng)前變量變?yōu)榭罩?nbsp;*/
@media (max-width: 37.499em) {
html {
--media-xs: ;
--media-lte-sm: ;
--media-lte-md: ;
--media-lte-lg: ;
}
}
其他
繼 CSS 鍵盤記錄器[6] 暴露了 CSS 安全性問(wèn)題之后,CSS 變量又一次讓我看到了玩技術(shù)是怎么樣的。CSS Space Toggle 技術(shù)不但可以應(yīng)用于上面的功能,甚至還可以編寫 UI 庫(kù) augmented-ui[7] 以及 掃雷[8] 游戲。這簡(jiǎn)直讓我眼界大開。在我有限的開發(fā)生涯中,很難找到類似于 css 這種設(shè)計(jì)意圖和使用方式差異如此之大的技術(shù)。
CSS 是很有趣的,而 CSS 的有趣之處就在于最終呈現(xiàn)出來(lái)的技能強(qiáng)弱與你自身的思維方式,創(chuàng)造力是密切相關(guān)的。上文只是介紹了 CSS 變量的一些玩法,也許有更多有意思的玩法,不過(guò)這就需要大家的創(chuàng)造力了。
augmented-ui[9]
css-media-vars[10]
css-sweeper[11]
參考資料
mvp.css: https://andybrewer.github.io/mvp/
[2]Vant Weapp: https://youzan.github.io/vant-weapp/#/theme
[3]theme.less: https://github.com/youzan/vant-weapp/blob/v1.1.0/packages/common/style/theme.less
[4]CSS Custom Properties 規(guī)范: https://drafts.csswg.org/css-variables/#guaranteed-invalid
[5]css-media-vars: https://propjockey.github.io/css-media-vars/
[6]CSS 鍵盤記錄器: https://github.com/maxchehab/CSS-Keylogging
[7]augmented-ui: http://augmented-ui.com/
[8]掃雷: https://github.com/propjockey/css-sweeper
[9]augmented-ui: https://github.com/propjockey/augmented-ui
[10]css-media-vars: https://github.com/propjockey/css-media-vars
[11]css-sweeper: https://github.com/propjockey/css-sweeper
最后
如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
歡迎加我微信「 sherlocked_93 」拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
關(guān)注公眾號(hào)「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。

