Vue3 组件化开发(4)——组件的 CSS 作用域问题

1,169 阅读2分钟

「这是我参与2022首次更文挑战的第24天,活动详情查看:2022首次更文挑战」。

我们在项目目录(learn_component)下的 src 目录下新建 02_组件的css作用域 文件夹,在该文件夹中新建 App.vueHelloWorld.vue 文件:

App.vue

<template>
  <h2>App</h2>
  <hello-world></hello-world>
</template>

<script>
  import HelloWorld from './HelloWorld.vue'
  
  export default {
    components: {
      HelloWorld
    }
  }
</script>

<style scoped>
  h2 {
    color: #f00;
  }
</style>

注意:这里我们在 <template> 下添加了两个根元素(在 Vue 3<template> 下是支持多个根元素的,但在 Vue 2 中只允许有一个根元素),而我们使用的 Vetur 插件默认会使用 eslint-plugin-vue<template> 里面的模板内容进行验证,它不允许 <template> 下有多个根元素,所以会出现如下报错信息:

image-20220102142436405

解决办法:对 Vuter 扩展进行配置,不对模板进行验证(下图中取消勾选 Vetur > Validation: Template 这一项):

image-20220102142805839

HelloWorld.vue

<template>
  <h2>Hello World</h2>
</template>

<script>
  export default {
    
  }
</script>

<style scoped>

</style>

同时修改 src/main.js 中引入 App.vue 组件的路径:

import { createApp } from 'vue'
import App from './02_组件的css作用域/App.vue'

createApp(App).mount('#app')

然后我们来看下效果:

image-20220102143619659

你会发现,App.vueHelloWorld.vue 两个组件中的 <h2> 元素中的文字都变成了红色。但我们明明只在 App.vue 文件中的 <style> 上添加了 scoped 属性设置的样式。

这里 <style> 上添加了 scoped(作用域内的)属性,目的是为了防止组件之间(比如我们这里 App.vue 组件和 HelloWorld.vue 组件)的样式相互污染。怎么防止样式污染的呢?从上图中可以看到,Vue 会在相应的元素上添加 data-v-xxxxxxxx 属性(xxxxxxxx 是一个十六进制的数字),然后在设置样式时,带有这个 data-v-xxxxxxxx 属性的相关元素才会设置对应的样式。因为整个 .vue 文件是交给 vue-loader 处理的,所以上面这个过程会由 vue-loader 完成。

也就是说,我们在 App.vue 中的 <style> 上添加 scoped 属性,本意是给 App.vue 组件的样式添加作用域,但现在的结果是并没有生效(App.vue 组件中使用的 HelloWorld.vue 组件中的样式被 App.vue 中的样式覆盖了,即父组件中的样式在子组件中也生效了)。

从上图中可以看到,作用域没有生效的原因是 Vue 在父组件和子组件的根元素上会添加相同的 data-v-xxxxxxxx 属性,而我们这里恰巧是用标签选择器 h2 设置的样式,而父子组件中的根元素又都是 <h2>,所以相应的样式对子组件中的元素也生效了。因此,结果看起来就有了样式穿透的效果,但个人觉得这种穿透肯定不是 Vue 的本意,因为 Vue 的本意应该是:在 App.vue 中设置的样式,是不应该在 HelloWorld.vue 中生效的。所以个人觉得这应该是一个 bug

当然,在 Vue 2 中,一般不会出现上面这种情况。因为在 Vue 2 中,我们在编写 <template> 中的内容时,一般会先用一个 <div> 元素作为根元素,然后在这个根元素下再编写内容:

<template>
  <div>
    <h2>Hello World</h2>
    <h2>Hello World</h2>
    <h2>Hello World</h2>
  </div>
</template>

那么,我们可以用一个根元素进行包裹来避免这样的问题,HelloWorld.vue 中的代码修改如下:

<template>
  <div>
    <h2>Hello World</h2>
  </div>
</template>

<script>
  export default {
    
  }
</script>

<style scoped>

</style>

效果如下:

image-20220102153620284

所以,推荐下面这种格式编写 <template> 模板:

<template>
  <div class="xxx">
    <!-- 模板的具体内容 -->
  </div>
</template>

此外,开发中一般很少直接使用一个标签选择器去设置样式,我们一般会通过给元素设置 class,再通过类选择器去设置样式:

<template>
  <h2 class="title">App</h2>
  <hello-world></hello-world>
</template>

<script>
  import HelloWorld from './HelloWorld.vue'
  
  export default {
    components: {
      HelloWorld
    }
  }
</script>

<style scoped>
  /* h2 {
    color: #f00;
  } */

  .title {
    color: #f00;
  }
</style>

最后,针对目前确实存在的这个问题,个人觉得应该还是在 Vue 3 中的 <template> 中可以有多个根组件了,我们编写的模板写法出现了变化,但 vue-loader 还没有完全考虑到这个问题,所以存在这个问题。但既然 scoped 是用来设置作用域的,那就不应该出现样式穿透,所以个人认为上面出现的问题应该是一个 bug