手机版 欢迎访问it开发者社区(www.mfbz.cn)网站

当前位置: > 开发

Vue SFC CSS 变量注入提案 (SFC CSS variable injection)

时间:2021/6/8 18:06:46|来源:|点击: 次

在 Vue 中想要实现动态样式绑定,基本只能通过设置行内样式来实现:

<template>
	<div class="header" :style="{'color': color, 'font-size': size}">测试内容</div>
</template>

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

在属性前面加 v-bind 或者 : ,在引号里面就是 JS 表达式,因此样式绑定实际上是接收了一个 JS 对象。由于对象的 key 不能包含 - ,所以 font-size 可以写成 'font-size' ,或者驼峰法 fontSize

使用这个方法,绝大多数的需求基本上都没问题。但是绑定太多行内样式,会对代码的维护性造成一定影响。此外,如果像动态覆盖组件库默认的样式,就不能用这个方法。

之前遇到过一个场景,当时用了 ElementUI 的弹框组件,弹框是一个按步骤填写的表单,由于中间有个步骤表单元素有点多,导致出现了滚动条,交互希望在那一步可以让弹框的高度增加,其他步骤使用默认高度即可。

之前在覆盖 ElementUI 组件默认样式的时候,样式全部都是定义在 <style> 里面,一般都是浏览器审查元素找到对应的类名,然后在 <style> 里面通过 ::v-deep 进行样式穿透从而实现覆盖组件库默认样式。由于样式是定义在 <style> 里面的,因此动态绑定样式就不能用了。当时的解决方案是在 <style> 里面定义多个类,每个类对应不同的样式,然后通过 Vue 动态绑定类名,从而实现样式的切换。

SFC CSS 变量注入

那么在 Vue 中有一个提案 SFC CSS 变量注入 (SFC CSS variable injection) ,现在 Vue 3 中已经支持了。其中 SFC 是指 单文件组件 (Single File Component) 。

使用的方法非常简单,在组件的 <script> 中声明一个响应式变量,然后在 CSS 中通过 v-bind 来使用这个变量:

<template>
	<div class="header">测试内容</div>
</template>

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

<style lang="less" scoped>
.header {
	color: v-bind(color);
}
</style>

CSS 变量注入也适用于更复杂的数据结构,如果需要传递 JS 表达式,可以用引号包裹一下:

<template>
	<div class="header">测试内容</div>
</template>

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

<style lang="less" scoped>
.header {
	color: v-bind(color);
	font-size: v-bind('font.size');
}
</style>

有没有感觉非常方便,在 CSS 和一些预编译器中有提供变量的用法,但不是响应式的。使用 v-bind 直接将响应式依赖和样式联系在一起了,有了这个之后,就基本不需要写行内样式了。只不过本人在 Vue 3 项目中测试的时候,控制台打印了警告信息,本人的 @vue/compiler-sfc 版本是 3.0.4
在这里插入图片描述

实现原理

CSS 变量注入是通过 CSS 变量实现的。首先在 SFC 编译的时候, <style> 中的 v-bind 会被编译掉,然后向 SFC 中注入一段代码。然后注入的代码在浏览器环境下执行生成 CSS 变量。

v-bind 的解析通过正则表达式实现:

const cssVarRE = /\bv-bind\(\s*(?:'([^']+)'|"([^"]+)"|([^'"][^)]*))\s*\)/g;

代码注入的实现:

function genNormalScriptCssVarsCode(cssVars, bindings, id, isProd) {
    return (`\nimport { ${CSS_VARS_HELPER} as _${CSS_VARS_HELPER} } from 'vue'\n` +
        `const __injectCSSVars__ = () => {\n${genCssVarsCode(cssVars, bindings, id, isProd)}}\n` +
        `const __setup__ = __default__.setup\n` +
        `__default__.setup = __setup__\n` +
        `  ? (props, ctx) => { __injectCSSVars__();return __setup__(props, ctx) }\n` +
        `  : __injectCSSVars__\n`);
}

编译的完整实现可以参考:

https://github.com/vuejs/vue-next/blob/master/packages/compiler-sfc/src/cssVars.ts

本人的项目是用 Vite 构建的,通过浏览器调试工具可以直接看到经过编译的 ESM 模块。我们看到,使用了 CSS 变量注入语法,在 SFC 编译之后可以看到被注入代码:
在这里插入图片描述
编译之后 CSS 变量被解析成了一个对象,然后传给了 useCssVars 函数。在 useCssVars 函数中,绑定了一个 onMounted 钩子,通过 watchEffect 去调用 setVars ,从而实现将 CSS 变量添加到组件树上。我们看到 watchEffect 后面传了一个 options 对象,其中 flush 设为 post ,意思就是在组件更新后触发,这样就可以访问更新后的 DOM 。

function useCssVars(getter) {
    const instance = getCurrentInstance();
    /* istanbul ignore next */
    if (!instance) {
        (process.env.NODE_ENV !== 'production') &&
            warn(`useCssVars is called without current active component instance.`);
        return;
    }
    const setVars = () => setVarsOnVNode(instance.subTree, getter(instance.proxy));
    onMounted(() => watchEffect(setVars, { flush: 'post' }));
    onUpdated(setVars);
}

完整实现可以参考:

https://github.com/vuejs/vue-next/blob/master/packages/runtime-dom/src/helpers/useCssVars.ts

最终渲染出来的 DOM 节点,我们可以看到添加了行内的 CSS 变量:
在这里插入图片描述

需要注意的问题

1. CSS 变量注入在子组件中不能用。由于子组件获取不到父组件的响应式变量,因此子组件不能用

2. 由于 CSS 变量注入是通过 CSS 变量实现的,因此使用前应该检查兼容性
在这里插入图片描述

参考:
Vue 3.0.3 : 新增CSS变量注入以及最新的Ref提案
Using CSS custom properties (variables) - MDN

Copyright © 2002-2019 某某自媒体运营 版权所有