拿捏scoped样式失效我不怕不怕啦~ scoped属性究竟干了啥?

前言

作为前端开发,在日常工作中总是避免不了与样式打交道。为了避免样式的互相影响,我们在vue的style标签往往会添加scoped进行样式隔离。但是加上scoped究竟做了什么呢?为什么有时候加上scoped后,我修改的样式不起作用了呢?下面我将对上述这两个问题进行说明

scoped做了什么

我们都知道scoped的作用是防止组件之间样式互相影响,起到隔离的作用。

我们来回想一下,在以前开发中没有scoped是如何避免样式冲突的。

举个简单的例子,我想分别修改页面中头部和尾部div的字体颜色。我们通常会给两个div分别起一个类名,来避免这两个div的样式互相影响。

html 复制代码
<!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

html 复制代码
<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

html 复制代码
<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中的表现形式为:

xml 复制代码
<style>
.div1[data-v-7c55d5f5]{
    color:red;
}

.div[data-v-1c412v9h]{
    color:pink;
}
</style>

由于属性选择器的值不同,这两个div1的样式自然就不会冲突,就起到了样式隔离的效果。

scoped导致样式失效

在日常开发中,我们偶尔会遇到设置的样式没有生效,这有很大的可能性和我们添加的scoped有关。我们再来看下面这个例子,我们在上面例子的基础上给空白的类名加上样式。

index.vue

html 复制代码
<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中添加空白的类

html 复制代码
<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添加属性选择器有几个特点

  1. 没有子组件的情况,它会给每个类的标签上都加上data-v-xxxxx
  2. 有子组件的情况,遵循上一条的同时,给子组件的最外层加上data-v-xxxxxx。具体表现是div3div4是子组件的元素,只有div3被添加上了data-v-7c55d5f5这个属性,而div4则没有。
  3. 在那个组件声明的样式默认会被添加上该组件生成的自定义属性。表现为div4是在index.vue中设置的样式,它的自定义属性被设置成了data-v-7c55d5f5

怎么解决样式失效

样式失效的主要原因就是,在父组件中添加的样式会被自动添加上父组件的自定义属性,而子组件中只有最外层会被添加上父组件的自定义属性,其余子孙元素都是子元素生成的自定义属性。找不到对应的选择器的元素,导致样式失效。

解决这个问题的办法有很多

  1. 要设置子组件的样式,就在子组件中设置。不在父组件中设置,我们对例子中子组件div4的样式进行设置
html 复制代码
<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生成的值了,理所当然样式也就生效了。

所以尽量不要在父元素中设置子组件的样式,以免出现样式失效的问题。

  1. 去掉scoped标签,最简单粗暴的方式。出问题的源头就是因为自动添加的自定义属性有问题,直接解决出问题的人,当然这种做法是不妥当的,因为我们还需要它进行样式隔离。
  2. 另写一个style标签,这种做法的原理和去掉scoped一样。算是一种较为妥当的方式,即解决了问题又没有破坏样式隔离的特性。需要注意的是额外写的那个标签可能会影响到其他同类名的样式,最好类名唯一一点。
html 复制代码
<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>
  1. 推荐 使用在子孙元素的类名前添加:deep()伪类,具有这个伪类的元素代表这个元素是我的子节点或者孙子节点,你不要加属性选择器了。
css 复制代码
<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

html 复制代码
<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(),或者使用上述几种解决方法的。

html 复制代码
<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上面的,也就意味着它不是当前组件的子元素。

html 复制代码
<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进行修改了,为防止样式互相影响,我们可以添加类名来限制作用域

html 复制代码
<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直接拿捏它。

感谢B站的水哥澎湃讲师,我是看完它讲的这个题目。在他的基础上又进行了各种尝试,加入了些自己的理解才写出来的。

最后希望大家都能有所收获。

相关推荐
forwardMyLife5 分钟前
element-plus的面包屑组件el-breadcrumb
javascript·vue.js·ecmascript
ice___Cpu6 分钟前
Linux 基本使用和 web 程序部署 ( 8000 字 Linux 入门 )
linux·运维·前端
JYbill9 分钟前
nestjs使用ESM模块化
前端
加油吧x青年27 分钟前
Web端开启直播技术方案分享
前端·webrtc·直播
计算机学姐39 分钟前
基于python+django+vue的影视推荐系统
开发语言·vue.js·后端·python·mysql·django·intellij-idea
luoluoal1 小时前
java项目之基于Spring Boot智能无人仓库管理源码(springboot+vue)
java·vue.js·spring boot
吕彬-前端1 小时前
使用vite+react+ts+Ant Design开发后台管理项目(二)
前端·react.js·前端框架
小白小白从不日白1 小时前
react hooks--useCallback
前端·react.js·前端框架
恩婧1 小时前
React项目中使用发布订阅模式
前端·react.js·前端框架·发布订阅模式
mez_Blog1 小时前
个人小结(2.0)
前端·javascript·vue.js·学习·typescript