前言
在 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.vue
的 style
标签内多了一个属性选择器,并且 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 预处理器不能解析 >>>
。
本文中涉及的代码已经放在这里
(本文完)
进一步阅读
- vue-loader github仓库 - 有兴趣的同学可以移步到 vue-loader 探索 scoped 源码的实现。