深入理解vue的scoped和module原理

3,975 阅读6分钟

一:scoped

<style> 标签有 scoped 属性时,它的 CSS 只作用于当前组件中的元素。

scoped的原理

  1. 给HTML的DOM节点加一个不重复data属性(形如:data-v-2311c06a)来表示他的唯一性。
  2. 在每句css选择器的末尾(编译后的生成的css语句)加一个当前组件的data属性选择器(如[data-v-2311c06a])来私有化样式。

    例如,转译前的代码如下:
//button.vue
<template>
    <div class="button-warp">
        <button class="button">text</button>
    </div>
</template>
...
<style scoped>
    .button-warp {
      display: inline-block;
    }
    .button{
        padding: 5px 10px;
        font-size: 12px;
        color: blue;
    }
</style>

渲染结果如下:

<div data-v-2311c06a class="button-warp">
    <button data-v-2311c06a class="button">text</button>
</div>

.button-warp[data-v-2311c06a]{
    display:inline-block;
}
.button[data-v-2311c06a]{
    padding: 5px 10px;
    font-size: 12px;
    color: blue;
}

这样的话,就算在两个互不相关的组件使用同样的类名,他们的样式也是互不影响的。

父子组件是否受scoped的影响

scoped的这一操作,虽然达到了组件样式模块化的目的,但是会出现两种情况:第一种,scoped CSS里每个样式的权重加重了,理论上我们可以修改某一个样式,但是却需要更高的权重去覆盖这个样式;第二种,无论父组件样式的权重多大,也可能无法修改子组件的样式。下面就分四种情况说明scoped的具体作用:

1.父组件未添加scoped,子组件未添加scoped

<template>
    <div class="content">
      <p class="title"></p>
      <!--Children是上面示例的组件-->
      <Children />
    </div>
</template>
<style>
.content {
  width: 1000px;
  margin: 0 auto;
}
.button {
  color: red;
}
</style>

运行结果如下:

这是最原始的一种情况,可以看到按钮字体颜色为red。此时父组件声明了.button {color:red;},子组件声明了.button {color:blue;},权重是一样的,但是父组件style会覆盖子组件的style,查看Elements后会发现,父组件的样式文件插入位置在子组件样式文件后面,对于权重相同的样式,后面的会覆盖前面,所以父组件的style生效。

2.父组件未添加scoped,子组件添加scoped

<template>
    <div class="content">
      <p class="title"></p>
      <!--Children是上面示例的组件-->
      <Children />
    </div>
</template>
<style>
.content {
  width: 1000px;
  margin: 0 auto;
}
.button {
  color: red;
}
</style>

运行结果如下:

我们可以看到子组件的.button {color:blue;}被编译成.button[data-v-469af010] { color: blue;},子组件中button样式的权重=类选择器(.button)+属性选择器([data-v-469af010]),而父组件中button样式的权重=类选择器(.button),子组件样式的权重加重了,覆盖本来能产生作用的父组件样式,导致按钮字体颜色为blue。这就是上面说到的情况一。

3.父组件添加scoped,子组件未添加scoped

代码就不贴上了,和上面的代码差不多,只是修改了scoped属性而已。运行结果如下:

我们可以看到按钮字体颜色为blue,父组件style添加scoped后,所有元素都加上data-v属性,包括子组件的根节点,但是子组件的内层元素就不会受影响,所以父组件的.button[data-v-7ba5bd90] {color:red;}作用不到子组件的.button上。

这也是修改element-ui内部组件的样式一般不能写在scoped CSS的原因,就算要写,也需要借助深度作用选择器。

注:如果只修改子组件根节点的样式,还是可以写到scoped CSS里,因为一个子组件的根节点会同时受其父组件的 scoped CSS 和子组件的 scoped CSS 的影响。

4.父组件添加scoped,子组件添加scoped

运行结果如下:

我们可以看到,子组件的根节点不仅有子组件的data-v属性,还有父组件的data-v属性。而.button元素依然不受父组件的影响,只拥有自己组件的data-v属性,所以按钮字体颜色为blue。

解决方案

那如果我就是想让父组件的样式覆盖子组件的样式,怎么办呢?

  1. 使用两个style,一个用于私有样式,一个用于共有样式。
<!--共有样式-->
<style>
.content .button {
  color: red;
}
</style>
<!--私有样式-->
<style scoped>
.content {
  width: 1000px;
  margin: 0 auto;
}
</style>
  1. 深度作用选择器
<style scoped>
.content {
  width: 1000px;
  margin: 0 auto;
}
.content >>> .button {
  color: red;
}
/* .content /deep/ .button {
  color: red;
} */
/* .content ::v-deep .button{
  color: red;
} */
</style>

上述代码将会编译成:

<style type="text/css">
.content[data-v-7ba5bd90] {
  width: 1000px;
  margin: 0 auto;
}
.content[data-v-7ba5bd90] .button {
  color: red;
}
/* .content /deep/ .button {
  color: red;
} */
/* .content ::v-deep .button{
  color: red;
} */
</style>

有些像 Sass 之类的预处理器无法正确解析 >>>。这种情况下你可以使用 /deep/ 或 ::v-deep 操作符取而代之——两者都是 >>> 的别名,同样可以正常工作。

总结scoped的缺点

  • scoped CSS里每个样式的权重加重了,理论上我们可以修改某一个样式,但是却需要更高的权重去覆盖这个样式;
  • 无论父组件样式的权重多大,也可能无法修改子组件的样式,除了子组件的根节点。
  • 使用标签选择器时scoped会严重降低性能,而使用class或id则不会。

二:module

高版本的@vue/cli已经集成CSS Moudles功能,不需要配置vue.config.js。

module原理

赋予组件特定的类名(默认配置为"文件名_原class名_不定后缀"),不产生副作用也能达到私有化样式的效果。
例如,转译前的代码如下:

// App.vue
<template>
  <div id="app">
    <div :class="$style['border']">1111</div>
  </div>
</template>

<style module>
.border {
  border: 1px solid red;
}
</style>

渲染结果如下:

<div id="app">
    <div class="App_border_y_ncl">1111</div>
</div>

<style>
.App_border_y_ncl {
    border: 1px solid red;
}
</style>

module的用法和优点

  • 相对于scoped的方式,module的方式不会生成多余的data-属性,混淆element,能一眼知道该元素时属于哪个文件组件中。在大型项目中能够帮助我们迅速定位到要查找的组件。
  • module会将所有的style都归入$style中,所以我们可以很灵活的将任意的父组件样式传递到任意深层的子组件中。
<!--App.vue-->
<template>
  <div id="app">
    <HelloWorld :propStyle="$style['border']" />
  </div>
</template>
<style module>
.border {
  border: 1px solid red;
}
</style>
<!--Child.vue-->
<template>
  <div>
    <div :class="$attrs.propStyle">1111</div>
  </div>
</template>
  • 可以导出定义的变量,将变量归入$style中。在script中也能拿到css module。
// App.vue
<template>
  <div id="app">
    <div>{{$style.titleColor}}</div> <!-- red -->
  </div>
</template>

<script>
mounted () {
    console.log(this.$style)
    // { border: "App_border_y_ncl",titleColor: "red" }
}
</script>

<style module lang="scss">
$title-color: red;
:export {
  titleColor: $title-color
}
.border {
  border: 1px solid red;
}
</style>
  • 单独使用module.css文件。如果想去掉文件名中的 .module,可以设置 vue.config.js 中的 css.requireModuleExtension 为 false。
// app.module.css
.border {
  border: 1px solid red;
}
<template>
  <div id="app" :class="styles.border">
    1111
  </div>
</template>

<script>
import styles from './app.module.css'
export default {
  data () {
    return {
      styles
    }
  }
}

使用css module在keyframes中的问题

使用CSS modules处理动画animation的关键帧keyframes,动画名称必须先写。animation: ani 1s能正常编译,而animation: 1s ani则会编译的不符合预期。

注意:@vue/cli已经集成了CSS Modules,可以通过 <style module> 达到开箱即用。但如果想去掉文件名中的 .module或自定义生成 CSS Modules 模块的类名,还需要配置vue.config.js。配置方式vue-cli3-config-reference

参考链接:

待办事项:

  • 阅读理解vue-loader的scoped相关源码
  • 阅读理解vue-loader的module相关源码