scoped

Vue scoped通过在DOM结构以及CSS样式上增加唯一不重复的标记data-v-hash的方式,确保样式的唯一性,实现样式私有化模块化的效果。

这个工作通过PostCSS转译实现。

scoped的三条渲染规则:

  • 给HTML的DOM节点加一个不重复的data属性(形如data-v-114514)以表示唯一性。
  • 在每句CSS选择器末尾(编译后生成的CSS语句)加一个当前组件的data属性选择器(如[data-v-114514])来私有化样式。
  • 如果组件内部包含其他组件,只会给其他组件的最外层标签加上当前组件的data属性

增加了data属性的DOM

增加了属性选择器的CSS

样式穿透

主要用于修改常用Vue组件库(例如ElementVantAnt Design等)的默认自带样式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<main>
<el-input placeholder="测试" class="ipt"></el-input>
</main>
</template>

<style scoped lang="scss">
.ipt {
width: 300px;
margin: 100px 400px;
// 这样做因为scoped的机制,是无法应用样式的
.el-input__inner {
background-color: green;
}
:deep(.el-input__inner) {
background-color: red;
}
}
</style>

属性选择器被放到.ipt后面而非.el-input__inner后面

源码

在Vue源码(/package/compiler-sfc/src/compileStyle.ts)中可以看到scoped和样式穿透的源码。

compiler-sfc用于处理.Vue单文件组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function doCompileStyle(
options: SFCAsyncStyleCompileOptions
): SFCStyleCompileResults | Promise<SFCStyleCompileResults> {

// ......省略

// PostCSS插件
const plugins = (postcssPlugins || []).slice()
plugins.unshift(cssVarsPlugin({ id: shortId, isProd }))
if (trim) {
plugins.push(trimPlugin())
}
// 如果scoped = true就向postCss添加一个插件
if (scoped) {
plugins.push(scopedPlugin(longId))
}

// ......省略
}

/package/compiler-sfc/src/style/pluginScoped.ts中可以看到对PostCSS的使用。

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// PostCSS插件
// PostCSS接收一个CSS文件并提供一个API来分析,修改它的规则(通过把CSS规则转换成一个抽象语法树的形式)
const scopedPlugin: PluginCreator<string> = (id = '') => {
const keyframes = Object.create(null)
const shortId = id.replace(/^data-v-/, '')

return {
// 定义PostCSS插件名称
postcssPlugin: 'vue-sfc-scoped',
// 处理CSS的AST
Rule(rule) {
processRule(id, rule)
},
// 处理@相关的CSS 例如media keyframes
AtRule(node) {
if (
/-?keyframes$/.test(node.name) &&
!node.params.endsWith(`-${shortId}`)
) {
// register keyframes
keyframes[node.params] = node.params = node.params + '-' + shortId
}
},
// 最后执行而且只处理一次
OnceExit(root) {
if (Object.keys(keyframes).length) {
// If keyframes are found in this <style>, find and rewrite animation names
// in declarations.
// Caveat: this only works for keyframes and animation rules in the same
// <style> element.
// individual animation-name declaration
root.walkDecls(decl => {
if (animationNameRE.test(decl.prop)) {
decl.value = decl.value
.split(',')
.map(v => keyframes[v.trim()] || v.trim())
.join(',')
}
// shorthand
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(',')
}
})
}
}
}
}

// 做缓存,如果已经有这个rule(一个AST)就不操作了
const processedRules = new WeakSet<Rule>()

function processRule(id: string, rule: Rule) {
if (
processedRules.has(rule) ||
(rule.parent &&
rule.parent.type === 'atrule' &&
/-?keyframes$/.test((rule.parent as AtRule).name))
) {
return
}
processedRules.add(rule)
// 遍历AST节点
rule.selector = selectorParser(selectorRoot => {
selectorRoot.each(selector => {
rewriteSelector(id, selector, selectorRoot)
})
}).processSync(rule.selector)
}