4
头图
CSS 变量可以跟 JavaScript 更好的通信,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 组合和封装,但它是纯静态的 — 这意味着到目前为止我们还没有能力在运行时根据组件的状态动态更新样式。

现在大多数现代浏览器都支持原生 CSS 变量,我们可以利用它轻松连接组件的状态和样式。

SFC Style 简单介绍

Vue 单文件组件 (SFC) 规范 中介绍到,.vue 文件是一个自定义的文件类型,用类 HTML 语法描述一个 Vue 组件。每个 .vue 文件包含三种类型的顶级语言块 <template><script><style>,还允许添加可选的自定义块。

Style 语言块:

  • 默认匹配:/\.css$/
  • 一个 .vue 文件可以包含多个 <style> 标签。

    <style> 标签可以有 scoped 或者 module 属性 (查看 scoped CSS 和 CSS Modules) 以帮助你将样式封装到当前组件。具有不同封装模式的多个 <style> 标签可以在同一个组件中混合使用。

  • 任何匹配 .css 文件 (或通过它的 lang 特性指定的扩展名) 的 webpack 规则都将会运用到这个 <style> 块的内容中。

vue-loader 会解析文件,提取语言块,如有必要会通过其它 loader 处理,最后将他们组装成一个 ES Module,它的默认导出是一个 Vue.js 组件选项的对象。

Stlye 模块中可以使用 lang 属性,指定 CSS 预处理语言(sass、less、stylus),如下:

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

还可以使用 src 属性,引入外部样式资源:

<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 提案实现的背后

破晓L
2.1k 声望3.6k 粉丝

智慧之子 总以智慧为是