vue样式穿透

617 阅读4分钟

背景:经常在UI框架进行样式穿透的时候,会发生不生效的情况,既会是因为选择器优先级问题或者穿透写法问题,这种比较好理解,提升优先级调整写法就好,但在vue中有时候需要将scoped去掉才能生效,有时候又不能去掉scoped,有时候因为写法的问题导致不生效【需要用一个大盒子包裹】,整理下具体原因,以后遇到其他场景再逐步完善。

核心遵旨

  1. 去掉scoped属性样式就变成全局样式了
  2. 添加:deep()是把其属性选择器放在最前面
  3. 第三方UI组件只会为根元素添加 data-v-xxx 属性
  4. 包一层大盒子实现属性嵌套

1. scoped的作用以及原理

作用:避免样式污染,不加scoped,书写的样式作用于全局,加了的话样式仅针对当前组件生效

原理:每个配置了scoped的组件分配一个唯一哈希,通过css属性选择器实现域划分

案例

// App.vue
<template>
  <RouterView></RouterView>
</template>

<style scoped>
</style>
// layout.vue
<template>
  <div class='AdminPage'>
    <div class='app-header-box'>
        <AppHeader/> 
     </div>
    <div class='app-content-box'></div>
  </div>
</template>

<style scoped>
.AdminPage {
  position: relative;
  width: 100%;
  background-color: #F3F2F2;
}

.AdminPage .app-header-box{
  position: fixed;
  width: 100%;
  height: 48px;
  z-index: 100;
}

.AdminPage .app-content-box{}
</style>

添加了scoped的组件,最终渲染效果:

  1. 每个添加了scoped的组件渲染出来都会附带一个唯一的属性data-v-xxx;
  2. 一个组件中的所有标签都会带上同样的data-v-xxx属性;
  3. 子组件会带上父组件的data-v-xxx属性;
  4. 在使用第三方的 UI 库时,只会为根元素添加 data-v-xxx 属性,子元素中则不会添加

1.png

2. deep()做了什么?

简介:可以在 style 标签中使用 :deep() 的方法进行样式穿透,主要是解决在使用第三方的 UI 库(如 element-plus 等)时导致的对其样式设置不生效的问题。

例子:

<template>
    <div class="main">
        <el-input class="ipt"></el-input>
    </div>
</template>

<script setup></script>

<style scoped>
  .ipt {
    width: 300px;
  }
</style>

2.png

当使用以下方法修改样式时并不能生效

.ipt .el-input_wrapper {
	background-color: red;
}

出现这种结果的原因就在于 Vue 将 [data-v-7a7a37b1] 属性添加到 .el-input 之后, 而 .el-input__wrapper 的标签上并不存在 [data-v-7a7a37b1] 属性。那么 deep 样式穿透随之而来。

2.1 deep样式穿透

:deep(.ipt .el-input__wrapper) {
	background-color: red;
}

3.png

:deep() 函数会把属性选择器放在最前面,那么就可以捕获到啦!

3. 源码解析

目录:core-main/packages/compiler-sfc/src/compileStyle.ts

export function doCompileStyle(
  scoped = false,
): SFCStyleCompileResults | Promise<SFCStyleCompileResults> {
  ......
  if (scoped) {
    plugins.push(scopedPlugin(longId))
  }
  ......
}

在这个函数中,如果存在 scoped 属性,就会调用 postcss 这个插件,这个插件的主要作用就是把 CSS 转换成抽象语法树 (AST) 便于之后的操作。

function processRule(id: string, rule: Rule) {
  ......
  rule.selector = selectorParser(selectorRoot => {
    selectorRoot.each(selector => {
      rewriteSelector(id, selector, selectorRoot)
    })
  }).processSync(rule.selector)
}

之后在 processRule 函数中调用 rewriteSelector()  方法对 CSS 选择器进行重写。

function rewriteSelector(
  id: string,
  selector: selectorParser.Selector,
  selectorRoot: selectorParser.Root,
  slotted = false,
) {
  let node: selectorParser.Node | null = null
  let shouldInject = true
  // find the last child node to insert attribute selector
  selector.each(n => {
    ......
    if (n.type === 'pseudo') {
      const { value } = n
      // deep: inject [id] attribute at the node before the ::v-deep
      // combinator.
      if (value === ':deep' || value === '::v-deep') {
        if (n.nodes.length) {
          // .foo ::v-deep(.bar) -> [xxxxxxx] .foo .bar
          // replace the current node with ::v-deep's inner selector
          let last: selectorParser.Selector['nodes'][0] = n
          n.nodes[0].each(ss => {
            selector.insertAfter(last, ss)
            last = ss
          })
          // insert a space combinator before if it doesn't already have one
          const prev = selector.at(selector.index(n) - 1)
          if (!prev || !isSpaceCombinator(prev)) {
            selector.insertAfter(
              n,
              selectorParser.combinator({
                value: ' ',
              }),
            )
          }
          selector.removeChild(n)
        } else {
          ......
        }
            ......
        }

当遇到 :deep 时,会将原来的属性选择器添加到前面元素中,即:.foo ::v-deep(.bar) -> [xxxxxxx] .foo .bar 通过这种方法就能定位到第三方 UI库中的 CSS 选择器了。

当使用一些第三方的 UI 库时,由于 Vue3 实现了模块化封装,那么在设置 UI 库的 CSS 样式时有时会出现设置不成功的问题,那么这个时候可以考虑使用 :deep() 来进行样式穿透。

4. 为什么有时候穿透需要多包裹一层大盒子

案例:father.vue嵌套子组件son.vue

// father.vue
<template>
  <div>father</div>
  <Son />
</template>

<script setup>
import Son from "./Son.vue"

</script>

<style lang="less" scoped>
div {
  color: red;
}
:deep(.second-row) {
  color: yellow;
}
</style>
// son.vue
<template>
  <div>111</div>
  <div class="second-row">222</div>
  <div>333</div>
</template>
<script></script>

<style scoped>
.second-row {
  color: green
}
</style>

按照上述书写,穿透样式并未生效,仅子组件样式生效

4.png

这是因为并没有生成嵌套关系,父组件中由于添加scoped注册的样式穿透是针对date-v-father这一前置条件下的,也就是:

[data-v-7a7a37b1] .second-row {
    color: blue;
}

但是,目前的dom结构下data-v-7a7a37b1下面并没有second-row的类名

所以需要调整,父组件使用一个大盒子包裹起来,完成嵌套,即:

// father.vue
<template>
  <div>
    <div>father</div>
    <Son />
  </div>
</template>

<script setup>
import Son from "./Son.vue"

</script>

<style lang="less" scoped>
div{
  color: red;
}
:deep(.second-row){
  color: blue;
}
</style>

5.png

5. 样式穿透的一些写法

写法有:::v-deep,>>>,:deep(),/deep/

具体区分:

如果你使用的是css,没有使用css预处理器,则可以使用>>>,/deep/,::v-deep。

如果用的less,node-sass,可以使用/deep/,::v-deep。

如果用的dart-sass,那么就不能用/deep/,使用::v-deep。

如果使用vue2.7以上版本以及vue3,::v-deep可以使用但是会有警告。