前言
作为前端开发,在日常工作中总是避免不了与样式打交道。为了避免样式的互相影响,我们在vue的style标签往往会添加scoped进行样式隔离。但是加上scoped究竟做了什么呢?为什么有时候加上scoped后,我修改的样式不起作用了呢?下面我将对上述这两个问题进行说明
scoped做了什么
我们都知道scoped的作用是防止组件之间样式互相影响,起到隔离的作用。
我们来回想一下,在以前开发中没有scoped是如何避免样式冲突的。
举个简单的例子,我想分别修改页面中头部和尾部div的字体颜色。我们通常会给两个div分别起一个类名,来避免这两个div的样式互相影响。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
<!-- 类名为header的div标签字体颜色是红色 -->
div.header {
color: red;
}
<!-- 类名为header的div标签字体颜色是粉色 -->
div.footer {
color: pink;
}
</style>
</head>
<body>
<div class="header">头部</div>
<div class="footer">尾部</div>
</body>
</html>
scoped的做法其实和上面的例子类似,只不过它不是加类名选择器,而是属性选择器。
我们来写一个简单的例子,我例采用vue3举例。vue3和vue2的表现是一样的
index.vue
<script setup>
import Component from './component.vue'
</script>
<template>
<div class="div1">
div1
<div class="div2">
div2
<Component />
</div>
</div>
</template>
<style scoped>
.div1 {
}
.div1 .div2 {
}
.div1 .div2 .div3 {
}
.div1 .div2 .div3 .div4 {
}
</style>
component.vue
<script setup>
</script>
<template>
<div class="div3">
div3
<div class="div4">
div4
</div>
</div>
</template>
<style scoped>
</style>
上面的例子是一个非常简单的div嵌套关系,我们写了类选择器但并没有赋予样式,是为了触发scoped让其生效。如果没有在style里面定义这些类选择器,出于性能考虑scoped是不会生效的。
运行代码,我们来看下html有什么变化
我们可以很直观的看到,在div标签上被添加了一些自定义属性 data-v-7c55d5f5。div3和div4都是子组件component的元素,div3被加上了自定义属性,div4为什么没有被加上自定义属性,在下面会提到。
vue就是根据这些添加的自定义属性来进行样式隔离的,我们可以打开控制台查看header便签内style的情况
不难发现,我们在style标签内编写的样式都被添加上了属性选择器。属性选择器的构成是data-v-唯一的随机数,这也就代表我们在另一个组件中如果也使用了scoped,并且同样有一个类名为div1的便签。它会生成另一个唯一的属性选择器,我们打个比方叫data-v-1c412v9h。
经过vue处理后在style中的表现形式为:
<style>
.div1[data-v-7c55d5f5]{
color:red;
}
.div[data-v-1c412v9h]{
color:pink;
}
</style>
由于属性选择器的值不同,这两个div1的样式自然就不会冲突,就起到了样式隔离的效果。
scoped导致样式失效
在日常开发中,我们偶尔会遇到设置的样式没有生效,这有很大的可能性和我们添加的scoped有关。我们再来看下面这个例子,我们在上面例子的基础上给空白的类名加上样式。
index.vue
<script setup>
import Component from './component.vue'
</script>
<template>
<div class="div1">
div1
<div class="div2">
div2
<Component />
</div>
</div>
</template>
<style scoped>
.div1 {
color: red;
}
.div1 .div2 {
color: blue;
}
.div1 .div2 .div3 {
color: green;
}
.div1 .div2 .div3 .div4 {
color: yellow;
}
</style>
子组件的代码在style中添加空白的类
<script setup>
</script>
<template>
<div class="div3">
div3
<div class="div4">
div4
</div>
</div>
</template>
<style scoped>
.div3 {}
.div4 {}
</style>
我们希望div1的字体颜色是红色,div2的字体颜色是蓝色,div3的字体颜色是绿色,div4的字体颜色是黄色。运行代码查看页面,神奇的现象发生了。div3和div4都是子组件,div3的样式生效了,但是div4并没有变成黄色,而是继承了div3父元素的字体颜色变成了绿色。
这到底是怎么回事呢?我们去掉index.vue中的scoped标签试试,一切正常。看来问题就出在scoped身上。
我们加上scoped之后,打开控制台观察一下
html
style
由于在父组件和子组件的style标签中都添加了scoped
属性,所以在html中有两个值不同的自定义属性。在父组件index.vue中生成的自定义属性是data-v-7c55d5f5
,子组件中生成的自定义属性是data-v-11761b0b
。
发现问题了,在html中scoped并没有给div4
添加data-v-7c55d5f5
这个自定义属性,而在style中样式却设置了data-v-7c55d5f5
自定义属性,在页面中没有类名是div4
并且自定义属性是data-v-7c55d5f5
的元素,这个样式自然也就没有生效。
据观察可得scoped添加属性选择器有几个特点
- 没有子组件的情况,它会给每个类的标签上都加上
data-v-xxxxx
- 有子组件的情况,遵循上一条的同时,给子组件的最外层加上
data-v-xxxxxx
。具体表现是div3
和div4
是子组件的元素,只有div3
被添加上了data-v-7c55d5f5
这个属性,而div4则没有。 - 在那个组件声明的样式默认会被添加上该组件生成的自定义属性。表现为div4是在index.vue中设置的样式,它的自定义属性被设置成了
data-v-7c55d5f5
怎么解决样式失效
样式失效的主要原因就是,在父组件中添加的样式会被自动添加上父组件的自定义属性,而子组件中只有最外层会被添加上父组件的自定义属性,其余子孙元素都是子元素生成的自定义属性。找不到对应的选择器的元素,导致样式失效。
解决这个问题的办法有很多
- 要设置子组件的样式,就在子组件中设置。不在父组件中设置,我们对例子中子组件div4的样式进行设置
<script setup>
</script>
<template>
<div class="div3">
div3
<div class="div4">
div4
</div>
</div>
</template>
<style scoped>
.div3 {
}
.div3 .div4 {
color: aquamarine;
}
</style>
此时它会格外的生成一个style标签,这个div4的自定义属性就是子组件的scoped生成的值了,理所当然样式也就生效了。
所以尽量不要在父元素中设置子组件的样式,以免出现样式失效的问题。
- 去掉scoped标签,最简单粗暴的方式。出问题的源头就是因为自动添加的自定义属性有问题,直接解决出问题的人,当然这种做法是不妥当的,因为我们还需要它进行样式隔离。
- 另写一个style标签,这种做法的原理和去掉scoped一样。算是一种较为妥当的方式,即解决了问题又没有破坏样式隔离的特性。需要注意的是额外写的那个标签可能会影响到其他同类名的样式,最好类名唯一一点。
<style scoped>
.div1 {
color: red;
}
.div1 .div2 {
color: blue;
}
.div1 .div2 .div3 {
color: green;
}
.div1 .div2 .div3 .div4 {
color: yellow;
}
</style>
<style>
.div1 .div2 .div3 .div4 {
color: aquamarine;
}
</style>
- 推荐 使用在子孙元素的类名前添加:deep()伪类,具有这个伪类的元素代表这个元素是我的子节点或者孙子节点,你不要加属性选择器了。
<style scoped>
.div1 {
color: red;
}
.div1 .div2 {
color: blue;
}
.div1 .div2 .div3 {
color: green;
}
.div1 .div2 .div3 :deep(.div4) {
color: yellow;
}
</style>
我们可以看到div4元素的属性选择器已经去掉了,因为div3是子组件的最外层有父组件的自定义元素,所以这个选择器可以找到对应的元素,样式也就生效了。
开发中的场景
介绍完解决方法,我们可以想一下在实际的开发过程中,有哪些场景是需要在父组件去修改子组件样式的。
最常见的那当然是修改element组件库的默认样式了,用::v-deep或者:deep()修改element组件库的样式是非常常见的做法,下面我们就来看一下当我们设置上scoped后element组件有什么变化。
el-button
<script setup>
</script>
<template>
<div class="div1">
<el-button>按钮</el-button>
</div>
</template>
<style scoped>
.div1 {}
</style>
可以看到el-button组件内的最外层是button标签被加上了自定义属性,而子标签span则没有。这符合我们上面发现的规律,子组件只有在外层会被加上父元素的自定义属性。
这也就意味着当我们要修改el-button的样式的时候,甚至都不需要使用:deep()伪类,直接修改.el-button即可。因为它是最外层的元素有父元素的自定义属性。
当然如果要修改的是button的子元素span还是需要使用:deep(),或者使用上述几种解决方法的。
<script setup>
</script>
<template>
<div class="div1">
<el-button>按钮</el-button>
</div>
</template>
<style scoped>
.div1 {}
.div1 .el-button{
width: 400px;
background-color: aqua;
}
</style>
看生效了,我之前一直以为只要修改element组件的样式都需要加:deep(),看来加不加:deep()和我们要修改的类名所在的位置有关,如果是最外层的元素直接修改就可以生效。
el-dialog
别的element组件都和el-button的方法类似,但是el-dialog这一类组件有些特殊,它可能是挂载到body上面的,也就意味着它不是当前组件的子元素。
<script setup>
import { ref } from 'vue'
const dialogVisible = ref(false)
</script>
<template>
<div class="div1">
<el-button @click="dialogVisible = true">按钮</el-button>
<el-dialog class="dialog1" v-model="dialogVisible" :append-to-body="true" title="Tips" width="30%">
<span>This is a message</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="dialogVisible = false">
Confirm
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.div1 {}
</style>
挂载到app之外了,它永远不可能被加上属性选择器,并且它也不是子元素或孙子元素 所以我们在scoped内是不可能修改这个样式的
只能通过额外的style进行修改了,为防止样式互相影响,我们可以添加类名来限制作用域
<script setup>
import { ref } from 'vue'
const dialogVisible = ref(false)
</script>
<template>
<div class="div1">
<el-button @click="dialogVisible = true">按钮</el-button>
<el-dialog class="myDialog" v-model="dialogVisible" :append-to-body="true" title="Tips" width="30%">
<span>This is a message</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="dialogVisible = false">
Confirm
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.div1{}
</style>
<style >
.myDialog .el-dialog__header{
background-color: aqua;
}
</style>
结尾
深入的了解一下scoped,对组件之间的样式有了更深刻的理解,以后发生类似的bug直接拿捏它。
最后希望大家都能有所收获。