Vue3 cssVars 🔗
“version”: “3.2.37”
vue3
中单文件SFC有个新特性,就是在css里可以使用变量了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <template>
<div>
<h1>123</h1>
</div>
</template>
<script lang='ts' setup>
const color = 'red'
</script>
<style scoped>
h1 {
color: v-bind(color)
}
</style>
|
具体的代码就是使用v-bind去绑定变量值,这里预览的结果就是h1
会显示出红色
查看h1
的标签,可以看到
color使用的是 css自带的变量var
语法进行绑定的
并且把变量值绑定到父元素上,通过js写入到父元素的行内样式里
css变量绑定分为2个阶段
- 编译阶段,把变量转换成css
var
变量语法机制 - 运行时阶段,js动态改变父元素上的变量绑定
doCompileStyle 🔗
这个函数是compileStyle
的主函数,其主要作用就是把SFC中的css部分编译识别出来
其中也有很多处理代码,最终也是使用postcss去处理
1
2
3
4
5
6
7
8
| // .....
const shortId = id.replace(/^data-v-/, '')
const longId = `data-v-${shortId}`
const plugins = (postcssPlugins || []).slice()
plugins.unshift(cssVarsPlugin({ id: shortId, isProd }))
// ....
|
可以看到其中最第一位的就是在plugins的首位插入了一个插件,就是cssVarsPlugin
cssVarsPlugin 🔗
插件中通过正则匹配v-bind
在字符串中的位置进行匹配替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
const vBindRE = /v-bind\s*\(/g
//....
export const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
const { id, isProd } = opts!
return {
postcssPlugin: 'vue-sfc-vars',
Declaration(decl) {
// rewrite CSS variables
const value = decl.value
if (vBindRE.test(value)) {
// 重置匹配位置到首位
vBindRE.lastIndex = 0
// 匹配后的字符
let transformed = ''
let lastIndex = 0
let match
while ((match = vBindRE.exec(value))) {
// 匹配字符的开始位置
const start = match.index + match[0].length
const end = lexBinding(value, start)
if (end !== null) {
const variable = normalizeExpression(value.slice(start, end))
transformed +=
value.slice(lastIndex, match.index) +
`var(--${genVarName(id, variable, isProd)})`
lastIndex = end + 1
}
}
decl.value = transformed + value.slice(lastIndex)
}
}
}
}
cssVarsPlugin.postcss = true
|
最终会在transformed进行字符串拼接, 并且生成一个var(--${genVarName(id, variable, isProd)})
字符串,其中genVarname
会根据当前组件的id进行生成,生产环境就是用随机字符串生成的
lastIndex会在拼接后向后移动1位
最后css转换后的值,v-bind就被替换成css的var()
语法
lexBinding 🔗
lexBinding函数要结合上下文来看,v-bind的括号中变量是有多重形式的
其中可能会有括号
1
2
| font-weight: v-bind("count.toString(");
font-weight: v-bind(xxx);
|
官方代码中就是使用for循环,结合switch\case
, 最终找到最后一个)
,返回索引值,也就是end值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| function lexBinding(content: string, start: number): number | null {
let state: LexerState = LexerState.inParens
let parenDepth = 0
for (let i = start; i < content.length; i++) {
const char = content.charAt(i)
switch (state) {
case LexerState.inParens:
if (char === `'`) {
state = LexerState.inSingleQuoteString
} else if (char === `"`) {
state = LexerState.inDoubleQuoteString
} else if (char === `(`) {
parenDepth++
} else if (char === `)`) {
if (parenDepth > 0) {
parenDepth--
} else {
return i
}
}
break
case LexerState.inSingleQuoteString:
if (char === `'`) {
state = LexerState.inParens
}
break
case LexerState.inDoubleQuoteString:
if (char === `"`) {
state = LexerState.inParens
}
break
}
}
return null
}
|
normalizeExpression 🔗
传入的是context截取的从开始到结束的字符串
如果是'/"
,返回去除的字符串
1
2
3
4
5
6
7
8
9
10
| function normalizeExpression(exp: string) {
exp = exp.trim()
if (
(exp[0] === `'` && exp[exp.length - 1] === `'`) ||
(exp[0] === `"` && exp[exp.length - 1] === `"`)
) {
return exp.slice(1, -1)
}
return exp
}
|
genCssVarsCode 🔗
编译后要去动态响应值的变化,就必然要通过js去控制css的值,这里通过genCssVarsCode
函数去生存响应的代码
- vars 通过编译后搜集的变量名
- bindings 当前组件中script暴露的变量
- id 当前组件的id
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| export function genCssVarsCode(
vars: string[],
bindings: BindingMetadata,
id: string,
isProd: boolean
) {
// 根据当前组件css搜集的变量名,生成以逗号分隔的字符串,最后用大括号包围
const varsExp = genCssVarsFromList(vars, id, isProd)
// 组装成一个对象,其中content就是之前生成的字符串
const exp = createSimpleExpression(varsExp, false)
// ....
const transformed = processExpression(exp, context)
// ... 这里是伪代码, 中间还有其他判断
const transformedString = transformed.content
// 最终会生成一个字符串
return `_${CSS_VARS_HELPER}(_ctx => (${transformedString}))`
}
|
我们把中间的变量替换过来,就是
_useCssVars(_ctx => ({
// ….. 中间就是css的动态变量的代码
}))
useCssVars 🔗
watchPostEffect
是在组件更新之后调用
同时在onMounted
钩子函数内,使用MutationObserver
监听父元素下的子元素变化,只要子元素发生变化,都会调用setVars
函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| export function useCssVars(getter: (ctx: any) => Record<string, string>) {
if (!__BROWSER__ && !__TEST__) return
const instance = getCurrentInstance()
/* istanbul ignore next */
if (!instance) {
__DEV__ &&
warn(`useCssVars is called without current active component instance.`)
return
}
const setVars = () =>
setVarsOnVNode(instance.subTree, getter(instance.proxy!))
watchPostEffect(setVars)
onMounted(() => {
const ob = new MutationObserver(setVars)
ob.observe(instance.subTree.el!.parentNode, { childList: true })
onUnmounted(() => ob.disconnect())
})
}
|
setVarsOnVNode 🔗
根据vode向上递归,找到非组件的父元素,要知道useCssVars是在运行时执行的,所以就是你写的html 元素节点
最后就是把属性写入到父元素的style属性内
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| function setVarsOnVNode(vnode: VNode, vars: Record<string, string>) {
if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
const suspense = vnode.suspense!
vnode = suspense.activeBranch!
if (suspense.pendingBranch && !suspense.isHydrating) {
suspense.effects.push(() => {
setVarsOnVNode(suspense.activeBranch!, vars)
})
}
}
// drill down HOCs until it's a non-component vnode
// vnode 向上递归,直到vnode不存在component属性
while (vnode.component) {
vnode = vnode.component.subTree
}
// 设定属性值
if (vnode.shapeFlag & ShapeFlags.ELEMENT && vnode.el) {
setVarsOnNode(vnode.el as Node, vars)
} else if (vnode.type === Fragment) {
// 如果是Fragment,递归调用
;(vnode.children as VNode[]).forEach(c => setVarsOnVNode(c, vars))
} else if (vnode.type === Static) {
// 静态节点
let { el, anchor } = vnode
while (el) {
setVarsOnNode(el as Node, vars)
if (el === anchor) break
el = el.nextSibling
}
}
}
function setVarsOnNode(el: Node, vars: Record<string, string>) {
// 根据nodeType, 如果node是元素属性
if (el.nodeType === 1) {
const style = (el as HTMLElement).style
// 使用setProperty
for (const key in vars) {
style.setProperty(`--${key}`, vars[key])
}
}
}
|