背景:经常在UI框架进行样式穿透的时候,会发生不生效的情况,既会是因为选择器优先级问题或者穿透写法问题,这种比较好理解,提升优先级调整写法就好,但在vue中有时候需要将scoped去掉才能生效,有时候又不能去掉scoped,有时候因为写法的问题导致不生效【需要用一个大盒子包裹】,整理下具体原因,以后遇到其他场景再逐步完善。
核心遵旨
- 去掉scoped属性样式就变成全局样式了
- 添加:deep()是把其属性选择器放在最前面
- 第三方UI组件只会为根元素添加 data-v-xxx 属性
- 包一层大盒子实现属性嵌套
1. scoped的作用以及原理
作用:避免样式污染,不加scoped,书写的样式作用于全局,加了的话样式仅针对当前组件生效
原理:每个配置了scoped的组件分配一个唯一哈希,通过css属性选择器实现域划分
案例
// App.vue
<template>
<RouterView></RouterView>
</template>
<style scoped>
</style>
// layout.vue
<template>
<div class='AdminPage'>
<div class='app-header-box'>
<AppHeader/>
</div>
<div class='app-content-box'></div>
</div>
</template>
<style scoped>
.AdminPage {
position: relative;
width: 100%;
background-color: #F3F2F2;
}
.AdminPage .app-header-box{
position: fixed;
width: 100%;
height: 48px;
z-index: 100;
}
.AdminPage .app-content-box{}
</style>
添加了scoped的组件,最终渲染效果:
- 每个添加了scoped的组件渲染出来都会附带一个唯一的属性data-v-xxx;
- 一个组件中的所有标签都会带上同样的data-v-xxx属性;
- 子组件会带上父组件的data-v-xxx属性;
- 在使用第三方的 UI 库时,只会为根元素添加 data-v-xxx 属性,子元素中则不会添加
2. deep()做了什么?
简介:可以在 style 标签中使用 :deep() 的方法进行样式穿透,主要是解决在使用第三方的 UI 库(如 element-plus 等)时导致的对其样式设置不生效的问题。
例子:
<template>
<div class="main">
<el-input class="ipt"></el-input>
</div>
</template>
<script setup></script>
<style scoped>
.ipt {
width: 300px;
}
</style>
当使用以下方法修改样式时并不能生效
.ipt .el-input_wrapper {
background-color: red;
}
出现这种结果的原因就在于 Vue 将 [data-v-7a7a37b1] 属性添加到 .el-input 之后, 而 .el-input__wrapper 的标签上并不存在 [data-v-7a7a37b1] 属性。那么 deep 样式穿透随之而来。
2.1 deep样式穿透
:deep(.ipt .el-input__wrapper) {
background-color: red;
}
:deep() 函数会把属性选择器放在最前面,那么就可以捕获到啦!
3. 源码解析
目录:core-main/packages/compiler-sfc/src/compileStyle.ts
export function doCompileStyle(
scoped = false,
): SFCStyleCompileResults | Promise<SFCStyleCompileResults> {
......
if (scoped) {
plugins.push(scopedPlugin(longId))
}
......
}
在这个函数中,如果存在 scoped 属性,就会调用 postcss 这个插件,这个插件的主要作用就是把 CSS 转换成抽象语法树 (AST) 便于之后的操作。
function processRule(id: string, rule: Rule) {
......
rule.selector = selectorParser(selectorRoot => {
selectorRoot.each(selector => {
rewriteSelector(id, selector, selectorRoot)
})
}).processSync(rule.selector)
}
之后在 processRule 函数中调用 rewriteSelector() 方法对 CSS 选择器进行重写。
function rewriteSelector(
id: string,
selector: selectorParser.Selector,
selectorRoot: selectorParser.Root,
slotted = false,
) {
let node: selectorParser.Node | null = null
let shouldInject = true
// find the last child node to insert attribute selector
selector.each(n => {
......
if (n.type === 'pseudo') {
const { value } = n
// deep: inject [id] attribute at the node before the ::v-deep
// combinator.
if (value === ':deep' || value === '::v-deep') {
if (n.nodes.length) {
// .foo ::v-deep(.bar) -> [xxxxxxx] .foo .bar
// replace the current node with ::v-deep's inner selector
let last: selectorParser.Selector['nodes'][0] = n
n.nodes[0].each(ss => {
selector.insertAfter(last, ss)
last = ss
})
// insert a space combinator before if it doesn't already have one
const prev = selector.at(selector.index(n) - 1)
if (!prev || !isSpaceCombinator(prev)) {
selector.insertAfter(
n,
selectorParser.combinator({
value: ' ',
}),
)
}
selector.removeChild(n)
} else {
......
}
......
}
当遇到 :deep 时,会将原来的属性选择器添加到前面元素中,即:.foo ::v-deep(.bar) -> [xxxxxxx] .foo .bar 通过这种方法就能定位到第三方 UI库中的 CSS 选择器了。
当使用一些第三方的 UI 库时,由于 Vue3 实现了模块化封装,那么在设置 UI 库的 CSS 样式时有时会出现设置不成功的问题,那么这个时候可以考虑使用 :deep() 来进行样式穿透。
4. 为什么有时候穿透需要多包裹一层大盒子
案例:father.vue嵌套子组件son.vue
// father.vue
<template>
<div>father</div>
<Son />
</template>
<script setup>
import Son from "./Son.vue"
</script>
<style lang="less" scoped>
div {
color: red;
}
:deep(.second-row) {
color: yellow;
}
</style>
// son.vue
<template>
<div>111</div>
<div class="second-row">222</div>
<div>333</div>
</template>
<script></script>
<style scoped>
.second-row {
color: green
}
</style>
按照上述书写,穿透样式并未生效,仅子组件样式生效
这是因为并没有生成嵌套关系,父组件中由于添加scoped注册的样式穿透是针对date-v-father这一前置条件下的,也就是:
[data-v-7a7a37b1] .second-row {
color: blue;
}
但是,目前的dom结构下data-v-7a7a37b1下面并没有second-row的类名
所以需要调整,父组件使用一个大盒子包裹起来,完成嵌套,即:
// father.vue
<template>
<div>
<div>father</div>
<Son />
</div>
</template>
<script setup>
import Son from "./Son.vue"
</script>
<style lang="less" scoped>
div{
color: red;
}
:deep(.second-row){
color: blue;
}
</style>
5. 样式穿透的一些写法
写法有:::v-deep,>>>,:deep(),/deep/
具体区分:
如果你使用的是css,没有使用css预处理器,则可以使用>>>,/deep/,::v-deep。
如果用的less,node-sass,可以使用/deep/,::v-deep。
如果用的dart-sass,那么就不能用/deep/,使用::v-deep。
如果使用vue2.7以上版本以及vue3,::v-deep可以使用但是会有警告。