Vue SFC Style CSS 變量注入詳解(新版)

CSS 變量能夠跟 JavaScript 更好的通訊,CSS 變量是運行時。

經過本文你會認識並理解如下概念:css

  1. SFC Style - 單文件組件的樣式;
  2. 原生 CSS 變量 - CSS 做者定義的標準規範;
  3. SFC Style Variables 提案(舊版);
  4. SFC style CSS variable injection(新版);
  5. Vue3 中的使用 CSS 變量注入以及使用原生 CSS 變量;
  6. 變量注入的背後原理;
  7. CSS 變量注入的優點。

SFC Style Variables 提案中介紹到, Vue SFC 樣式提供了簡單的 CSS 組合和封裝,但它是純靜態的 — 這意味着到目前爲止咱們尚未能力在運行時根據組件的狀態動態更新樣式。html

如今大多數現代瀏覽器都支持原生 CSS 變量,咱們能夠利用它輕鬆鏈接組件的狀態和樣式。vue

SFC Style 簡單介紹

Vue 單文件組件 (SFC) 規範 中介紹到,.vue 文件是一個自定義的文件類型,用類 HTML 語法描述一個 Vue 組件。每一個 .vue 文件包含三種類型的頂級語言塊 <template><script><style>,還容許添加可選的自定義塊。node

Style 語言塊:react

  • 默認匹配:/\.css$/
  • 一個 .vue 文件能夠包含多個 <style> 標籤。webpack

    <style> 標籤能夠有 scoped 或者 module 屬性 (查看 scoped CSS 和 CSS Modules) 以幫助你將樣式封裝到當前組件。具備不一樣封裝模式的多個 <style> 標籤能夠在同一個組件中混合使用。git

  • 任何匹配 .css 文件 (或經過它的 lang 特性指定的擴展名) 的 webpack 規則都將會運用到這個 <style> 塊的內容中。

vue-loader 會解析文件,提取語言塊,若有必要會經過其它 loader 處理,最後將他們組裝成一個 ES Module,它的默認導出是一個 Vue.js 組件選項的對象。github

Stlye 模塊中能夠使用 lang 屬性,指定 CSS 預處理語言(sass、less、stylus),以下:web

/* lang 屬性指定擴展名 */
<style lang="sass">
  /* write Sass! 
</style>

還能夠使用 src 屬性,引入外部樣式資源:express

<style src="./style.css"></style>

/* 從 npm 依賴中引入資源 */
<style src="todomvc-app-css/index.css">

更多 Vue 單文件組件 (SFC) 規範 介紹。

原生 CSS 變量

CSS 變量是 CSS 做者定義的標準規範。

image.png

CSS 變量又稱爲 CSS 自定義屬性,它包含的值能夠在整個文檔中重複使用,示例以下:

/* :root 僞類表明 HTML 文檔的根元素,是存放自定義屬性的最佳位置。*/
:root {
  /* --text-color 爲自定義屬性 */
  --text-color: #000000; 
}  

p {
  /* 使用時,須要使用 var() 函數並傳入自定義屬性。 */
  color: var( --text-color );
  font-size: 16px;
}

h1 {
  color: var( --text-color );
  font-size: 42px;
}

定義及使用 CSS 自定義屬性

在使用 CSS 自定義屬性以前,咱們須要先聲明自定義屬性,屬性名須要以兩個減號(--)開始,屬性值則能夠是任何有效的 CSS 值。和其餘屬性同樣,自定義屬性也是寫在規則集以內的,以下:

element {
  --main-bg-color: brown;
}

規則集所指定的選擇器,定義了自定義屬性的可見做用域。一般的最佳實踐是定義在根僞類 :root 下,這樣就能夠在HTML文檔的任何地方訪問到它了。

:root {
  --main-bg-color: brown;
}
注意:自定義屬性名是大小寫敏感的, --my-color--My-color 會被認爲是兩個不一樣的自定義屬性。

如前所述,使用一個局部變量時用 var()) 函數包裹以表示一個合法的屬性值:

element {
  background-color: var(--main-bg-color);
}
:root {
  --main-bg-color: brown;
}

.one {
  color: white;
  background-color: var(--main-bg-color);
  margin: 10px;
  width: 50px;
  height: 50px;
  display: inline-block;
}

關於 CSS 自定義屬性的繼承性、備用值等更多介紹,請參考使用CSS自定義屬性(變量)

SFC 提案

sfc-style-variables 主要概述中指出,此提案支持單文件組件狀態驅動的 CSS 變量注入到單文件組件樣式中

<template>
  <div class="text">hello</div>
</template>

<script>
export default {
  data() {
    return {
      color: 'red'
    }
  }
}
</script>

<style vars="{ color }">
.text {
  color: var(--color);
}
</style>

爲何使用狀態驅動的 CSS 變量

因爲 Vue SFC 樣式提供了簡單的 CSS 搭配和封裝,但它是純靜態的 — 這意味着到目前爲止咱們尚未能力在運行時根據組件的狀態動態更新樣式

如今大多數現代瀏覽器都支持原生 CSS 變量,咱們能夠利用它輕鬆鏈接組件的狀態和樣式。

關於提案設計

在此提案設計中(舊版),Vue SFC Style 支持 vars 綁定,它接受一個 key/values 表達式做爲 CSS變量注入。它與 <template> 中的表達式在相同的上下文中進行計算。

變量將做爲內聯樣式應用於組件的根元素。在上面的示例中,給定一個值爲 {color:'red'}vars 綁定,呈現的 HTML 將是:

<div style="--color:red" class="text">hello</div>

scoped 模式下

當在 scoped 模式下使用,須要確保 CSS 變量不會泄漏到後代組件或不當心將 CSS 變量遮蔽到 DOM 樹的更高層。應用的 CSS 變量將以組件的做用域 ID 爲前綴:

<div style="--6b53742-color:red" class="text">hello</div>

請注意,當 scoped 和 vars 同時存在時,全部 CSS 變量都被視爲本地變量。

在這種狀況下,使用全局 CSS 變量,須要使用 global: 前綴:

<style scoped vars="{ color }">
h1 {
  color: var(--color);
  font-size: var(--global:fontSize);
}
</style>

舊版提案設計的弊端

  1. 須要手動聲明 vars 以公開能夠使用的變量。
  2. 沒有明顯的視覺暗示變量被注入和響應。
  3. scoped/non-scoped 模式下的不一樣行爲。
  4. non-scoped 模式下,CSS 變量會泄漏到子組件中。
  5. scoped 模式下,使用在組件外部聲明的普通 CSS 變量須要 global: 前綴。(一般 CSS 變量用法最好在組件內外保持相同)

新版提案的改進

爲了解決上述問題,新版改進用法以下:

<template>
  <div class="text">hello</div>
</template>

<script>
  export default {
    data() {
      return {
        color: 'red',
        font: {
          size: '2em'
        }
      }
    }
</script>

<style>
  .text {
    color: v-bind(color);

    /* expressions (wrap in quotes) */
    font-size: v-bind('font.size');
  }
</style>
  • 無需明確聲明哪些屬性被注入爲 CSS 變量(從CSS 中的使用 v-bind() 進行推斷);
  • 反應變量的視覺差異更明顯;
  • scoped/non-scoped 模式下的相同行爲;
  • 不會泄漏到子組件中;
  • 普通 CSS 變量的使用不受影響。

在 Vue3 中使用

示例不詳細介紹,請結合註釋進行理解。

示例中包含:

  1. 使用了新的 script setup ;
  2. 原生 CSS 變量的定義以及使用 var()
  3. CSS 變量注入的使用 v-bind
  4. 在運行時,響應式改變 CSS 樣式;
  5. 推薦風格。
<template>
  <div class="root">
      <span class="test" @click="changeColor"> Vue GoldenLayout</span>
    <div>
</template>

// script setup
<script lang="ts" setup>
import { defineComponent, reactive } from "vue";

// 將 css 樣式單獨抽離
import { style } from "../styles/vlogo";
const css = reactive({ ...style });

// 點擊,修改組件的樣式
const changeColor = () => (css.color = "yellow");
</script>

<style>
.root {
  // 原生 css 變量的定義
  --custom-color: v-bind("css.background");
}
.test {
  // 使用 v-bind 進行狀態驅動 
  color: v-bind("css.color");
  // 使用 var()
  background: var(--custom-color);
}
</style>

最終呈現的效果以及編譯後的 HTML 、CSS:

image.png

注:在 vue3 中使用 CSS 變量注入,推薦的風格是將 CSS 樣式單獨抽離出去

變量注入的背後原理

咱們在上文 SFC Style 簡單介紹中瞭解到,vue-loader 會解析 .vue 文件,並提取語言塊,若有必要會經過其它 loader 處理,最後將他們組裝成一個 ES Module,它的默認導出是一個 Vue.js 組件選項的對象。

若是在 <style> 中經過 lang 屬性,使用其餘 CSS 預處理語言(lesssass)等,則會匹配構建工具(webpack、vite)所配置的 loader 進行特定處理。

/*packages/compiler-sfc/sfc/stylePreprocessors.ts */
// .scss/.sass processor
const scss: StylePreprocessor = (source, map, options, load = require) => {...}
const sass: StylePreprocessor = (source, map, options, load) => {...}

// .less
const less: StylePreprocessor = (source, map, options, load = require) => {...}

// .styl
const styl: StylePreprocessor = (source, map, options, load = require) => {...}

Vue3 SFC Style 中的部分編譯主要是由 postcss 完成的,在 Vue 源碼中對應着 packages/compiler-sfc/sfc/compileStyle.ts 中的 doCompileStyle() 方法。

這裏,咱們看一下其針對 <style> 動態變量注入的編譯處理,對應的代碼(省略代碼):

export function doCompileStyle(
  options: SFCAsyncStyleCompileOptions
): SFCStyleCompileResults | Promise<SFCStyleCompileResults> {
  const {
    ...
    id,
    ...
  } = options
  ...
  const plugins = (postcssPlugins || []).slice()
  plugins.unshift(cssVarsPlugin({ id: shortId, isProd }))
  ...
}

能夠看到,在使用 postcss 編譯 <style> 以前會加入 cssVarsPlugin 插件,並給 cssVarsPlugin 傳入 shortId(即 scopedId 替換掉 data-v 內的結果)和 isProd(是否處於生產環境)。

cssVarsPlugin 則是使用了 postcss 插件提供的 Declaration 方法,來訪問 <style> 中聲明的全部 CSS 屬性的值,每次訪問經過正則來匹配 v-bind 指令的內容,而後再使用 replace() 方法將該屬性值替換爲 var(--xxxx-xx),表如今上面這個例子會是這樣:

img

cssVarsPlugin 插件的定義:

const cssVarRE = /\bv-bind\(\s*(?:'([^']+)'|"([^"]+)"|([^'"][^)]*))\s*\)/g
const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
  const { id, isProd } = opts!
  return {
    postcssPlugin: 'vue-sfc-vars',
    Declaration(decl) {
      // rewrite CSS variables
      if (cssVarRE.test(decl.value)) {
        decl.value = decl.value.replace(cssVarRE, (_, $1, $2, $3) => {
          return `var(--${genVarName(id, $1 || $2 || $3, isProd)})`
        })
      }
    }
  }
}

這裏 CSS var() 的變量名( --以後的內容)是由 genVarName() 方法生成,它會根據 isProdtruefalse 生成不一樣的值:

function genVarName(id: string, raw: string, isProd: boolean): string {
  if (isProd) {
    return hash(id + raw)
  } else {
    return `${id}-${raw.replace(/([^\w-])/g, '_')}`
  }
}

以上只是對 SFC Style 塊的相關處理的部分解讀,關於更完整的源碼解析請參考 Vue 3 的 SFC Style CSS Variable Injection 提案實現的背後

CSS 變量注入的優點

  1. 主題 - 經過響應式的全局樣式,進行主題變動。(參考 Naive UI)。
  2. 同其餘 CSS 預處理語言(Less、Sass 等)相比,免於安裝,不用配置 loader
  3. 結合響應式特性,能夠很好的模塊化,不用導出 CSS 樣式文件。

最後,想吐槽的一點是,從 Vue2 到 Vue3 的轉變是思想上的轉變,Vue 3 給了用戶太多選擇,讓用戶無所適從。

參考:

  1. 單文件組件(SFC)規範
  2. 使用CSS自定義屬性(變量)
  3. Vue 3 的 SFC Style CSS Variable Injection 提案實現的背後
相關文章
相關標籤/搜索