◆

Vue 样式穿透 deep 的使用方法

2023-02-02 01:370

scoped 做了什么

给 <style> 加上 scoped 之后,Vue 会做两件事:给当前组件模板里的每个元素加一个 data 属性(形如 data-v-7ba5bd90),同时给每条 CSS 选择器的最后一个选择器追加这个属性选择器。

所以写下这样的样式:

<style scoped>
.list .item {
  color: red;
}
</style>

编译出来是:

.list .item[data-v-7ba5bd90] {
  color: red;
}

注意属性选择器只加在最后一个 .item 上,.list 上没有。这就是「样式只作用于当前组件」的实现方式,也是穿透问题的根源。

为什么需要穿透

问题出在子组件的元素上。父组件模板里写 <MyInput />,编译时父组件的 hash 只会打到 <MyInput> 这个根元素上,子组件内部的元素带的是子组件自己的 hash。

于是父组件里想改子组件内部结构的样式就失效了:

<template>
  <el-input class="my-input" />
</template>

<style scoped>
/* 无效:el-input 内部的 .el-input__inner 没有父组件的 hash */
.my-input .el-input__inner {
  border-radius: 8px;
}
</style>

编译成 .my-input .el-input__inner[data-v-xxx],而实际 DOM 里那个 input 元素上根本没有 data-v-xxx,所以不匹配。

这在用组件库的时候几乎天天遇到——库的组件内部结构是它自己的,我们要定制样式就必须有办法让选择器绕过 scoped 的限制。

深度选择器的写法演进

历史上有过好几种写法,容易混乱,这里按 Vue 版本理一下。

Vue 2 时代:>>> 是原生 CSS 深度选择器,但 Sass/Less 这类预处理器解析不了它,所以有了别名 /deep/ 和 ::v-deep。

Vue 3 现在推荐的唯一写法是 :deep(),它是一个伪类,接受选择器作为参数:

<style scoped>
.my-input :deep(.el-input__inner) {
  border-radius: 8px;
}
</style>

编译结果是把属性选择器加在 :deep() 之前的部分上:

.my-input[data-v-7ba5bd90] .el-input__inner {
  border-radius: 8px;
}

这样 .my-input 是父组件自己的元素(带 hash 能匹配上),后代 .el-input__inner 不带 hash,就能匹配到子组件内部元素了。

已废弃的写法:>>>、/deep/、不带括号的 ::v-deep .foo。Vue 3 里后两者会输出警告,>>> 在预处理器里本来就不可用。看老代码遇到这些直接换成 :deep()。

必须给 :deep() 加前缀限定

这是最容易忽略的一点。可以直接写 :deep(.el-input__inner) 不带前缀:

<style scoped>
/* 危险 */
:deep(.el-input__inner) {
  border-radius: 8px;
}
</style>

编译结果是 [data-v-xxx] .el-input__inner,属性选择器加在了一个「隐含的当前组件根元素」上。这看着还行,但实际上它会命中当前组件下所有层级的 .el-input__inner,包括嵌套很深的孙组件、通过插槽传进来的内容。在一个复杂页面里这就是全局污染。

正确做法是永远带一个自己的 class 作为限定:

<style scoped>
.my-form-item :deep(.el-input__inner) {
  border-radius: 8px;
}
</style>

配合 Sass 嵌套

在 Sass 里写嵌套时 :deep() 的位置容易搞错:

// 正确
.wrapper {
  :deep(.el-input__inner) {
    border-radius: 8px;
  }

  :deep(.el-table) {
    th {
      background: #f5f7fa;
    }
  }
}

// 错误:& 展开后 :deep 在最前,等于没有前缀限定
:deep(.el-table) {
  .wrapper & {
    color: red;
  }
}

:deep() 一旦出现,它右边的所有选择器都不再有 hash 限定。所以要限定作用域,前缀必须写在 :deep() 左边。

:slotted 和 :global

Vue 3 还提供了另外两个伪类,一起说清楚。

:slotted 用于给通过插槽传入的内容设置样式。默认情况下插槽内容带的是传入方(父组件)的 hash,不带当前组件的 hash,所以子组件的 scoped 样式管不到它:

<!-- 子组件 -->
<style scoped>
/* 无效,插槽内容没有本组件的 hash */
.card-body > p {
  margin: 0;
}

/* 有效 */
:slotted(p) {
  margin: 0;
}
</style>

:global 用于在 scoped 块里写一条全局样式,不加 hash:

<style scoped>
:global(body.modal-open) {
  overflow: hidden;
}
</style>

这比再开一个不带 scoped 的 <style> 块更内聚,但要慎用,写进去的东西是真正全局的。

动态样式用 v-bind 更合适

如果只是想根据组件状态改一个颜色或尺寸,不必用 :deep() 硬改结构,v-bind() 配合 CSS 变量更干净:

<script setup>
const theme = ref('#409eff');
</script>

<template>
  <div class="box">内容</div>
</template>

<style scoped>
.box {
  /* 编译成 CSS 变量,值变化时不重新生成样式表 */
  color: v-bind(theme);
}
</style>

组件库的定制也优先看它有没有暴露 CSS 变量。Element Plus 从 2.2 开始大量使用 --el-* 变量,改变量比 :deep() 覆盖选择器稳定得多——选择器依赖内部 class 名,库升级改了结构样式就失效,而变量是它的公开约定。

<style scoped>
.my-form {
  --el-input-border-radius: 8px;
  --el-color-primary: #722ed1;
}
</style>

权重不足的问题

:deep() 只解决了 hash 匹配,不解决 CSS 权重。组件库自己的样式可能选择器更长权重更高,这时候 :deep() 写对了样式还是不生效。

排查顺序是先在 devtools 里确认选择器有没有匹配上(匹配不上是 :deep() 用法问题),再看有没有被划掉(划掉是权重问题)。权重不够的解法是加长选择器而不是直接上 !important:

// 优先:加长自己的选择器
.my-form .el-form-item :deep(.el-input__inner) {
  border-radius: 8px;
}

// 最后手段
.my-form :deep(.el-input__inner) {
  border-radius: 8px !important;
}

另外样式的加载顺序也有影响。如果组件库的 CSS 是在业务样式之后被打包进去的,同权重下它会赢。这种情况检查一下入口文件里 import 'element-plus/dist/index.css' 的位置,它应该在业务样式之前。

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEVue 样式穿透 deep 的使用方法加载中