解决 vue 开发中 scoped 嵌套样式失效问题

10,297 阅读4分钟

前言

在 vue 项目开发的过程中我们常常需要引用子组件,包括第三方的 ui 组件(element、iview),但是在父组件中 添加 scoped 之后,在父组件中无法编写子组件的样式。

vue-loader 的作用

要解决在父组件中编写子组样式的失效问题,有必要先了解一个.vue 组件的编译前后的情况对比。

在一个 vue 的项目中,整个项目是通过 .vue 单文件组件组织的,一个组件即是从 UI 拆分下来的每个包含模版(HTML)+ 样式(CSS)+ 逻辑(JS)功能完备的结构单元。正是因为 vue-loader 的存在我们才得以组件化的形式进行编码。下面是一个简单的 vue 组件例子:

<template>
  <div class="example">{{ msg }}</div>
</template>

<script>
export default {
  data () {
    return {
      msg: 'Hello world!'
    }
  }
}
</script>

<style>
.example {
  color: red;
}
</style>

它通过 vue-loader 进行编译后,在浏览器中是这样显示的:

<head>
	<style type="text/css">
		.example {
		  color: red;
		}
	</style>
</head>

<body>
  <div class="example">Hello world!</div>
  <script type="text/javascript" src="/js/app.js"></script>
</body>

.vue 组件里的 style、script、template 分别会编译为相应的 style 标签、html 标签以及 script 标签引入。

样式失效原因分析

我们已经知道 vue-loader 的基本作用了。接下来,我们通过一个常见的父子组件例子来说明样式失效的原因。

项目结构:

App.vue
ParentA.vue
ParentB.vue
Child.vue

本文中我们主要关注 .vue 组件的 html 和 style 部分。

App.vue

<template>
  <div class="App">
    {{ msg }}
    <ParentA></ParentA>
    <ParentB></ParentB>
  </div>
</template>

<style>
.App {
  color: #000;
}
</style>

ParentA.vue

<template>
  <div class="ParentA">
    {{ msg }}
  </div>
</template>

<style>
.ParentA {
  color: red;
}
</style>

ParentB.vue

<template>
  <div class="ParentA">{{ msg }}</div>
</template>
<style>
.ParentB {
  color: blue;
}
</style>

添加 scoped 前

在 style 标签添加 scoped 属性前, .vue 组件 的编译结果是:

<head>
  <style type="text/css">
	.ParentA {
	  color: red;
	}
  </style>
</head>

<body>
  <div class="ParentA"> ParentA </div>
  <div class="ParentA">ParentB</div>
</body>

其中 ParentB.vue 里使用了 ParentA 的类名,也就是说 ParentA.vue 组件对 .ParentA 类 的样式更改是会污染到全局样式的,也就是说会影响到 ParentB.vue 组件样式,这会大大减低了项目的可维护性。

在一个大型项目中,不同的人的开发会导致两个不同的组件中使用了相同的类名,从而互相之间产生影响。好在 vue-loader 给我们提供了 scoped 属性,从编码上解决了这个问题。

添加 scoped 后

现在让我们给 ParentA.vue 的 style 标签添加上 scoped 属性,这时候编译输出的是:

<style type="text/css">
  .ParentA[data-v-183fa219] {
	color: red;
  }
</style>
<div data-v-183fa219  class="ParentA"> ParentA</div>
<div class="ParentA">ParentB</div>

可以看到输出的 ParentA.vuestyle 标签内多了一个属性选择器,并且 html 标签上也多了对应的 data-v-183fa219 属性值。这样的话,无论 .ParentA 的样式怎么变都不会影响到 ParentB 的样式了,即使它们使用了同一个类名,这就是所谓的作用域约束。

在 vue 中, 一个 ui 界面的组成是通过 vue 的组件嵌套而成。下面我们要 ParentA 中引用一个子组件,为了避免污染全局样式,所以我们也给它的 style 标签添加上 scoped 属性。

Child.vue

<template>
  <div class="Child">
    {{ msg }}
  </div>
</template>

<style scoped>
.Child {
  color: green;
}
</style>

然后在 ParentA.vue 中更改 Child.vue 的样式(PS:在引入第三方组件如 iview 中,我们也常常覆盖它的组件样式来满足 UI 的需求)

<template>
  <div class="ParentA">
    {{ msg }}
    <Child></Child>
  </div>
</template>

<style lang="scss" scoped>
.ParentA {
  color: red;
 // 覆盖 Child 组件的文字颜色
  .Child {
    color: pink;
  }
}
</style>

ParentA.vue 编译后输出:

<style type="text/css">
  .Child[data-v-0fcd625e] {
	color: green;
  }
</style>
<style type="text/css">
  .ParentA[data-v-183fa219] {
    color: red;
  }
  .ParentA .Child[data-v-183fa219] {
	color: pink;
  }
</style>

<div data-v-183fa219 class="ParentA"> 
	ParentA 
	<div data-v-0fcd625e data-v-183fa219 class="Child">Child </div>
</div>

这里要注意的是:可以看到 Child.vue 生成的标签里,带有自身的属性 id 值 data-v-0fcd625e,也含有 Parent.vue 的属性id值 data-v-183fa219,因此在 ParentA.vue 组件设置 Child.vue 的样式也是有效的。

**那什么时候会无效呢?**我们继续在 Child.vue 中添加一个子元素。

<template>
  <div class="Child">
    {{ msg }}
    <div class="Child-content">Child 内容</div>
  </div>
</template>

假设这个时候,我们还想继续通过 ParentA.vue 来更改 Child-content 的样式:

// ParentA.vue
<style lang="scss" scoped>
.ParentA {
  color: red;
  .Child {
    color: pink;
    &-content {
      background: green;
    }
  }
}
</style>

这个时候你会发现不起作用,我们来看看具体的编译输出就知道原因了:


<style type="text/css">
	.ParentA[data-v-183fa219] {
	  color: red;
	}
	.ParentA .Child[data-v-183fa219] {
		color: pink;
	}
	// Child-content
	.ParentA .Child-content[data-v-183fa219] {
		  background: green;
	}
</style>
<body>
	<div data-v-183fa219  class="ParentA"> 
	ParentA 
	<div data-v-0fcd625e data-v-183fa219  class="Child"> 
		Child 
		<div data-v-0fcd625e  class="Child-content">
			Child 内容
		</div>
	</div>
</div>
</body>

可以看到父组件的 scoped 属性 id 值并没有赋予给子组件的子元素 Child-content,因此在父组件修改这些子组件的样式不会生效。

.ParentA .Child-content[data-v-183fa219] {
	  background: green;
}

与 Child-content 无法对应起来。

<div data-v-0fcd625e  class="Child-content">
	Child 内容
</div>

解决方法

后来 vue-loader 给出了新的解决方案(PS:这是官方的 issue 讨论地址Support /deep/ selector ),那就是可以在 scoped 设置的情况下,添加 deep 选择器 深度处理子组件的元素样式。

现在我们试试给 ParentA.vue 添加 deep 的处理:

<style lang="scss" scoped>
.ParentA {
  color: red;
  /deep/ .Child {
    color: pink;
    &-content {
      background: green;
    }
  }
}
</style>

然后看看添加 deep 后的 style 标签编译输出:

<style type="text/css">
.ParentA[data-v-183fa219] {
  color: red;
}
.ParentA[data-v-183fa219] .Child {
    color: pink;
}
.ParentA[data-v-183fa219] .Child-content {
      background: green;
}
</style>

这个时候,可以看到 ParentA[data-v-183fa219] .Child-content 替代了

.ParentA .Child-content[data-v-183fa219] {
	  background: green;
}

这样只要在 ParentA[data-v-183fa219] 覆盖下的子元素都可以在父组件中进行样式修改了。

小结

本文通过前后对比的方式一步步引出 vue scoped 带来的问题与解决方法。很多时候,我们通过 google 可以快速找出问题答案,而多问为什么这样做会让我们举一反三。

另外补充:vue-loader 还提供了 ::v-deep 以及 >>>,效果跟 deep 是一样的,注意的是像 sass 预处理器不能解析 >>>

本文中涉及的代码已经放在这里

(本文完)

进一步阅读