進階:玩轉 CSS 變量

2021-02-20 Vue中文社區

如果當年的 CSS  預處理器變量對於初入前端的我來說是開啟了新世界的大門,那麼 CSS 變量對於我來說無疑就是晴天霹靂。其功能不但可以優雅的處理之前 js 不好處理或不適合的業務需求。更在創造力無窮的前端開發者手中大放異彩。

基礎用法

在前端的領域中,標準的實現總是比社區的約定要慢的多,前端框架最喜歡的 $ 被 Sass 變量用掉了。而最常用的 @ 也被 Less 用掉了。官方為了讓 CSS 變量也能夠在 Sass 及 Less 中使用,無奈只能妥協的使用 --。

<style>
  /* 在 body 選擇器中聲明了兩個變量  */ 
  body {
    --primary-color: red;
    /* 變量名大小寫敏感,--primary-color 和 --PRIMARY-COLOR 是兩個不同變量 */  
    --PRIMARY-COLOR: initial;  
  }

  /** 同一個 CSS 變量,可以在多個選擇器內聲明。優先級高的會替換優先級低的 */
  main {
    --primary-color: blue;
  }
    
  /** 使用 CSS 變量 */
  .text-primary {
    /* var() 函數用於讀取變量。 */  
    color: var(--primary-color)
  }
<style>
    
<!-- 呈現紅色字體,body 選擇器的顏色  -->    
<div class="text-primary">red</div> 
    
<!-- 呈現藍色字體,main 選擇器定義的顏色  -->    
<main class="text-primary">blue</main>
    
<!-- 呈現紫色字體,當前內聯樣式表的定義  --> 
<div style='--primary-color: purple" class="text-primary">purple</main>    

這裡我們可以看到針對同一個 CSS 變量,可以在多個選擇器內聲明。讀取的時候,優先級最高的聲明生效。這與 CSS 的"層疊"(cascade)規則是一致的。

由於這個原因,全局的變量通常放在根元素:root裡面,確保任何選擇器都可以讀取它們。

:root {
  --primary-color: #06c;
}

同時, CSS 變量提供了 JavaScript 與 CSS 通信的方法。就是利用 js 操作 css 變量。我們可以使用:

<style>
  /* ...和上面 CSS 一致 */  
</style>    

<!-- 呈現黃色字體  -->    
<div class="text-primary">red</div> 
    
<!-- 呈現藍色字體,main 選擇器定義的顏色  -->    
<main id='primary' class="text-primary">blue</main>
    
<!-- 呈現紫色字體,當前內聯樣式表的定義  --> 
<div id="secondary" style='--primary-color: purple" class="text-primary">purple</main>    

<script>
// 設置變量
document.body.style.setProperty('--primary-color', 'yellow');
                                                           
// 設置變量,js DOM 元素 ID 就是全局變量,所以直接設置 main 即可
// 變為 紅色
primary.style.setProperty('--primary-color', 'red');    

// 變為 黃色,因為當前樣式被移除了,使用 body 上面樣式
secondary.style.removeProperty('--primary-color');

// 通過動態計算獲取變量值
getComputedStyle(document.body).getPropertyValue('--primary-color')

</script>

我們可以在業務項目中定義以及替換 CSS 變量,大家可以參考 mvp.css[1]。該庫大量使用了 CSS 變量並且讓你去根據自己需求修改它。

: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 變量,可以更友好的和設計師的設計意圖結合在一起。也易於修改,在業務項目中合理使用無疑可以事半功倍。

實現默認配置

如果讓我來思考,我肯定無法想像出結合 CSS 預處理器 + CSS 變量便可以實現組件樣式的默認配置。這裡我先介紹兩個關於該功能的前置知識點:

事實上,CSS 變量的 var() 函數還可以使用第二個參數,表示變量的默認值。如果該變量此前沒有定義或者是無效值,就會使用這個默認值。

/* 沒有設置過 --primary-color,顏色默認使用 #7F583F */
color: var(--primary-color, #7F583F);

雖然目前 CSS 變量不是新的屬性,但終究不是所有的瀏覽器都支持 CSS 變量的,這裡我們還是要考慮一下優雅降級。

/* 對於不支持 CSS 變量的瀏覽器,可以採用下面的寫法。*/
a {
 /* 顏色默認值 */
  color: #7F583F;
  /* 不支持則不識別該條規則 */  
  color: var(--primary);
}

結合 CSS 處理器 + CSS 變量便可以實現組件樣式的默認配置。這裡參考了有贊的 Vant Weapp[2] 的做法。有贊代碼 **theme.less**[3] 如下所示:

// 先導入所有 Less 變量
@import (reference) './var.less';

// 利用正則去替換 Less 變量 為 CSS 變量
.theme(@property, @imp) {
@{property}: e(replace(@imp, '@([^() ]+)', '@{$1}', 'ig'));
@{property}: e(replace(@imp, '@([^() ]+)', 'var(--$1, @{$1})', 'ig'));
}

函數效果如下所示:

@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);
}

我們可以看到每調用一次 Less 函數將會被編譯成兩個屬性。第一個屬性的設定對於不支持 CSS 變量的設備可以直接使用,如果當前設備支持 CSS 變量,則會使用 CSS 變量,但是由於當前 CSS 變量未定義,就會使用變量的默認值。雖然 '@button-default-height 雖然也是一個變量,但是該變量僅僅只是 less 變量,最終生成的代碼中並沒有 --button-default-height 這樣的變量。此時我們就可以在使用樣式的位置或者 :root 中添加變量 --button-default-height。

這種方式更適合組件開發,因為該方案不聲明任何 css 變量,只是預留的 css 變量名稱和默認屬性。這樣的話,無論開發者的選擇器優先度有多低,代碼都可以很容易的覆蓋默認屬性。因為我們僅僅使用 css 的默認值。

大家可能有時候會想,這樣的話,我們不是有更多的代碼了嗎?其實未必,事實上我們可以直接直接在頁面內部定義變量樣式。其他組件直接通過 style 去使用頁面內的變量。當然了,事實上書寫的代碼多少,重點在於想要控制默認樣式的粒度大小。粒度越小,則需要在各個組件內部書寫的變量越多,粒度大,我們也就不必考慮太多。

Space Toggle 邏輯切換

CSS 沒有邏輯切換似乎是一種共識,但是我們可以利用選框(單選與多選)和 CSS 變量來實現判斷邏輯。我們先來看看如何使用 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> 

這裡因為一個變量 --toggler 使用空格 或者 initial 而產生了不同的結果,基於這樣的結果我們不難想像我們可以觸發變量的修改而產生不同的結果。

他不是一個 bug,也不是一個 hack。他的原理完完全全的在 CSS Custom Properties 規範[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.

解釋如下,事實上 -foo: ; 這個變量並不是一個無效值,它是一個空值。initial 才是 CSS 變量的無效值。其實這也可以理解,css 沒有所謂的空字符串,空白也不代表著無效,只能使用特定值來表示該變量無效。這個時候,我們再回頭來看原來的 CSS 代碼。

.red-box {
  /* 當前為空值 */
  --toggler: ;

  /* 因為 var(--toggler) 得到了空,所以得到結果 為 --red-if-toggler:  red */
  --red-if-toggler: var(--toggler) red;
  /** 變量是 red, 不會使用 green */
  background: var(--red-if-toggler, green); /* will be red! */
}
.green-box {
  /** 當前為無效值 */
  --toggler: initial;
  /** 仍舊無效數據,因為 var 只會在參數不是 initial 時候進行替換 */
  --red-if-toggler: var(--toggler) red;
  /** 最終無效值沒用,得到綠色 */
  background: var(--red-if-toggler, green); /* will be green! */

  /* 根據當前的功能,我們甚至可以做到 and 和 or 的邏輯 
   * --tog1 --tog2 --tog3 同時為 空值時是 紅色
   */
  --red-if-togglersalltrue: var(--tog1) var(--tog2) var(--tog3) red;

 /*
  * --tog1 --tog2 --tog3 任意為 空值時是 紅色
  */ 
  --red-if-anytogglertrue: var(--tog1, var(--tog2, var(--tog3))) red;
}

新式媒體查詢

當我們需要開發響應式網站的時候,我們必須要使用媒體查詢 @media。先看一下用傳統的方式編寫這個基本的響應式 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 變量來優化代碼結構,我們可以寫出這樣的代碼:

/** 移動優先的樣式規則 */
.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 代碼非常清晰,數據和邏輯保持在一個 CSS 規則中,而不是被 @media 切割到多個區塊中。這樣,不但更容易編寫,也更加容易開發者讀。詳情可以參考 css-media-vars[5]。該代碼庫僅僅只有 3kb 大小,但是卻是把整個編寫代碼的風格修改的完全不同。原理如下所示:

  
/**
 * 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;
}

/* 把當前變量變為空值 */
@media print {
  html { --media-print: ; }
}

@media screen {
  html { --media-screen: ; }
}

@media speech {
  html { --media-speech: ; }
}

/* 把當前變量變為空值 */
@media (max-width: 37.499em) {
  html {
    --media-xs: ;
    --media-lte-sm: ;
    --media-lte-md: ;
    --media-lte-lg: ;
  }
}

其他

繼 CSS 鍵盤記錄器[6] 暴露了 CSS 安全性問題之後,CSS 變量又一次讓我看到了玩技術是怎麼樣的。CSS Space Toggle 技術不但可以應用於上面的功能,甚至還可以編寫 UI 庫 augmented-ui[7] 以及  掃雷[8] 遊戲。這簡直讓我眼界大開。在我有限的開發生涯中,很難找到類似於 css 這種設計意圖和使用方式差異如此之大的技術。

CSS 是很有趣的,而 CSS 的有趣之處就在於最終呈現出來的技能強弱與你自身的思維方式,創造力是密切相關的。上文只是介紹了 CSS 變量的一些玩法,也許有更多有意思的玩法,不過這就需要大家的創造力了。

augmented-ui[9]

css-media-vars[10]

css-sweeper[11]

參考資料[1]

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 規範: 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

相關焦點

  • CSS字體樣式
    DOCTYPE html><html>    <head>        <meta charset="UTF-8" />        <title>字體類型</title>        <style type="text/css">
  • VBA進階 | 數組基礎06: 與數組相關的函數——Array函數與IsArray函數
    由Array函數返回的數組只可賦值給一個Variant型變量,不能賦值給已聲明為數組變量的變量。Array函數返回的數組中元素的順序與傳遞給函數的參數值的順序相同。Array函數總是返回Variant類型的數組,但元素的數據類型可以不同,這取決於傳遞給該函數的數值類型。
  • 【前端-CSS動畫】製作聖誕節彩燈
    css動畫 animationcss動畫 keyframesbox-shadow
  • HTML+CSS系列:登錄界面實現
    /css/style.css" /> <link rel="stylesheet" href="css/iconfont.css"/> <title>登錄界面</title> </head> <body> <div id="bigBox"> <
  • jquery css() 增加值
    script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script><script>  $( function() {    $( "p" ).click( function() {      $( this ).css
  • css3 تە نۇر چاقناپ رەڭ ئۆزگەرتىپ تۇرىدىغان كۇنۇكا ياساش (سىنلىق)
    css3 تە ھەركەتجان تەگلىكى بار كۇنۇپكا ياساش
  • CSS魔法堂:Box-Shadow沒那麼簡單啦:)
    二話不說看效果3D小球<style type="text/css">.ball{  background: rgba(100,100,100,0.2);  width: 100px;  height: 100px;  padding
  • 純CSS實現Tab頁籤樣式
    doctype html><html lang="zh"><head> <meta charset="UTF-8"> <title>純CSS實現Tab頁籤樣式</title> <style type="text/css"> body {
  • 用JQ + CSS實現浪漫表白必備
    DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><title>浪漫表白 By:阿杜</title> <style type = "text/css">
  • css動畫 | 實現炫酷的旋轉動畫
    DOCTYPE html><html> <head> <meta charset="utf-8"> <title></title> <style type="text/css"> body,html{ margin: 0; padding: 0; height
  • CSS高級技巧:陰影效果
    點擊上方「web前端網頁設計」一鍵關注,更多網頁設計 UI設計 Html div css HTML5+CSS3、JavaScript
  • CSS樣式大全
    (微信後臺回復「CSS」,即可獲得關於css設計的幫助文檔哦)字體屬性:(font)大小 {font-size: x-large;}(特大) xx-small;(極小) 一般中文用不到,只要用數值就可以,單位:PX、PD樣式 {font-style: oblique;}(偏斜體) italic;(斜體) normal;(正常)行高
  • 公司的一個面試題:如何用CSS讓一個容器水平垂直居中?
    DOCTYPE html>   <html>      <head>          <meta charset="UTF-8">          <title>demo</title>      </head>      <body>          <style type="text/css
  • 又一波乾貨:CSS 資源大全
    偶爾在http://Hop.ie上寫博客,目前在建設@cssanimationCSS Commits – 最近忙於 CSSWG 的公共 Mercurial 庫Scott Jehl – responsiblerwd 的作者,現在 abookapart上 面在打折Dudley Storey – Web開發者、作者、老師以及演說者。Zoe M.
  • 嬛嬛的進階之路: 嬪妃等級大起底
    《甄嬛傳》宮鬥情節讓人看的酣暢淋漓,同時它也完整地揭示了一個女人在後宮的進階之路。   甄嬛晉封時誤穿純元皇后的吉服,皇上大怒,禁足了甄嬛,未封成莞妃。  皇上讓甄嬛回宮,封為熹妃,賜鈕秙祿大姓,用半副皇后的儀仗迎甄嬛回宮。  甄嬛誕下一對龍鳳胎,弘瞻和靈犀,皇上龍顏大悅,晉甄嬛為熹貴妃。
  • 送你一朵小紅花,CSS實現旋轉的小紅花
    lt;meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>送你一朵小紅花:公眾號AlbertYang</title> <link rel="stylesheet" href="style.css
  • 詞彙進階:抗擊新冠肺炎疫情中英對照,共600條詞彙!
    詞彙進階:-ble / -aunt詞彙進階:超活躍後綴ate詞彙進階:後綴 -acious/-taneous/-age詞彙進階:-nounce詞彙進階:liter-/liber-/ad- 詞彙精講詞彙進階:-sist詞彙進階:學科詞彙-logy/-ics詞彙進階:學科詞彙進階
  • C語言進階技術:同事這些操作把我驚呆了!
    我們在位置1進行兩個變量的定義,成功編譯運行得到如上的結果,符合我們的預期,然而當我們去掉位置1進行位置2的定義,程序卻無法進行編譯,看來跟我們預期在編譯過程中直接展開.c文件是一致的。比如說你需要對源文件中的一些靜態變量進行相關的監控處理,然而又不想在本文件中增加測試代碼,於是便可以在#include"xxx.c"中進行測試函數的編寫來供使用,比如 : 1//FileName :main  2#include <stdio.h> 3#include <stdlib.h> 4 5static int
  • CSS Grid 網格布局:「16」使用 place-content 屬性指定網格元素水平、垂直方向元素分布方式
    語法如下所示:<style type="text/css">.container { place-content: <align-content> <justify-content>?
  • 【狩獵進階】勇者的遊戲,狩獵有多險?(上篇)
    北美狩獵微信號:NorthAmericaHunting (←長按複製)【狩獵進階】狩獵第一常識,看狩獵進階!