更新時間:2024-05-29 21:17:53作者:佚名
專欄介紹
“頂級寫手”是OPPO工程師解讀最新熱點技術的專欄,在這里你不僅可以看到最新最熱的動態,還能和OPPO優秀的工程師一起學習技術知識。
頂級作家
■鮑勃
■一位在前端領域耕耘9年多的前端“新生代IT民工”,立志分享各種前端酷知識。
01
背景
相信用過 UI 框架的同學應該或多或少都修改過它的樣式,最常見需要修改的樣式應該就是:顏色。大多數情況下,官方框架可能會提供主題配置,比如主題顏色配置(使用 Sass 變量)或者 Ant 主題顏色配置(使用 Less 變量)。你可以定義 @-color(Ant) 或者 $--color-() 這樣的變量來定制想要的主題顏色,比如 OPPO 綠色。
但相信大家都經歷過這種情況,因為是借助了 CSS 預處理器的能力,而非原生的 CSS 支持,這不僅意味著需要打包工具的支持,也意味著這些變量并不是真正的“變量”。它們在打包時會被替換成 CSS 能夠識別的顏色值,無法再通過改變其中一種 - 來改變頁面上所有相關的顏色。對于只使用一套 OPPO 綠色的場景來說這還好,但如果要改變動態主題色怎么辦?或者像夜間模式這種需要改變顏色的場景怎么辦?這對于 CSS 來說其實并不容易。
使用 CSS 不是不可以,只是開發體驗不好。比如可以為不同的主題設置不同的 class,并根據這些不同的 class 將所有使用顏色的地方重寫。試想一下,業務 CSS 中有 20 個地方用到了主題顏色custom什么意思,每增加一套主題,就需要重寫這 20 個地方的顏色定義。這其實也是我們之前常用的方法。
當然,也有辦法將瀏覽器版的CSS預處理器嵌入到頁面中,如果你已經在網上使用過這種方法,或者有這樣的想法,請務必看完本文,思考是否還有其他方法。
遇到需要動態更換主題的同學肯定都搜索過并了解過CSS自定義變量的概念,如果你還沒有用過,不用擔心,看完你就懂了。
不知道大家有沒有注意到,標題里有一個奇怪的---,和變量定義方式$--color-很像。那么這是什么呢?CSS自定義變量就是這么定義的。
02
基本定義
我們先來看一下基本的定義。CSS自定義變量由兩部分組成 - 加一個名稱。名稱和變量名規則差不多,比如區分大小寫(這個和普通的CSS屬性名不同,CSS屬性名一般不區分大小寫),但不同之處在于CSS自定義變量名可以以數字開頭,包括純數字,例如-1就是合法的CSS自定義變量,甚至允許使用漢字,甚至允許使用表情符號。
正如那句老話所說:你能做某件事并不意味著你應該做它。想想當你看到有人在別人的代碼中寫了 --1: 5px, --2: red,然后毫無理由地到處使用 --1 和 --2 時,試著表達你的感受。
其實 CSS 規范和 MDN 文檔中都使用了“自定義屬性”這個術語,但是“自定義屬性”看起來不如“自定義變量”那么清晰吸引人,所以本文使用了“自定義變量”這個術語。請記住它對應的是“自定義屬性”或者規范中提到的與變量相關的術語:“自定義變量”。
這里之所以可以是數字開頭,也可以是純數字,準確的說是因為前面有一個--,這個--不能算是一個變量名,因為實際的名字應該包含整個--。
但是,有了名字并不代表變量就可以用,需要賦值才有用。而且,CSS自定義變量必須存在于CSS的一定的元素規則定義中,不能像全局變量那樣寫在最外層。當然,全局變量也有相應的定義方式,后面會提到。所以CSS自定義變量的完整定義應該是:
:root {
--custom-variable:
; }
/* 舉幾個栗子 */
html {
--color-primary: green;
--color-disabled: gray;
--wide-border: 3px;
}
除了在CSS文件、樣式屬性、對應方法中定義外,請注意,這也是在“某一元素規則的定義”中,因為它只對該屬性所在的元素及其子元素有效。
03
使用自定義變量
用法
好的,現在我們有了一個有效的 CSS 自定義變量,如何使用它?CSS 定義了 var() 方法,例如,您可以使用 var(--color-) 來讀取它。因此,可以像這樣設置具有淺綠色背景的 div 的樣式:
div {
background-color: var(--color-primary);
}
var() 方法也支持默認值,當對應變量 或者 value 為 時,會讀取默認值。定義很簡單,用 隔開,后面的值就是默認值。例如 var(--color-,blue),當 --color- 或者 value 等于 時,會返回 blue。
但請注意,如果有多個逗號,則第一個逗號后面的整個值將作為默認值,而不是用逗號分隔。所以 var(--color-,blue,cyan) 的默認值是 blue, cyan。
在定義自定義變量的時候,還可以使用其他自定義變量。同時,var() 支持多層嵌套,因此默認值也可以是另一個自定義變量。例如以下形式:
div.bordered {
--color-border: var(--color-secondary, green); /* 注意 --color-secondary 并未定義 */
border: var(--wide-border) var(--color-border) solid;
color: var(--color-text, var(--color-disabled, black));
}
看到這里,大家應該知道怎么用了,但是為什么說了這么久,好像跟 CSS 預處理器的變量定義沒什么區別。好啦,別著急,我先問大家,還記得 CSS 縮寫里的 C 代表什么嗎?沒錯,就是“”:“”。這個詞也是 CSS 的獨特魅力所在,也是最容易讓人混淆的部分。不過,相信看文章的大家應該對 的優先級很熟悉了,如果不熟悉的話趕緊復習一下吧。為什么突然提到它呢?因為 CSS 自定義變量也遵循了 的概念。相同的變量定義會默認繼承,優先級高的定義會覆蓋優先級低的定義。這也是變量能“動態”改變其值的重要原因。
那么思考一下,如果前面所有的代碼塊都是在同一個頁面中依次定義,而頁面中有一個div和一個div,那么它們應該分別如何顯示呢?
了解了用法之后,假設你需要為這個頁面添加多個主題樣式,結合上面提到的級聯,你想到怎么添加了嗎?沒錯,我們可以用新樣式覆蓋變量:
.pink-theme {
--color-primary: pink;
--color-border: deeppink;
}
.gold-theme {
--color-primary: gold;
--color-border: goldenrod;
}
/* 還可以加更多 */
當然如果你不想添加class的話,也可以直接將樣式寫在對應的標簽上來覆蓋。
正如文章開頭所說,這種動態改變 CSS 中變量的值的功能在 CSS 預處理器中是無法輕易做到的,因為它們的變量定義就像它們的名字一樣,都是經過預先處理并放置在 CSS 中的。這也是 CSS 自定義變量相較于預處理器的一大優勢。
全局變量
看完前面的使用部分,大家有沒有搞清楚如何定義一個全局的 CSS 變量呢?因為自定義變量默認是被繼承的,所以簡單來說就是把樣式放到覆蓋范圍最廣的根元素上,也就是 HTML 中的元素上(其實一般情況下放在那里應該就可以了)。
上例中寫了一個 :root 偽類,這個偽類也引用了根元素,在 HTML 中也引用了 html 元素,這有什么區別呢?比如下面的定義中,div 的背景應該是什么顏色呢?
:root {
--color-bg: red;
}
html {
--color-bg: blue;
}
div {
background-color: var(--color-bg);
}
如果你看實際的頁面,會發現是紅色的。為什么呢?:root 是偽類,所以是類的優先級。回想一下優先級的定義,類的優先級要高于元素類型選擇器的 html。
除了這種通過繼承來增加覆蓋率的形式之外,還有一種定義全局變量的方式,后面會提到。首先我們來看幾個問題。
無效值
從上面的用法來看,一般情況下,CSS 自定義變量可以簡單理解為在調用 var() 的地方將其值替換為文本,但在實現上還是有些區別的。拋開 CSS 解析計算過程的細節不談,可以看作是 CSS 解析器忽略了使用自定義變量的屬性值的語法檢查,即不管該值本身是否可以在對應屬性中使用。所以即使給自定義變量傳遞了非法值,這個屬性還是會被正常解析,只是在計算值的時候會出錯;這不同于直接寫入非法值,CSS 解析器會提前檢測到語法錯誤并忽略這條規則。
這樣可能有點讓人困惑,我們以MDN上的例子為例,做一個簡單的擴展:
:root { --text-color: 16px; }
p { color: blue; }
p { color: var(--text-color); }
div { color: blue; }
div { color: 16px; }
如果你打開 demo 頁面,會發現第一行〈p〉是黑色的,而第二行〈div〉是藍色的。這是因為在使用 CSS 自定義變量的〈p〉定義中,color:var(--text-color)被正常解析,覆蓋了之前定義的color:blue(CSS 解析器一般會直接丟棄這個沒用的規則定義)。然后在替換-text-color 的時候發現它不是一個值,不是合法的顏色,導致〈p〉元素的顏色定義被非法重置。因為color是繼承的屬性,所以〈p〉會首先嘗試取繼承的值。由于 demo 中上沒有定義color,所以使用了繼承的瀏覽器默認樣式color:black。這里因為color:16px在解析過程中被瀏覽器認為是非法的而被忽略,所以選擇了之前有效的定義color:blue。
無效值重置和 CSS 全局關鍵字中 unset 的效果一致custom什么意思,即對繼承的屬性啟用 unset 時相當于繼承父級,對繼承的屬性禁用 unset 時相當于初始值。這三個字也是 CSS 的三個全局關鍵字。另外請注意,這三個關鍵字在使用自定義變量時也是有效的,將自定義變量值設置給它們時,就意味著對這個自定義變量執行 unset,以及操作:
:root {
--text-color: green;
color: red;
}
div {
--text-color: inherit;
color: var(--text-color);
}
在上面的例子中,div的文字顏色會是綠色而不是紅色,因為這意味著自定義變量是從父級繼承下來的,也就是--text-color的值被計算為綠色,而不是用color:;替換后續color屬性的文字。
另一個具有此功能的關鍵字可能是臭名昭著的 !。當用于自定義變量時,它還意味著自定義變量本身具有 ! 的優先級,而不是將其替換為文本。我不會為此寫一個例子,我留給你自己去嘗試。
因為不是文本替換,所以有些地方不能寫自定義變量。比如不能寫自定義變量來替換 CSS 屬性名,也不能把值中的數字和單位分開留學之路,用自定義變量替換:
:root {
--property-name: padding;
--padding:10;
}
p {
var(--property-name):100px;/* 語法錯誤,忽略 */
}
p {
padding:10px;
padding:var(--padding)px; /* 正常解析但計算值失敗,等同于 padding: unset,所以前一條規則會被覆蓋 */
}
p.correct {
padding: calc(var(--padding)*1px); /* 注意可以利用 calc() 來實現單位 */
}
注意上面 p 的第二個定義 :10px; 是無效的,因為 var(--)px 在語法上不算無效,但實際計算時發現無法正確計算值,所以相當于 unset 了。不過如果你確實想只定義一個數字,并且在使用時加上單位,也可以使用例子中的 calc() 方法來實現。
雖然您不能像本例一樣連接單元,但是您可以使用 CSS 自定義變量將屬性值拆分為多個部分值,這將在后面的示例中提到。
循環引用
CSS 自定義變量允許你在 var() 內部嵌入另一個 var(),但不可避免地存在一個問題:
:root {
--margin:10px;
}
div {
--padding: calc(var(--margin)-10px);
margin:var(--margin);
padding:var(--padding);
}
div.cyclic {
--margin: calc(var(--padding)+10px);
}
那么,div.的--和--該怎么解析呢?規范中定義,如果在同一個元素下發生循環引用,則本次循環內的所有自定義變量都等于該值,也就是值。
注意關鍵字“同一元素”,因為自定義變量默認是繼承的,而繼承行為發生在值計算之后,所以不同元素下的定義不一定構成循環引用,例如我們將最后一段定義改為div下的p:
:root {
--margin:20px;
}
div {
--padding: calc(var(--margin)-10px);
margin:var(--margin);
padding:var(--padding);
background:#f00;
}
div.cyclic p {
--margin: calc(var(--padding)+10px);
margin:var(--margin);
background:#0f0;
}
在這個例子中,div.p的--可以正常計算得到20px,因為p在計算--時,--繼承自div,而div已經被計算為10px了。
更多示例
前面的例子中提到,CSS 自定義變量在取值時基本相當于文本替換,沒有類型的概念。這樣的操作在某些場景下也能出乎意料的方便,比如 CSS-Trick 上提到的例子:
button {
--h: 100;
--s: 50%;
--l: 50%;
--a: 1;
background: hsl(var(--h) var(--s) var(--l) / var(--a));
}
button:hover { /* Change the lightness on hover */
--l: 75%;
}
button:focus { /* Change the saturation on focus */
--s: 75%;
}
button[disabled] { /* Make look disabled */
--s: 0%;
--a: 0.5;
}
甚至有一種方法可以使用自定義變量作為開關,但請謹慎使用......
這里只是給出一些簡單的使用示例,相信很多同學在實際工作中,包括我們的項目中,都有自定義變量的情況。
04
關于動畫
自定義變量是 CSS 屬性,因此可以在動畫中使用。但是,由于它們沒有類型,CSS 解析器不知道如何應用動畫樣式。效果不會像您預期的那樣:
.color-div {
--angle: 0deg;
background: linear-gradient(var(--angle), red, yellow, blue, purple);
animation: rotate 5s ease-in-out both alternate infinite;
}
@keyframes rotate {
to {
--angle: 180deg;
}
}
打開這個 demo 你會發現背景并沒有“動”,只是顏色跳動了一下。其實這應該符合預期,畢竟漸變算是圖片,本身不支持動畫。
其實一眼就能看懂這段 CSS 代碼想要表達什么,但是替換的方式讓自定義變量支持放在過渡動畫、動畫樣式中,其實并沒有什么用。不過這么明顯的問題,規范已經考慮到了,那么接下來我們看看他們是怎么解決這個問題的呢?
定義全局變量的另一種方法(注冊)
鑒于CSS自定義變量的語法非常松散,無法定義其值類型、是否繼承、及其初始值,這也導致無法很好實現動畫等問題,CSS增加了@來定義,或者更準確的說,使用(注冊)一個CSS自定義變量,可以設置其類型(或者其遵循的語法)、是否繼承、及其初始值-value。結合上面的例子,我們來簡單講解一下@的用法以及一些需要注意的地方。前面的例子只要加上這個定義聲明,就可以達到預期的效果了~
@property --angle {
syntax: "
" ;inherits: false;
??initial-value:?‘0deg’;
}
同時@也有一個等價的接口可以調用,比如之前的聲明,可以使用如下方式實現:
CSS.registerProperty({
name: '--angle',
??syntax:?'
' ,??inherits:?false,
??initialValue:?‘0deg’;
});
從上面的示例和解釋來看,這些屬性的含義可能非常簡單。 但是,使用它們時需要注意以下幾點:
●首先,與大多數@語句一樣,@目前必須出現在CSS的最外層,不能嵌套在其他樣式聲明中,也不能位于最內層(但這種行為可能會改變,規范正在討論中)。它可以嵌套在條件@語句中,例如@media:
:root {
@property --primary-color {
/*不會生效*/
}
}
@media (min-width: 1200px) {
@property --width {
/* 有效 */
}
}
●其次,所有屬性都應設置,否則整個定義將被忽略。唯一的例外是,當屬性為*時,-value可以留空。不過,帶*的自定義變量的行為與普通定義基本相同。
●當需要填寫 -value 時,其值必須符合定義的語法,并且必須是計算獨立( )值,否則整個定義將被忽略。什么是“計算獨立”?簡單來說,就是不再依賴于其他 CSS 屬性值。例如,1em 不符合條件,因為它依賴于 font-size 的定義,var(--other-) 也不起作用,但像 1px 和 #F00 這樣的值是可以的。
@property --width {
syntax: "
" ;inherits: true;
initial-value:
}
@property --width {
syntax: "
" ;inherits: true;
initial-value: 1rem; /* 依賴根元素font-size,不符合條件,整個定義被忽略 */
}
這里還要說一下-value,由于它的存在,var()的替換和之前略有不同,我們先來舉一個上一篇文章中的例子:
:root { --text-color: 16px; }
p { color: blue; }
p { color: var(--text-color); }
div { color: blue; }
div { color: 16px; }
在這個例子中,如前所述,由于計算值時出現錯誤,定義未設置,因此 〈p〉 的顏色變為黑色。但是如果我們添加定義:
@property --text-color {
syntax: "
" ;inherits: true;
initial-value:
}
這種情況下,在計算值的時候, --text-color: 16px; 并不符合聲明定義的語法,相當于 --text-color: unset;,所以結果為 --text-color: #f00; (即初始值),所以〈p〉 會顯示為紅色,而不是黑色。但請注意,驗證是在計算 div 值時發生的,而不是在解析 CSS 時發生的,所以如果我們將 --text-color 的定義改為如下形式,效果還是一樣:
:root {
--text-color: #0f0;
--text-color: 16px; /* 依舊是unset,而不是選擇讀取前一條,因為不是在解析時判斷的語法錯誤 */
}
由于種種原因,規范選擇在解析時不做語法檢查,所以即使計算時有語法檢查,也和常規的解析錯誤不一致。如果這句話你還不太理解,回想一下前面錯誤檢查的例子中,div文字是藍色的。
另外要注意,對于有初始值的全局變量,當使用 var(--, ) 時,即使你手動將 --:; 替換為 var(--, ),也不會讀取相應 -value 定義的初始值。
可以填寫的類型有很多,也支持復合類型。前面的“”,或者“”和“>”等可以猜到的名字都是合法的值。詳細列表請看這里。注意引號是必須的,因為這個值需要類型。
關于多個同名聲明的優先級,如果CSS定義中存在同名的@聲明,則最后一個生效;但如果存在多個同名聲明,則其優先級最高,并且該方法不允許重復聲明相同的變量名。
最后,目前沒有辦法取消注冊變量,但規范提到可能會稍后添加此功能。
好了,本文到此結束。感謝您花時間閱讀。希望您閱讀完本文后對 CSS 自定義變量有更好的了解。