深入style的scoped
# 概述
scoped的作用就是样式模块化(CSS Module),即给组件每一个元素(以及非动态添加的子组件的根元素)加上一个data-v-xxxx的属性,样式选择器也会格式化成选择器[data-v-xxxx],这样就做到了样式隔离,每个组件内定义的样式只对该组件生效,避免了不同组件或页面的样式(选择器)冲突。本文将以vue3为例,深入了解scoped原理。
# scoped实践
vue3组件是如下定义样式:
<template>
<div class="header">
<span>标绘管理</span>
</div>
</template>
<style scoped>
.header span {
position: relative;
margin-left: 54px;
font-size: 16px;
display: inline-flex;
height: 24px;
line-height: 24px;
font-family: "Microsoft YaHei";
font-weight: bold;
color: #303133;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 效果如下:
# scoped源码分析
# compiler-sfc模块
vue3中有个模块@vue/compiler-sfc,这个模块是单独拎出来,不会被打包到vue.global.js。compiler-sfc主要作用就是用来编译单文件组件,就是.vue。因为scoped的实现是在compiler-sfc模块中,所以本文的所有的讨论也是基于SFC。
# vite与plugin-vue
vue3如果是通过vite搭建的,那么compiler-sfc会通过vite的plugin-vue调用,这在script 标签的 setup 实现原理 (opens new window)中有讲解,可以简短回顾。
# Style 样式选择器中的处理
在 plugin-vue中会读取.vue组件,并识别<style></style>部分,如下
if (query.type === "style") {
return transformStyle(
code,
descriptor,
Number(query.index || 0),
options.value,
this,
filename
);
}
2
3
4
5
6
7
8
9
10
transformStyle函数会将<style></style>的 code 传给compiler.compileStyleAsync,并返回编译的结果,其中参数包含id和scoped。
id的生成id包含于descriptor中,其生成过程如下所示:
descriptor.id = getHash(normalizedPath + (isProduction ? source : "")); // normalizedPath:序列化文件路径后的字符串,如果是生产环境,还会加上源码
function getHash(text) {
return node_crypto
.createHash("sha256")
.update(text)
.digest("hex")
.substring(0, 8);
}
2
3
4
5
6
7
8
9
# compileStyleAsync
compileStyleAsync会返回一个函数doCompileStyle,该函数会加载一些css插件对样式进行编译。
上面图中的data-v-xxxx就是在这个函数中根据参数id先用正则匹配前缀data-v-替换再加上data-v-。
如果参数scoped为true,就会执行plugins.push(scopedPlugin(longId)),plugins是一个数组,后面调用post-css库对这些plugin进行处理。如下:
postcss(plugins).process(source, postCSSOptions); // source:源代码,
而scoped的核心实现就是在scopedPlugin中。
# scopedPlugin
scopedPlugin是vue3封装的post-css插件,其实现如下:
const scopedPlugin = (id = "") => {
const keyframes = /* @__PURE__ */ Object.create(null);
const shortId = id.replace(/^data-v-/, "");
return {
postcssPlugin: "vue-sfc-scoped",
Rule(rule) {
processRule(id, rule);
},
AtRule(node) {
if (
/-?keyframes$/.test(node.name) &&
!node.params.endsWith(`-${shortId}`)
) {
keyframes[node.params] = node.params = node.params + "-" + shortId;
}
},
OnceExit(root) {
if (Object.keys(keyframes).length) {
root.walkDecls((decl) => {
if (animationNameRE.test(decl.prop)) {
decl.value = decl.value
.split(",")
.map((v) => keyframes[v.trim()] || v.trim())
.join(",");
}
if (animationRE.test(decl.prop)) {
decl.value = decl.value
.split(",")
.map((v) => {
const vals = v.trim().split(/\s+/);
const i = vals.findIndex((val) => keyframes[val]);
if (i !== -1) {
vals.splice(i, 1, keyframes[vals[i]]);
return vals.join(" ");
} else {
return v;
}
})
.join(",");
}
});
}
},
};
};
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
45
scopedPlugin就是一个对象,包含三个方法:Rule、AtRule和OnceExit,这是post-css插件的里面的概念。
Rule:表示CSS里的普通规则,比如选择器和申明。AtRule:表示CSS中的@规则OnceExit:用于在整个CSS文件的解析完成后执行一次操作
# processRule
processRule主要就是处理一般规则。定义了一个WeakSet用于避免重复操作,过滤@规则还有keyframes。
下面这段代码就是遍历了选择器中的每个部分,并通过 rewriteSelector 函数进行修改,最后将修改后的选择器转换回字符串并赋值给 rule.selector
const processedRules = /* @__PURE__ */ new WeakSet();
function processRule(id, rule) {
if (
processedRules.has(rule) ||
(rule.parent &&
rule.parent.type === "atrule" &&
/-?keyframes$/.test(rule.parent.name))
) {
return;
}
processedRules.add(rule);
rule.selector = selectorParser$2((selectorRoot) => {
selectorRoot.each((selector) => {
rewriteSelector(id, selector, selectorRoot);
});
}).processSync(rule.selector); // selectorParser$2就是`postcss-selector-parser`插件
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# rewriteSelector
rewriteSelector顾名思义就是重写选择器selector,scoped的data-v-xxxx是只加在选择器的最后一个,作为它的属性。而且还要考虑一些伪类选择器::after、::before等等,最后就是插入属性data-v-xxxx,操作如下
selector.insertAfter(
node as any,
selectorParser.attribute({
attribute: idToAdd,
value: idToAdd,
raws: {},
quoteMark: `"`,
}),
)`
2
3
4
5
6
7
8
9
# template 元素处理
scoped在元素中的处理其实就是给元素加一个属性,同一个.vue中的元素data-v-xxxx是一样的,和style的属性选择器data-v-xxxx也是一致。
在plugin-vue中就生成了id,这个id不仅会给style用,元素也是用的这个相同的id。
对于template和style部分是调用不同的解析器进行解析的。元素的属性是在@vue/compiler-dom、@vue/compiler-core这两个模块中属性,本质上就是解析语法树,生成 DOM 节点时判断scopeId是否为true
if (context.scopeId) {
res += ` ${context.scopeId}`;
}
2
3