在 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