Code前端首页关于Code前端联系我们

Vue3组件样式不生效?必须搞懂的v-deep替换规则和正确用法

terry 1小时前 阅读数 13 #Vue

最近不少从Vue2转Vue3的小伙伴都踩过这个坑:明明在组件里给子组件内部的DOM元素写了样式,刷新页面一看一点变化没有,检查浏览器开发者工具的样式面板,要么找不到自己写的规则,要么被划了删除线,这时候有人会赶紧去翻旧代码里的v-deep,结果一用还报错“v-deep usage as a combinator has been deprecated”,瞬间懵圈——原来Vue3对样式穿透做了大调整啊,别慌,今天咱们就把这件事说透,从为什么会不生效、v-deep的前世今生、不同构建工具下的正确写法,再到什么时候不该用穿透式样式,全捋一遍。

Vue3为什么不让子组件内部样式“随便改”了?

想搞懂用法,得先明白底层逻辑,不管是Vue2还是Vue3,单文件组件(SFC)的样式默认都是scoped作用域的——这是一个特别实用的设计,能避免样式污染,比如你在页面组件里加了一个通用的按钮类.btn,如果不加scoped,整个项目里所有叫.btn的按钮都会受影响,改样式就像拆炸弹;加了scoped之后,Vue会给这个组件里的所有DOM元素(包括通过插槽传入的?哦不对,插槽后面单独说)加上一个唯一的属性,比如data-v-7ba5bd90,然后把你的样式规则也改成.btn[data-v-7ba5bd90],这样就只在当前组件生效了。

那为什么给子组件内部的元素写scoped样式没用呢?因为这个唯一属性只加在父组件自己的根元素直接创建的子元素上,不会穿透到子组件的模板里去,比如你有个父组件<Parent>,里面用了<Child />,Child的模板是<div class="child-inner"></div>,那Vue只会给<Parent>的根加data-v-p,给<Child>标签加data-v-p,但不会给Child里的.child-inner加,这时候你在Parent的scoped样式里写.child-inner { color: red; },浏览器会找带.child-innerdata-v-p的元素,根本找不到,自然白搭。

v-deep在Vue2里什么样?Vue3为什么要弃用旧写法?

Vue2.0刚出的时候,scoped样式里改子组件内部样式靠的是/deep/或者:v-deep伪类选择器,后来因为/deep/在某些预处理器(比如Sass/Less)里是用来做深度嵌套的关键字,容易冲突,Vue2.2之后推荐优先用:v-deep,Vue2.6之后又加了更简洁的v-deep作为预处理器兼容的写法(也就是直接用v-deep包裹要穿透的选择器,不用冒号)。

那到了Vue3,旧的:v-deep作为伪元素、v-deep作为组合子的写法为什么都被弃用了?核心原因是要和W3C的CSS标准对齐,旧写法本质上是Vue对CSS语法的自定义扩展,这不仅容易和未来的CSS标准冲突,还会给一些第三方工具(比如CSS-in-JS、CSS后处理器的高级插件)带来解析压力,所以Vue3官方决定换一种更“标准”的实现方式。

Vue3样式穿透的3种正确写法,你要根据构建工具选

Vue3现在推荐的穿透式样式写法有两种,加上一种兼容旧浏览器的可选方案,一共三种,关键是得看你用的是什么构建工具:

用Vite或者Vue CLI 5+?优先选deep()伪类

Vite和Vue CLI 5用的都是Volar提供的官方编译器插件,这种写法是目前最标准、最符合未来规划的。

它的用法很简单:把你需要穿透到子组件内部的那部分选择器,用deep()包裹起来就行,举个例子,刚才的Parent组件场景,正确的写法应该是:

<style scoped>
/* 只针对父组件自己创建的.btn,不会影响Child里的 */
.btn {
  background-color: blue;
}
/* 会穿透到Child组件内部,找到它的.btn */
:deep(.child-inner .btn) {
  color: red;
  font-size: 14px;
}
</style>

这里有个小细节:deep()包裹的是从子组件内部开始的选择器链,或者说包裹的是会受到作用域属性影响的那部分选择器,比如刚才的例子里,.child-inner是Child组件里的元素,没有父组件的data-v-p,所以整个.child-inner .btn都得被包起来,如果反过来,你写parent-wrapper :deep(.child-inner),那也是可以的——parent-wrapperdata-v-p,所以可以放在外面,deep()只包没有属性的部分。

还在用Vue CLI 4.x?得用:v-deep()(或者旧的:v-deep凑合用但尽量换)

Vue CLI 4用的还是旧版的Vue Loader(一般是15.x版本),虽然15.9.0+的Vue Loader也支持deep(),但有时候预处理器里会出问题,或者旧项目里依赖的其他工具对新语法兼容不好,这时候可以用升级版的:v-deep()伪类,用法和deep()完全一样,只是多了两个冒号。

<style scoped lang="scss">
/* Vue CLI 4.x + Sass/Less推荐写法 */
.parent-wrapper {
  background-color: white;
  ::v-deep(.child-inner .btn) {
    border: none;
    border-radius: 4px;
  }
}
</style>

旧的:v-deep作为伪元素写法(比如:v-deep .child-inner)在Vue3的旧编译器下还能跑,但会在控制台报废弃警告,建议尽快改成伪函数形式,避免以后升级工具后直接报错。

需要兼容IE11?那只能去掉scoped或者用全局样式了?等等,还有个备选

虽然现在用Vue3的项目大多不需要兼容IE11了,但万一有老项目改造呢?IE11完全不支持CSS伪函数,这时候deep():v-deep()都用不了,这时候怎么办?有两个办法: 第一个办法比较稳妥但会增加复杂度:给子组件暴露一个props或者插槽,让父组件可以直接控制内部元素的类名,比如Child组件可以加个innerClass prop:

<!-- Child.vue -->
<template>
  <div :class="['child-inner', innerClass]"></div>
</template>
<script setup>
defineProps(['innerClass'])
</script>

然后Parent组件里传类名就行,完全不需要样式穿透:

<!-- Parent.vue -->
<template>
  <Child innerClass="parent-custom-inner" />
</template>
<style scoped>
/* 这里的类名通过props传给Child,Child的组件会自动加自己的data-v-c,不过没关系,我们可以写两个?不对,等下,Parent的scoped样式里的.parent-custom-inner会有data-v-p,Child的模板里的.parent-custom-inner只有data-v-c,这时候怎么办?哦,对了,给Parent的style也加个非scoped的块? */
</style>
<style>
/* 非scoped块里的样式是全局的,但这里类名起得尽量特殊,比如加个组件前缀,就不会污染了 */
.parent-custom-inner {
  color: red;
}
</style>

第二个办法是去掉父组件相关样式的scoped,但这个办法风险大,除非你能保证类名绝对唯一,否则不推荐。

这些坑一定要避开!别让样式穿透帮倒忙

虽然样式穿透很方便,但用不好也会出大问题,比如样式污染、代码难以维护,下面这几个是新手最容易踩的:

坑1:随便穿透第三方组件库的内部元素

第三方组件库(比如Element Plus、Ant Design Vue)的内部DOM结构是随时可能更新的——这次你穿透了.el-input__inner,下次官方可能把类名改成.el-input__field,你的样式就全失效了,甚至会导致组件布局错乱。 那如果必须改第三方组件的样式怎么办?官方一般都会推荐几种更安全的方式:

  1. 用组件暴露的props:比如Element Plus的<el-button>sizetypeplain这些属性,能满足大部分样式需求;
  2. 用组件暴露的插槽:比如按钮的内容、输入框的前缀后缀,都可以通过插槽自定义;
  3. 用组件暴露的自定义类名:很多组件都有classwrapper-classinput-class这类属性,可以把你的自定义类名加在你需要的层级上,然后在非scoped样式里写,或者用穿透(但前提是类名加的层级足够高,不会因为内部结构变化失效)。

坑2:把deep()写得太宽

比如你写deep(.btn),那当前组件里所有子组件里的.btn都会受影响,哪怕你只想改其中一个,这时候应该尽量把deep()的选择器链写得更具体,比如加上父组件的特定类名、子组件的特定类名:

<style scoped>
/* 只针对有.special-wrapper的父组件里的.special-child组件的.btn */
.special-wrapper :deep(.special-child .btn) {
  color: green;
}
</style>

坑3:在插槽内容的样式里用穿透?哦不对,插槽其实有特殊规则

刚才提到过scoped样式不会给插槽内容加属性?其实不完全对,Vue2和Vue3的插槽样式规则不一样:

  • Vue2里,插槽内容的样式完全由父组件的scoped样式控制,子组件的scoped样式改不了插槽内容的元素;
  • Vue3里,刚好反过来——插槽内容被视为子组件的“一部分”,所以会加子组件的data-v-c属性,父组件的scoped样式如果要改插槽内容,反而不需要用穿透?不对不对,举个例子你就懂了:
    <!-- Child.vue -->
    <template>
    <div class="child-wrapper">
      <slot></slot>
    </div>
    </template>
    <style scoped>
    /* Vue3里,子组件的scoped样式可以改插槽里的元素,因为插槽里的span加了data-v-c */
    .slot-span {
    color: blue;
    }
    </style>
``` 哦,刚才的表述差点错了,现在纠正一下:Vue3插槽内容的DOM元素会同时加**父组件的data-v-p**吗?不对,去翻一下Vue SFC的编译结果就知道了——刚才这个例子里,插槽里的span只会加**子组件Child的data-v-c**,因为Vue3的编译器会把插槽内容的作用域切换到子组件,除非你用的是作用域插槽(带`v-slot:xxx="scope"`的那种)?不对不对,作用域插槽也是一样的,除非?哦,对了,不管是普通插槽还是作用域插槽,Vue3里父组件的scoped样式要改插槽里的元素,**都需要用样式穿透**,因为那个元素没有父组件的作用域属性。

刚才这段可能有点绕,建议大家可以自己写个小Demo,用浏览器开发者工具看一下每个元素的属性,再写几个样式试一下,就完全明白了。

什么时候该用样式穿透?什么时候尽量别用?

最后给大家总结一下使用原则:

该用样式穿透的场景

  1. 改自己项目里封装的、内部结构完全稳定的公共组件的样式——比如你自己封装了一个项目级的<MyTable>组件,里面有个表头的背景色需要根据不同页面调整,这时候用穿透没问题,因为组件是你自己写的,结构不会随便变;
  2. 改第三方组件库官方没有提供props/slot/custom-class,但内部结构在近期版本里非常稳定的小细节——比如Element Plus的表格滚动条样式,官方可能没有直接的属性,但一般不会随便改DOM结构,这时候可以偶尔用一下,但最好在代码里加个注释,说明是穿透了哪个版本的哪个类名,以后升级的时候记得检查。

尽量别用样式穿透的场景

  1. 改第三方组件库的核心布局或者交互相关的样式——比如改Element Plus的<el-dialog>的弹窗位置计算方式相关的样式,很容易导致组件出bug;
  2. 类名可能频繁变化的组件——不管是自己的还是第三方的;
  3. 可以通过props/slot/custom-class解决的问题——尽量用官方推荐的方式,代码更可维护,也更安全。

好了,今天关于Vue3样式穿透的内容就讲到这里,从为什么会不生效,到v-deep的前世今生,再到不同构建工具下的正确写法,还有常见的坑和使用原则,应该能帮你解决大部分样式问题,如果还有其他Vue3的疑问,欢迎在评论区留言哦。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门