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 提案实现的背后
相关文章
相关标签/搜索