【譯】CSS 自定義屬性的策略指南

閱文前端團隊發表於2018-07-05

原文地址: www.smashingmagazine.com/2018/05/css…

譯文地址: github.com/yued-fe/y-t…

譯者: 波波

校對者: ziven27小爺

關於作者 Mike 是來自澳大利亞的獨立網站開發人員,曾在澳大利亞的一些大型網站和一些小型社群工作過...... 關於Michael的更多資訊...

CSS 自定義屬性(也稱為“CSS 變數”),在目前所有的現代瀏覽器中都得到了支援,開發者已經在專案中開始使用,但是它們與前處理器中的變數不同,雖然我已經看到過很多例子,卻沒有搞清楚他們真正的優勢在哪裡。

自定義屬性有很大的潛力可以改變我們編寫和組織 CSS 的方式,並且在一定程度上改變 JavaScript 與 UI 元件的呼叫方式。我並不關心語法和它們的工作方式(為此,我建議你閱讀“現在是時候開始使用自定義屬性了”這篇文章)。同時我想更深入地研究如何充分利用 CSS 自定義屬性。

自定義屬性與前處理器中的變數有何相似之處?

自定義屬性有點像前處理器中的變數,但還是有很大的差別。最重要也是最明顯的區別是在於語法。

在 SCSS 中我們用 $ 符號來定義變數:

$smashing-red: #d33a2c;
複製程式碼

在 Less 中我們用 @ 符號:

@smashing-red: #d33a2c;
複製程式碼

自定義屬性遵循類似的約定並使用 -- 字首的方式:

:root { --smashing-red: #d33a2c; }
.smashing-text { 
  color: var(--smashing-red);
}
複製程式碼

自定義屬性和前處理器中的變數最大的不同在於"鍵值對"的語法規則。自定義屬性採用 var() 函式去取值。

另一個明顯的區別是名稱。它們之所以被稱為"自定義屬性",是因為它們是純粹的 CSS 屬性。在前處理器中,你可以在任何位置宣告和使用變數,包括外部宣告塊,在媒體查詢中,甚至在選擇器中也可以使用,例如:

$breakpoint: 800px;
$smashing-red: #d33a2c;
$smashing-things: ".smashing-text, .cats";

@media screen and (min-width: $breakpoint) {
  #{$smashing-things} {
    color: $smashing-red;
  }
}
複製程式碼

而使用自定義屬性,上面的大多數示例都是無效的。

自定義屬性和常規 CSS 屬性的用法是一樣的。把它們當作動態屬性會比變數更好。這意味著它們只能在宣告塊中使用,換句話說,自定義屬性和選擇器是強繫結的。這可以是 :root 選擇器或任何其它有效的選擇器。

:root { --smashing-red: #d33a2c; }

@media screen and (min-width: 800px) {
  .smashing-text, .cats {
    --margin-left:  1em;
  }
}
複製程式碼

你可以在屬性宣告中的任何地方獲取變數宣告的值,這個意味著它們可以作為單個值使用,作為一個簡寫語句的一部分,甚至是在 calc() 函式中使用。

.smashing-text, .cats {
  color: var(--smashing-red);
  margin: 0 var(--margin-horizontal);
  padding: calc(var(--margin-horizontal) / 2)
}
複製程式碼

但是,它們不能用於媒體查詢或選擇器,包括 :nth-child()。

關於語法和自定義屬性的工作原理可能還有很多,比如如何使用 fallback 值,以及我們還可以將變數分配給其他變數,本文介紹的基礎知識應該已經足以讓大家理解其中的概念。更多關於自定義屬性工作方式的詳細資訊,可以閱讀由 Serg Hospodarets 編寫的“是時候開始使用自定義屬性了”這篇文章。

動態變數與靜態變數

拋開樣式差異,前處理器中的變數和自定義屬性中的變數之間最大的差別是作用域。我們可以將變數根據作用域分為靜態變數和動態變數兩個部分。前處理器中的變數是靜態的,而自定義屬性是動態的。

在 CSS 中,靜態意味著你可以在編譯過程中更新變數的值,但是這不能改變它之前的值。

$background: blue;
.blue {
  background: $background;
}
$background: red;
.red {
  background: $background;
}
複製程式碼

結果是:

.blue {
  background: blue;
}
.red {
  background: red;
}
複製程式碼

一旦編譯成了 CSS,這個變數就會消失。這意味著我們在讀取一個 .scss 檔案並輸出的時候並不需要關心 HTML、瀏覽器或其它輸入,而自定義屬性並非如此。 前處理器確實有一種“塊級作用域”,其中變數可以在選擇器,函式或 mixin 中臨時更改。這改變了塊內變數的值,但它仍然是靜態的。這與塊有關,而不是選擇器。在下面的例子中,變數 $background 在 .example 類內部被改變。即使我們使用相同的選擇器,它也會變回塊級作用域之外的初始值。

$background: red;
.example {
  $background: blue;
  background: $background;
}

.example {
  background: $background;
}
複製程式碼

編譯後:

.example {
  background: blue;
}
.example {
  background: red;
}
複製程式碼

自定義屬性不同於前處理器。在涉及自定義屬性的地方,動態範圍意味著它們受到繼承和級聯的影響。屬性與選擇器繫結,如果值發生變化,就會像其他 CSS 屬性一樣影響所有匹配的 DOM 元素。

這聽起來很贊,因為你可以在媒體查詢中,通過使用類似 hover 的偽類選擇器甚至是 JavaScript 改變自定義屬性的值。

a {
  --link-color: black;
}
a:hover,
a:focus {
  --link-color: tomato;
}
@media screen and (min-width: 600px) {
  a {
    --link-color: blue;
  }
}

a {
  color: var(--link-color);
}
複製程式碼

我們不需要在自定義屬性使用的地方去修改它,我們可以通過 CSS 去修改它的值。這意味著同一個自定義屬性,可以在不同的地方,或者是上下文中有不同的值。

全域性變數與區域性變數

除了靜態變數和動態變數之外,變數還可以是全域性的或區域性的。如果你經常編寫 JavaScript,你可能會更瞭解這一點。變數既可以作用在應用程式全域性環境中,也可以將其作用域限制在特定的功能或程式碼塊中。

CSS 也一樣。有全域性的變數,也有區域性的變數。品牌顏色、垂直間距、排版方式,這些你可能會希望都能在 app 端和網頁中全域性呼叫。當然也有一些區域性的東西,比如,按鈕元件可能具有大小尺寸。你不希望這些按鈕的大小適用於所有輸入元素或頁面上的每個元素。

這是我們在 CSS 中熟悉的應用場景。我們開發了設計系統、命名規範和 JavaScript 庫,這些可以分離區域性元件和全域性元件。自定義屬性給這類問題提供了新的思路。

通常 CSS 自定義屬性的範圍侷限於我們指定的選擇器中。這看起來有點像區域性變數。但是,自定義屬性具有繼承的特性,所以在大多數情況下,它們表現的更像全域性變數 —— 特別是在應用於 :root 選擇器的時候。這意味著我們需要考慮如何使用它們。

大量的示例都表示將自定義屬性應用到 :root 元素上,對於 Demo 這還說得過去,但它可能會汙染全域性作用域,從而導致意外的繼承問題。幸運的是,我們已經吸取了教訓。

全域性變數趨於靜態

可能會有一些例外,但通常來說,CSS 中的大多數的全域性變數也是靜態的。

比如品牌顏色、字型和間距之類的變數不會在不同的元件之間產生太大的變化。當它們發生變化時,這往往是一個全域性性的品牌重塑,或者是在一個成熟產品上很少發生的其他重大變化。對於這些變數來說它們仍然是有意義的,它們在很多地方被使用,而變數有助於保持一致性。但讓它們成為動態變數是沒有意義的。這些變數的值不會以任何動態的方式變化。

因此,我強烈建議對全域性(靜態)變數使用前處理器。這不僅確保了它們始終是靜態的,而且還可以在程式碼中顯得更直觀。這可以使 CSS 更易於閱讀和維護。

區域性靜態變數也儘量少用

你可能會認為,全域性變數是趨於靜態的,那麼相反的,可能所有的區域性變數都應該是動態的,但其實遠不如全域性變數是靜態的開發起來更方便。 而區域性靜態變數在很多情況下是動態的,是因為我在元件檔案中使用前處理器變數,也主要是為了開發的方便。

【譯】CSS 自定義屬性的策略指南

我的 SCSS 可能看起來像這樣:

$button-sml: 1em;
$button-med: 1.5em;
$button-lrg: 2em;

.btn {
  // Visual styles
}

.btn-sml {
  font-size: $button-sml;
}

.btn-med {
  font-size: $button-med;
}

.btn-lrg {
  font-size: $button-lrg;
}
複製程式碼

顯然,如果我多次使用變數或從變數計算得到 margin 或 padding 值,這個示例將更有意義。然而,快速原型化不同尺寸的能力可能是一個充分的理由。

因為大多數靜態變數都是全域性的,所以我喜歡區分只在元件內部使用的靜態變數。為此,可以在這些變數前面加上元件名,或者可以使用另一個字首,如元件的 c 變數名或 l 變數名。您可以使用任何您想要的字首,或者可以在全域性變數前面加上字首。無論您選擇什麼,區分都是很有幫助的,特別是當轉換一個現有的程式碼來使用自定義屬性時。

何時使用自定義屬性

如果可以在元件內部使用靜態變數,那麼什麼時候應該使用自定義屬性呢? 將現有的前處理器變數轉換為自定義屬性通常沒什麼意義。畢竟,自定義屬性的用途是完全不同的。當我們有 CSS 屬性時,自定義屬性是有意義的,尤其是在 DOM 中(尤其是動態條件),例如 :fouces、hover、媒體查詢或 JavaScript。

我猜想我們將始終使用某種形式的靜態變數,儘管我們將來可能需要更少的靜態變數,因為自定義屬性提供了組織邏輯和程式碼的新方法。在此之前,我認為在大多數情況下,我們可以將前處理器變數和自定義屬性組合使用。

我們可以為自定義屬性分配靜態變數。無論它們是全域性的還是區域性的,在許多情況下,將靜態變數轉換為區域性動態自定義屬性都是有意義的。

注意:您知道 $var 是自定義屬性的有效值嗎? Sass 的最新版本認識到了這一點,因此我們需要插入分配給自定義屬性的變數,如: #{$var}。這告訴 Sass 您希望輸出變數的值,而不是樣式表中的 $var。這隻適用於自定義屬性等情況,其中變數名也可以是有效的 CSS。

如果我們以上面的按鈕示例為例,決定所有的按鈕都應該使用移動裝置上的小變化,而不考慮 HTML 中應用的類,這是一種更動態的情況。為此,我們應該使用自定義屬性。

$button-sml: 1em;
$button-med: 1.5em;
$button-lrg: 2em;

.btn {
  --button-size: #{$button-sml};
}

@media screen and (min-width: 600px) {
  .btn-med {
    --button-size: #{$button-med};
  }
  .btn-lrg {
    --button-size: #{$button-lrg};
  }
}

.btn {
  font-size: var(--button-size);
}
複製程式碼

這裡,我建立了一個自定義屬性: --button-size。這個自定義屬性最初的作用域是使用 btn 類的所有按鈕元素。然後,我將 btn-med 和 btn-lrg 類的按鈕大小更改為 600px 以上。最後,我將這個自定義屬性應用到一個位置的所有按鈕元素。

不要聰明過頭

自定義屬性的動態特性允許我們建立一些聰明而複雜的元件。

隨著前處理器的推出,我們中的許多人可以使用 mixin 和自定義函式建立具有巧妙抽象的庫。在有限的情況下,像這樣的例子仍然有用,但大多數情況下,隨著使用前處理器的時間越長,我使用的功能就越少。現在,我使用前處理器的場景幾乎只在靜態變數的部分。

自定義屬性同樣也會出現這樣的狀況,我期待看到更多聰明的例子。但從長遠來看,可讀和可維護的程式碼總會是更好的選擇(至少在專案中是這樣)。

我最近在 Free Code Camp Medium 上閱讀了關於此主題的優秀文章。它是由 Bill Sourour 撰寫的,叫‘ Don’t Do It At Runtime. Do It At Design Time ’。與其解釋他的觀點,我更推薦你先去看一下這篇文章。

預處理變數和自定義屬性之間的一個關鍵區別是:自定義屬性在執行時工作。這意味著,在複雜性方面自定義屬性是可以被接受的,因為前處理器沒有自定義屬性這麼好的辦法。

最近我常用來舉例說明的一個例子是:

:root {
  --font-scale: 1.2;
  --font-size-1: calc(var(--font-scale) * var(--font-size-2));
  --font-size-2: calc(var(--font-scale) * var(--font-size-3)); 
  --font-size-3: calc(var(--font-scale) * var(--font-size-4));   
  --font-size-4: 1rem;     
}
複製程式碼

這產生了一個比例元件。比例元件是一系列和比率相互關聯的數字。它們經常用於網頁設計和開發來設定字型大小或間距。

在本例中,每個自定義屬性都是使用 calc() 確定的,方法是取之前的自定義屬性的值並將其乘以比率。這樣做,我們可以得到下一個數字。

這意味著在執行時計算比率,您可以通過只更新 --font-size 屬性的值來更改它們。例如:

@media screen and (min-width: 800px) {
  :root {
    --font-scale: 1.33;
  }
}
複製程式碼

如果你想改變比例,這比再次計算所有的值要聰明、簡潔和快得多。這也是我在開發過程中不會做的事情。

雖然上面的例子對於原型設計很有用,但在開發中我更喜歡看到類似這樣的東西:

:root {
  --font-size-1: 1.728rem;
  --font-size-2: 1.44rem;
  --font-size-3: 1.2em;
  --font-size-4: 1em;
}

@media screen and (min-width: 800px) {
  :root {
    --font-size-1: 2.369rem; 
    --font-size-2: 1.777rem;     
    --font-size-3: 1.333rem; 
    --font-size-4: 1rem;     
  }
}
複製程式碼

與 Bill 的文章中的例子類似,我發現檢視實際值的含義很有幫助。我們閱讀程式碼的次數比我們編寫的次數多得多,而且字型等全域性引數在專案中很少變化。

上面的例子還不算完美。它違反了以前的規則,即全域性變數應該是靜態的。我更願意使用前處理器變數,並使用前面演示的例子將它們轉換為區域性動態自定義屬性。

避免使用一個自定義屬性到另一個自定義屬性的情況也很重要。當我們命名這樣的屬性時,會發生這種情況。

修改屬性值而不是變數

這是使用自定義屬性的最重要準則之一。

通常來說,不推薦修改僅有單一目的的自定義屬性。雖然這很容易實現,因為這正是前處理器應該做的事情,但對於自定義屬性來說這沒有多大意義。

在這個例子中,示例元件上我們建立了兩個使用的自定義屬性。根據螢幕的尺寸變化,將自定義屬性 --font-size-small 修改為 --font-size-large。

:root {
  --font-size-small: 1.2em;
  --font-size-large: 2em;            
}
.example {
  font-size: var(--font-size-small);
}
@media screen and (min-width: 800px) {
  .example {
    font-size: var(--font-size-large);
  }
}
複製程式碼

更好的方式是在這個組建當中只定義一個自定義屬性,然後通過媒體查詢或者其它的選擇器改變這個屬性的值。

.example {
  --example-font-size: 1.2em;
}
@media screen and (min-width: 800px) {                             
  .example {
    --example-font-size: 2em;            
  }
}
複製程式碼

最後,只通過呼叫這個自定義屬性來獲取你想要的屬性值。

.example {
  font-size: var(--example-font-size);
}
複製程式碼

在這個和之前的例子中,我們都僅通過使用媒體查詢去修改自定義屬性的值。也只在一個地方通過 var()去宣告這個自定義屬性,然後 CSS 的值就通過這個自定義屬性自動更新了。

我們故意分離了值宣告和屬性宣告,這樣做的原因其實有很多,響應性設計就是一個最明顯的例子。

使用自定義屬性建立響應佈局

響應式佈局中最大的難點在於它太過於依賴媒體查詢,這導致無論你多麼用心的管理你的 CSS,和元件相關的樣式都會變得很零散。

你很難知道哪些 CSS 屬性會發生改變。CSS 自定義屬性,正好可以幫助我們處理與響應式設計相關的這個邏輯關係,從而降低媒體查詢的使用難度。

如果它改變了,這僅僅只是一個變數而已

媒體查詢中屬性的變化本身就是動態的,自定義屬性正好彌補了 CSS 屬性不具有動態性的特點。這意味著如果你想通過媒體查詢來修改 CSS 屬性,自定義屬性是一個不錯的選擇。

然後,你可以將媒體查詢規則、hover 的狀態或者任何定義了屬性值修改方式的動態選擇器,都移動到文件的頂部。

將邏輯和設計分離

如果你的操作正確,那麼邏輯與設計的分離意味著媒體查詢只是用來改變自定義屬性的值。而這說明與響應性設計相關的所有邏輯都應該放到於文件的頂部,並且無論我們在哪裡看到 var() 宣告語句,我們都能很明顯的知道這個屬性會發生變化。而使用傳統的 CSS 方式,我們是無法察覺這一點的。

我們中的大多數人都非常擅長閱讀和理解 CSS,我們需要思考在不同的狀態下哪些屬性發生了變化。我受夠這樣的方式了。現在自定義屬性幫我們把邏輯和實現連結了起來,所以我們不需要在大腦去跟蹤這些變化,這在專案中真的非常有用!

摺疊邏輯

在文件或函式頂部宣告變數的想法是很早就有的方式。也是大多數語言中推薦的做法,現在我們也可以在 CSS 中完成。以這種方式編寫 CSS,光從視覺角度就很容易區分頂部和之後的程式碼。我要使用它們時我能很方便的找到它們。而這就是我強調的“摺疊邏輯”的概念。 在這個摺疊的上方包含所有前處理器變數和自定義屬性。這包含了所有的引數和自定義屬性可能的變化。這樣我們就很容易的知道自定義屬性發生了哪些變化。

在這段摺疊之後的 CSS 程式碼可讀性也是很高的,這就和你原來寫媒體查詢,和其它必要的程式碼一樣沒有什麼區別。

我們再來看一個關於六列 Flexbox 佈局的網格系統非常簡單的例子:

.row {
  --row-display: block;
}
@media screen and (min-width: 600px) {
  .row {
    --row-display: flex;
  }
}
複製程式碼

這個 --row-display 自定義屬性的初始值設定為 block。當螢幕超過 600px 之後這個值會被重置為 flex。

在摺疊區域下面的程式碼可能看起來像這樣:

.row {
  display: var(--row-display);
  flex-direction: row;
  flex-wrap: nowrap;
}
.col-1, .col-2, .col-3,
.col-4, .col-5, .col-6 {
  flex-grow: 0;
  flex-shrink: 0;
}
.col-1 { flex-basis: 16.66%; }
.col-2 { flex-basis: 33.33%; }
.col-3 { flex-basis: 50%; }
.col-4 { flex-basis: 66.66%; }
.col-5 { flex-basis: 83.33%; }
.col-6 { flex-basis: 100%; }
複製程式碼

我們可以很直觀的知道 --row-display 是一個變數。並且它的值現在應該是 block,而不是 flex。

這是一個簡單的例子,但如果我們要擴充一個填充剩餘空間列的話,flex-grow、flex-shrink 和 flex-basis 值則同樣需要轉換為自定義屬性。你可以嘗試寫一下,或者點選這裡看一下更詳細的例子

基於主題建立自定義屬性

我不推薦使用自定義屬性去建立全域性動態變數,也不推薦將自定義屬性附加到 :root 選擇器作用域下。但是每個規則都有一個例外,對於自定義屬性而言,在建立主題的場景下,這就是一個例外。

有節制的使用全域性自定義屬性可以使主題的建立更容易。

主題化通常指的是讓使用者以某種方式定製UI。這可能類似於在配置檔案上修改顏色。更簡單的說,就像你在 Google Keep 這個應用程式中為你的筆記選擇了一個顏色一樣。

【譯】CSS 自定義屬性的策略指南

主題化通常會有獨立的樣式表用與基於使用者的選擇來覆蓋之前的樣式,或者對於不同的樣式有完全獨立的樣式表檔案。這兩種方法實現都比較困難,並且會對效能產生影響。

用全大寫的方式表示全域性動態屬性

自定義屬性對大小寫是敏感的,自定義屬性建議都是區域性的,如果你需要使用到是全域性動態屬性,推薦使用全大寫的方式。

:root {
  --THEME-COLOR: var(--user-theme-color, #d33a2c);            
}
複製程式碼

全大寫的變數常常用於表示全域性常量。對我們來說,一看到全大寫的變數,則這意味著該屬性是全域性的,我們不應該在區域性去修改它。

避免直接設定全域性動態屬性

自定義屬效能接受一個備選值。我們應該避免直接覆蓋全域性自定義屬性的值並儘量與其它值分離。我們可以使用備選值來實現這一點。

上個例子我們有將 --THEME-COLOR 的值設定為 --user-theme-color。如果 --user-theme-color 未設定,則將使用備選值 #d33a2c。這樣,我們無需在每次使用 --THEME-COLOR 時都提供一個備選值。

在下面的例子中你可能希望的是背景被設定為 green。但是在 :root 作用域下的 --user-theme-color 的值並沒有設定,所以最後 --THEME-COOR 值不會發生變化。

:root {
  --THEME-COLOR: var(--user-theme-color, #d33a2c);            
}
body {
  --user-theme-color: green;
  background: var(--THEME-COLOR);
}
複製程式碼

像這樣間接設定全域性動態屬性可以防止它們在區域性被覆蓋,並確保使用者的設定始終都從根元素繼承。這個約定能避免主題引數被意外的繼承。

如果我們想要將某些特定的屬性暴露出去,我們可以用 * 號選擇器替換 :root 選擇器:

* {
  --THEME-COLOR: var(--user-theme-color, #d33a2c);            
}
body {
  --user-theme-color: green;
  background: var(--THEME-COLOR);
}
複製程式碼

現在 --THEME-COLOR 在每個元素中的值都會重新計算,因此可以使用 --user-theme-color 這個區域性變數。換句話說,這個例子中的背景顏色是 green。

你可以在 “使用自定義屬性控制顏色”一節中看到更多詳細示例。

使用 JavaScript 更新自定義屬性

如果你想通過 JavaScript 設定自定義屬性,這裡有個相當簡單的 API:

const elm = document.documentElement;
elm.style.setProperty('--USER-THEME-COLOR', 'tomato');
複製程式碼

在這裡,我設定了 --USER-THEME-COLOR 元素的值,換言之,:root 將被所有元素繼承。

這並不是一個新的 API, 它僅僅只是用於更新元素樣式的 JavaScript 方法。這些是內聯樣式,因此它們比普通 CSS 具有更高的權重。

這讓區域性自定義變得很容易:

.note {
  --note-color: #eaeaea;
}
.note {
  background: var(--note-color);
}
複製程式碼

在這裡,我設定了預設值 --note-color 並將其作用域限制在了 .note 元件下。即使在這個簡單的例子中,我也將變數宣告與屬性宣告分開。

const elm = document.querySelector('#note-uid');
elm.style.setProperty('--note-color', 'yellow');
複製程式碼

然後,我定位了一個 .note 元素的例項,並僅更改該例項自定義屬性 --note-color 的值。這將比預設值具有更高的權重。

你可以看使用 React 的例子來了解它是如何工作的。這些使用者首選項可以儲存在本地儲存中,在更大應用中也可以儲存到資料庫裡。

使用自定義屬性控制顏色

除了十六進位制值和已命名的顏色之外,CSS 還具有諸如 rgb() 和 hsl() 這樣的顏色方法可以使用。這些允許我們為元件設定例如色調或亮度等特定的顏色屬性。自定義屬性可以與這些方法結合使用。

:root {
  --hue: 25;
}
body {
  background: hsl(var(--hue), 80%, 50%);
}
複製程式碼

這很有用,而前處理器中提供了更多更高階的顏色方法,我們可以通過這些方法去實現顏色的亮化、變暗或去飽和等功能:

darken($base-color, 10%);
lighten($base-color, 10%);
desaturate($base-color, 20%);
複製程式碼

如果瀏覽器自身就提供了這些方法那就更好了。“他們即將到來”,但在 CSS 原生支援擁有這些功能之前,自定義屬性剛好能填補這個空缺。

我們已經看到,自定義屬性不僅可以在比如 rgb()、hsl() 等顏色方法中使用,也可以在 calc() 中使用。這意味著我們可以通過乘法把一個數轉換成百分比的形式,例如 calc(50 * 1%) = 50%。

:root {
  --lightness: 50;
}
body {
  background: hsl(25, 80%, calc(var(--lightness) * 1%));
}
複製程式碼

我們將亮度值儲存為整數的原因是:在將其轉化為百分比之前可以使用 calc 方法來進行轉換。舉個例子,如果我想一個顏色變暗 20%,我可以將其亮度值乘以它 0.8 實現。通過自定義屬性,將亮度的計算方法限制在區域性作用域下,可以使我們的程式碼可讀性更強:

:root {
  --lightness: 50;
}
body {
  --lightness: calc(var(--lightness * 0.8));
  background: hsl(25, 80%, calc(var(--lightness) * 1%));
}
複製程式碼

我們甚至可以抽象出更多的計算方法,並建立類似“基於 CSS 自定義屬性的顏色擴充功能”中提到的方法一樣。這個例子對於大多數主題的實際情況來說可能相對複雜,但是它充分展示了動態自定義屬性的能力。

簡化主題

使用自定義屬性的優點之一是能夠讓主題的建立更加的簡單。應用程式不需要知道自定義屬性是如何使用的。相反,我們通過 JavaScript 或伺服器端程式碼來設定自定義屬性的值,而這些值又直接由樣式表控制。

這意味著我們進一步將邏輯與設計分離。如果你有一個專業的設計團隊,設計師可以通過更新樣式表來決定如何應用自定義屬性,而無需更改一行 JavaScript 或後端程式碼。

自定義屬性,將主題的複雜度移動到了 CSS 中,這種複雜性可能會對 CSS 的維護性產生負面影響,所以請可能保持主題的簡單。

是時候使用自定義屬性了

即使你的專案依然需要支援 IE10 和 IE11,你也可以使用自定義屬性。本文中的大多數示例都與如何編寫和構建 CSS 有關。但是,從可維護性的角度來說,這些好處是非常重要的,本文大多數示例只是減少了原本需要使用更復雜的程式碼才能實現的事情。

我使用一個名為 postcss-css-variables 的工具將自定義屬性的大部分方法轉換為相同功能的靜態方法。而其他類似的工具忽略了媒體查詢或複雜選擇器中的自定義屬性,將其當作預處理變數來處理。

這些工具不能做到的是模擬出自定義屬性的實時特性。而這意味著失去了之前我們在主題和通過 JavaScript 更改屬性中提到的動態特性。這在很多情況下也是可以接受的。畢竟基於不同的情況,自定義UI也不失為一種漸進式增強的方式,而預設的主題對於舊的瀏覽器來說也是完全可以接受的。

載入正確的樣式表

使用 postCSS 的有很多。我通過使用 Gulp 的程式來來區分新舊瀏覽器的樣式表。一個我建立的 Gulp 任務如下:

import gulp from "gulp";
import sass from "gulp-sass";
import postcss from "gulp-postcss";
import rename from "gulp-rename";
import cssvariables from "postcss-css-variables";
import autoprefixer from "autoprefixer";
import cssnano from "cssnano";

gulp.task("css-no-vars", () =>
  gulp
    .src("./src/css/*.scss")
    .pipe(sass().on("error", sass.logError))
    .pipe(postcss([cssvariables(), cssnano()]))
    .pipe(rename({ extname: ".no-vars.css" }))
    .pipe(gulp.dest("./dist/css"))
);

gulp.task("css", () =>
  gulp
    .src("./src/css/*.scss")
    .pipe(sass().on("error", sass.logError))
    .pipe(postcss([cssnano()]))
    .pipe(rename({ extname: ".css" }))
    .pipe(gulp.dest("./dist/css"))
);
複製程式碼

這個結果將輸出兩個 CSS 檔案:一個具有自定義屬性的(styles.css)檔案,另一個用於舊瀏覽器(styles.no-vars.css)。我希望在 IE10 和 IE11 瀏覽器中使用 styles.no-vars.css 這個檔案,其它瀏覽使用常規的 CSS 檔案。

通常,我主張使用功能查詢,但“ IE11 不支援功能查詢”,可是我們已經大量使用了自定義屬性,在這種情況下,為其提供不同樣式表就顯得有意義了。

通過提供不同的樣式表以避免無樣式內容的閃爍並不是一個簡單的事情。如果不需要自定義屬性中的動態特性,可以考慮僅使用適用於所有瀏覽器的 styles.no-vars.css 檔案,而自定義屬性僅作為開發工具。

如果你想充分利用自定義屬性的所有動態特性,我建議使用“關鍵的 CSS 技術”。基於這些技術,主樣式表是非同步載入的,而關鍵的 CSS 是內聯的。你的頁面的 header 可能看起來像這樣:

<head>
  <style> /* inlined critical CSS */ </style>
  <script> loadCSS('non-critical.css'); </script>
</head>
複製程式碼

我們可以擴充套件這個方法,來實現基於瀏覽器是否支援支援自定義屬性來決定載入 styles.css 檔案還是 styles.no-vars.css 檔案的效果。我們可以這樣來實現:

if ( window.CSS && CSS.supports('color', 'var(--test)') ) {
  loadCSS('styles.css');
} else {
  loadCSS('styles.no-vars.css');
}
複製程式碼

結論

如果你想要更有效地管理 CSS、在使用響應式的功能上遇到困難、想要實現類似客戶端主題的效果或者只是想嘗試一下自定義屬性,這篇文章應該可以幫你解答這些問題了。這不僅解釋了 CSS 中動態變數和靜態變數之間的區別,還有如下的其它的幾條規則:

  • 將邏輯從設計中分離;
  • 如果 CSS 屬性需要發生變化,可以考慮使用自定義屬性;
  • 僅修改自定義屬性的值,而不是修改自定義屬性本身;
  • 全域性變數通常是靜態的。

如果理解了以上這些條例,你會發現使用自定義屬性比你想象的要容易得多,甚至可能會改變你對 CSS 的處理方式。

進一步閱讀

是時候使用自定義屬性了 ”,Serg Hospodarets 介紹的自定義屬性的語法和特性的。
漸進式實用的CSS自定義屬性 ”,Harry Roberts 介紹的更多關於主題的有用資訊。
自定義屬性集合”,Mike Riethmuller 在 CodePen 上提供的大量的不同的示例。

檢視更多分享,請關注閱文集團前端團隊公眾號:

【譯】CSS 自定義屬性的策略指南

相關文章