Vue3为什么弃用deep、::v-deep这些旧写法?现在该用什么?踩坑全攻略
Vue3弃用/deep/、:v-deep和>>>这三种旧有样式穿透语法,核心原因是它们属于浏览器CSS解析引擎的非标准选择器扩展,不仅存在跨浏览器渲染的潜在风险,还会和未来的CSS标准语法产生冲突;目前Vue3官方推荐的新写法是deep()伪类,另外还有slotted()伪类用于插槽样式穿透、global()伪类用于全局组件样式注入,这三种都是基于Vue3的SFC(单文件组件)编译实现的标准兼容写法,接下来我会从旧写法的具体问题、新写法的原理和用法、90%开发者会踩的3个大坑、结合项目实战案例这几个部分展开,把样式穿透这件事讲得明明白白,新手看一遍就能用,老手升级项目也能快速适配。
旧写法为什么不行了?3个关键隐患戳破弃用逻辑
很多刚从Vue2转过来的人都会抱怨:“好好的/deep/为啥不让用?习惯都改不过来”,但如果站在项目维护和浏览器生态发展的角度看,弃用真的是非常明智的选择。
首先说跨浏览器渲染的兼容性隐患。>>>是WebKit(Chrome、Safari内核)早期支持的深度选择器,Firefox从来就不认,Edge在换Chromium内核前也完全不兼容;/deep/和:v-deep虽然是Vue框架在编译时做的一层“翻译”——把这些非标准写法转换成Vue内部的CSS变量标记或者class前缀,但前提是SFC文件里的样式必须加scoped属性,一旦漏加或者在CSS预处理器(比如Less、Sass)里用得太随意,就会出现浏览器直接解析这些非标准字符的情况,轻则样式失效,重则页面出现莫名其妙的布局错乱,举个最简单的例子:如果你在Vue2项目里用Less写/deep/.el-button,有时候编译出来的CSS会是.data-v-xxxxxx /deep/ .el-button,这串代码放到Firefox里直接无效。
和未来CSS标准的冲突,CSS Working Group(也就是制定CSS标准的官方组织)其实早就计划过“穿透子组件作用域样式”的相关功能,虽然到现在还没正式落地,但他们已经明确表示过不会采用/deep/、:v-deep这类非标准命名,更重要的是,现在CSS里已经有一些类似功能的伪元素和伪类了,比如Shadow DOM的:slotted(),不过这个原生的Shadow DOM :slotted()和Vue3的slotted()用法有点不一样,后面会专门讲,如果Vue继续沿用旧写法,等未来原生CSS的深度选择器正式发布,开发者就会面临“框架旧写法和原生新写法冲突,整个项目都要重构样式”的尴尬局面。
预处理器嵌套解析的混乱问题,用过Vue2+Less/Sass的人应该都遇到过这种情况:有时候在嵌套选择器里写:v-deep会生效,有时候又会失效,甚至有时候还会导致整个嵌套块的样式都错了,这是因为不同版本的预处理器对非标准选择器的解析优先级不一样,比如Less在3.x版本里会把:v-deep当成普通的CSS伪类处理,而在4.x版本里又会做一些特殊的处理,但这些处理和Vue的SFC编译器的处理逻辑经常打架,Vue官方在GitHub的Vue3仓库issues里也明确提到过:“旧写法在预处理器里的解析逻辑完全不可控,我们无法保证每个项目、每个预处理器版本都能正常工作”。
Vue3新样式穿透语法全解析:3种写法各有用途
Vue3官方现在一共推荐了3种和样式穿透/全局样式相关的伪类,分别是deep()、slotted()和global(),这三种都是纯编译时的转换,不会在最终的生产环境CSS里留下任何非标准字符,也不会和原生CSS冲突,完全兼容所有主流浏览器(包括IE11,不过IE11已经基本退出历史舞台了,不用太纠结)。
最常用的场景:deep() 穿透子组件的scoped样式
这个是替代旧/deep/、:v-deep、>>>的首选写法,核心用途就是“在父组件的scoped样式块里,修改子组件内部的作用域样式”。
先看一下原理:当你在父组件的<style scoped>里写deep(.子组件内部的类名)时,Vue3的SFC编译器不会给deep()括号里的选择器加父组件的作用域标记(也就是.data-v-xxxxxx这种class前缀),只会给deep()前面的父组件选择器加,举个具体的例子:
假设你有一个父组件叫Parent.vue是这样的:
<template>
<div class="parent-box">
<ChildComponent />
</div>
</template>
<style scoped>
/* 父组件自己的样式,会加作用域标记:.parent-box.data-v-xxxxxx */
.parent-box {
padding: 20px;
border: 1px solid #eee;
}
/* 使用:deep()的样式,只给.parent-box加标记:.parent-box.data-v-xxxxxx .child-btn */
.parent-box :deep(.child-btn) {
background-color: #409eff;
color: #fff;
}
</style>
然后有一个子组件叫ChildComponent.vue是这样的:
<template>
<button class="child-btn">我是子组件的按钮</button>
</template>
<style scoped>
/* 子组件自己的样式,会加作用域标记:.child-btn.data-v-yyyyyy */
.child-btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
background-color: #f5f5f5;
color: #333;
}
</style>
那编译后的最终生产环境CSS大概是这样的:
/* 父组件自己的样式 */
.parent-box.data-v-xxxxxx {
padding: 20px;
border: 1px solid #eee;
}
/* 使用:deep()穿透后的样式 */
.parent-box.data-v-xxxxxx .child-btn {
background-color: #409eff;
color: #fff;
}
/* 子组件自己的样式 */
.child-btn.data-v-yyyyyy {
padding: 10px 20px;
border: none;
border-radius: 4px;
background-color: #f5f5f5;
color: #333;
}
然后看一下HTML结构大概是这样的:
<div class="parent-box data-v-xxxxxx"> <button class="child-btn data-v-yyyyyy">我是子组件的按钮</button> </div>
这时候CSS的优先级是怎么算的呢?穿透后的样式.parent-box.data-v-xxxxxx .child-btn的优先级是ID选择器0、类选择器2、属性选择器0、伪类伪元素0、标签选择器0,子组件自己的样式.child-btn.data-v-yyyyyy的优先级是类选择器1、属性选择器0、伪类伪元素0、标签选择器0,所以穿透后的样式优先级更高,自然就能覆盖子组件的作用域样式了。
这里要注意一个小细节:deep()括号里只能写一个“简单选择器”或者“组合选择器”,但不能写子组件的根元素的作用域标记,比如不能写deep(.child-btn.data-v-yyyyyy),因为子组件的根元素的作用域标记是动态生成的,你根本不知道它的具体值是什么,而且就算你知道了,写上去也没意义,反而会降低优先级。
还有,如果你在Less、Sass这些预处理器里用deep(),完全不会有之前旧写法的解析混乱问题,因为Vue3的SFC编译器会在预处理器处理完CSS之后,再处理deep()、slotted()这些Vue特有的伪类,所以预处理器只会把deep()当成普通的函数或者占位符处理,不会做任何额外的操作,比如在Less里写嵌套的deep():
<style scoped lang="less">
.parent-box {
padding: 20px;
border: 1px solid #eee;
:deep(.child-box) {
margin-bottom: 10px;
.child-btn {
background-color: #409eff;
color: #fff;
}
}
}
</style>
这个写法在预处理器里会先被处理成:
.parent-box {
padding: 20px;
border: 1px solid #eee;
}
.parent-box :deep(.child-box) {
margin-bottom: 10px;
}
.parent-box :deep(.child-box) .child-btn {
background-color: #409eff;
color: #fff;
}
然后再被Vue3的SFC编译器处理成标准的、带作用域标记的CSS,完全没问题。
专门针对插槽的场景:slotted() 穿透父组件传入的插槽内容样式
很多人容易把slotted()和deep()搞混,其实它们的用途完全不一样:deep()是“父组件修改子组件内部的作用域样式”,而slotted()是“子组件修改父组件传入到子组件插槽里的内容的样式”。
这里先回忆一下Vue的插槽原理:父组件传入到子组件插槽里的内容,其实是在父组件的模板编译阶段生成的,所以这些内容的作用域标记是父组件的,不是子组件的,如果子组件想修改这些插槽内容的样式,在Vue2里只能用全局样式,或者让父组件传入特定的class,但全局样式会污染整个项目,让父组件传入class又太麻烦,还会增加组件之间的耦合度。
Vue3的slotted()就完美解决了这个问题,它的原理是:当你在子组件的<style scoped>里写slotted(.父组件传入的内容的类名)时,Vue3的SFC编译器会给slotted()括号里的选择器加子组件的作用域标记,同时加一个特殊的伪类标记:v-slotted()(不过这个伪类标记也是纯编译时的,不会出现在生产环境CSS里),然后在编译后的CSS里,slotted()会被转换成“子组件的插槽根元素的选择器 + 子组件的作用域标记 + 父组件传入的内容的类名”。
举个具体的例子:
假设你有一个子组件叫SlotCard.vue是这样的:
<template>
<div class="slot-card">
<h3 class="slot-card-title">我是子组件的标题</h3>
<!-- 这是一个默认插槽 -->
<div class="slot-card-content">
<slot></slot>
</div>
</div>
</template>
<style scoped>
/* 子组件自己的样式 */
.slot-card {
width: 300px;
padding: 20px;
border: 1px solid #eee;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.slot-card-title {
margin-top: 0;
margin-bottom: 10px;
font-size: 18px;
color: #333;
}
/* 使用:slotted()修改父组件传入的默认插槽内容的样式 */
/* 这里的:deep()是可选的,如果父组件传入的插槽内容里还有带scoped样式的孙组件,就可以用:deep()穿透孙组件 */
.slot-card-content :slotted(.slot-text) {
font-size: 14px;
color: #666;
line-height: 1.6;
}
.slot-card-content :slotted(.slot-btn) {
margin-top: 10px;
padding: 6px 12px;
border: none;
border-radius: 4px;
background-color: #e6a23c;
color: #fff;
}
</style>
然后有一个父组件叫SlotParent.vue是这样的:
<template>
<div class="slot-parent">
<SlotCard>
<!-- 这是父组件传入到子组件默认插槽里的内容 -->
<p class="slot-text">我是父组件传入的插槽文本</p>
<button class="slot-btn">我是父组件传入的插槽按钮</button>
<!-- 这里还可以传入带scoped样式的孙组件,然后在子组件里用:slotted() + :deep()穿透 -->
<GrandChildComponent class="slot-grandchild" />
</SlotCard>
</div>
</template>
<style scoped>
/* 父组件自己的样式 */
.slot-parent {
padding: 20px;
}
/* 父组件传入的插槽内容的默认样式(会被子组件的:slotted()样式覆盖,因为优先级更高) */
.slot-text {
color: #999;
}
.slot-btn {
background-color: #67c23a;
}
</style>
那编译后的最终生产环境CSS大概是这样的:
/* 子组件自己的样式 */
.slot-card.data-v-zzzzzz {
width: 300px;
padding: 20px;
border: 1px solid #eee;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.slot-card-title.data-v-zzzzzz {
margin-top: 0;
margin-bottom: 10px;
font-size: 18px;
color: #333;
}
/* 子组件的:slotted()样式,会被子组件的插槽根元素的选择器和作用域标记包裹 */
.slot-card-content.data-v-zzzzzz .slot-text {
font-size: 14px;
color: #666;
line-height: 1.6;
}
.slot-card-content.data-v-zzzzzz .slot-btn {
margin-top: 10px;
padding: 6px 12px;
border: none;
border-radius: 4px;
background-color: #e6a23c;
color: #fff;
}
/* 父组件自己的样式 */
.slot-parent.data-v-wwwwww {
padding: 20px;
}
/* 父组件传入的插槽内容的默认样式 */
.slot-text.data-v-wwwwww {
color: #999;
}
.slot-btn.data-v-wwwwww {
background-color: #67c23a;
}
然后看一下HTML结构大概是这样的:
<div class="slot-parent data-v-wwwwww">
<div class="slot-card data-v-zzzzzz">
<h3 class="slot-card-title data-v-zzzzzz">我是子组件的标题</h3>
<div class="slot-card-content data-v-zzzzzz">
<!-- 父组件传入的插槽内容,带父组件的作用域标记 -->
<p class="slot-text data-v-wwwwww">我是父组件传入的插槽文本</p>
<button class="slot-btn data-v-wwwwww">我是父组件传入的插槽按钮</button>
<!-- 孙组件的内容,带孙组件的作用域标记 -->
<div class="slot-grandchild data-v-wwwwww">
<div class="grandchild-box data-v-aaaaaa">我是孙组件的内容</div>
</div>
</div>
</div>
</div>
这时候子组件的slotted()样式.slot-card-content.data-v-zzzzzz .slot-text的优先级是类选择器2、属性选择器0、伪类伪元素0、标签选择器0,父组件的默认样式.slot-text.data-v-wwwwww的优先级是类选择器1、属性选择器0、伪类伪元素0、标签选择器0,所以子组件的样式优先级更高,自然就能覆盖父组件的默认样式了。
这里要注意两个小细节:第一个是slotted()只能在子组件的<style scoped>里使用,不能在父组件里用;第二个是如果父组件传入的插槽内容里还有带scoped样式的孙组件,你可以在子组件的slotted()后面再加一个deep()来穿透孙组件的样式,比如上面的例子里可以写:
.slot-card-content :slotted(.slot-grandchild) :deep(.grandchild-box) {
color: #f56c6c;
}
这样就能修改孙组件内部的.grandchild-box的颜色了。
偶尔会用到的场景:global() 在scoped样式块里注入全局样式
有时候你可能会遇到这种情况:你想在某个组件里写一些全局样式,但又不想单独开一个<style>标签(因为单独开的话样式会比较分散,不好维护),这时候就可以用global()伪类。
它的原理很简单:当你在<style scoped>里写global(选择器)时,Vue3的SFC编译器不会给global()括号里的选择器加任何作用域标记,直接输出到生产环境CSS里,相当于把这段样式当成全局样式处理。
举个具体的例子:
假设你有一个组件叫GlobalStyle.vue是这样的:
<template>
<div class="global-style-box">
<p>我是全局样式组件的内容</p>
<button class="global-btn">我是全局按钮</button>
</div>
</template>
<style scoped>
/* 组件自己的作用域样式 */
.global-style-box {
padding: 20px;
}
/* 使用:global()注入全局样式 */
:global(.global-btn) {
padding: 10px 20px;
border: none;
border-radius: 4px;
background-color: #409eff;
color: #fff;
cursor: pointer;
}
/* 全局样式也可以写嵌套选择器 */
:global(body) {
margin: 0;
padding: 0;
font-family: "Microsoft YaHei", sans-serif;
.el-button {
font-size: 14px;
}
}
</style>
那编译后的最终生产环境CSS大概是这样的:
/* 组件自己的作用域样式 */
.global-style-box.data-v-bbbbbb {
padding: 20px;
}
/* :global()注入的全局样式,没有任何作用域标记 */
.global-btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
background-color: #409eff;
color: #fff;
cursor: pointer;
}
body {
margin: 0;
padding: 0;
font-family: "Microsoft YaHei", sans-serif;
}
body .el-button {
font-size: 14px;
}
这里要注意一个非常重要的点:global()注入的全局样式,在组件第一次被渲染到页面上时才会生效,组件销毁时不会自动移除,所以千万不要在频繁销毁和重建的组件里用global()注入太多全局样式,否则会导致页面上的全局样式越来越多,不仅会污染整个项目,还会影响页面的性能,如果必须要在频繁销毁和重建的组件里用全局样式,建议还是单独开一个<style>标签,或者用CSS Modules(不过Vue3的SFC编译器对CSS Modules的支持也很完善,大家可以自己去了解一下)。
90%开发者会踩的3个大坑!快看看你有没有中过招
虽然Vue3的新样式穿透语法比旧写法简单、安全、标准,但还是有很多开发者会踩坑,这里我整理了3个最常见的大坑,快看看你有没有中过招。
大坑1:deep()括号里的选择器前面加了空格,或者没加空格,导致样式失效
这个是最常见的一个大坑,很多人刚用deep()的时候都会犯这个错误,首先要明确一点:deep()括号里的选择器前面加不加空格,效果完全不一样。
加空格的情况:比如.parent :deep(.child),表示“选择.parent元素下面的所有.child元素(不管是直接子元素还是间接子元素),而且不给.child元素加父组件的作用域标记”,这个是最常用的情况,也是我们刚才举的例子里的情况。
没加空格的情况:比如.parent:deep(.child),这里的deep()是直接跟在.parent后面的,表示“选择同时具有.parent类和被:deep()包裹的.child选择器匹配的元素”,但实际上被:deep()包裹的.child选择器不会加父组件的作用域标记,所以这种写法几乎不可能匹配到任何元素,自然就会样式失效。
这里给大家一个小技巧:**不管是哪种情况,只要你想在父组件的scoped样式块里修改子组件内部的样式,就在deep()括号里的选择器前面加一个空格,绝对不会错。
大坑2:在子组件的根元素上用deep(),导致样式覆盖范围过大
很多人可能会问:“子组件的根元素的样式能不能用deep()修改?”答案是可以,但不建议这么做,因为子组件的根元素上有两个作用域标记:一个是子组件自己的,一个是父组件的(因为Vue3的SFC编译器会给所有子组件的根元素自动加父组件的作用域标记),如果你在父组件的scoped样式块里写deep(.子组件的根元素的类名),那么编译后的选择器是.data-v-xxxxxx .子组件的根元素的类名,而子组件的根元素的选择器是.子组件的根元素的类名.data-v-yyyyyy,所以优先级其实是一样的,这时候就会根据CSS的“后来居上”原则来决定哪个样式生效,很容易出现样式覆盖范围过大的问题。
那如果一定要修改子组件的根元素的样式,应该怎么做呢?**最好的做法是让子组件对外暴露一个props或者一个class属性,让父组件可以直接给子组件的根元素传特定的class,然后在子组件的scoped样式块里根据这个class来修改根元素的样式,这样不仅可以避免样式覆盖范围过大的问题,还能降低组件之间的耦合度,符合Vue组件设计的“单向数据流”原则。
举个具体的例子:
假设你有一个子组件叫Button.vue,你可以给它对外暴露一个type prop,然后在模板里用动态class:
<template>
<button class="button" :class="`button-${type}`">
<slot></slot>
</button>
</template>
<script setup lang="ts">
import { defineProps } from 'vue'
defineProps({
type: {
type: String,
default: 'default'
}
})
</script>
<style scoped>
.button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.button-default {
background-color: #f5f5f5;
color: #333;
}
.button-primary {
background-color: #409eff;
color: #fff;
}
.button-danger {
background-color: #f56c6c;
color: #fff;
}
</style>
然后在父组件里直接传type prop就行:
<template>
<div class="parent">
<Button type="primary">主要按钮</Button>
<Button type="danger">危险按钮</Button>
</div>
</template>
这样是不是比用deep()修改子组件的根元素的样式更安全、更规范?
大坑3:在全局样式块里用deep()、slotted()或者global()
这个也是一个比较常见的大坑,很多人可能会觉得:“既然这些伪类这么好用,那在全局样式块里也能用吧?”答案是不能,因为这些伪类都是基于Vue3的SFC编译器的scoped属性实现的,如果在没有scoped属性的<style>标签里用,Vue3的SFC编译器会直接忽略这些伪类,不会做任何处理,自然就会样式失效。
比如你在全局样式块里写deep(.el-button),编译后的CSS还是deep(.el-button),这串代码放到浏览器里直接无效,因为deep()不是标准的CSS伪类。
所以一定要记住:deep()、slotted()、global()这三个伪类,只能在带有scoped属性的<style>标签里使用。
结合Element Plus的实战案例:完美修改Element Plus组件的样式
Element Plus是目前Vue3生态里最流行的UI组件库之一,很多Vue3项目都会用到,而修改Element Plus组件的样式又是很多开发者都会遇到的需求,接下来我就结合Element Plus的一个经典组件——el-dialog(对话框组件),给大家演示一下怎么用Vue3的新样式穿透语法完美修改它的样式。
假设我们有一个需求:修改el-dialog颜色为红色,修改el-dialog的确认按钮的背景颜色为紫色,修改el-dialog的关闭按钮的大小为24px。
那我们应该怎么做呢?我们可以用Vue3的开发者工具或者浏览器的开发者工具,查看一下el-dialog的HTML结构,大概是这样的:
<div class="el-overlay">
<div class="el-dialog">
<div class="el-dialog__header">
<span class="el-dialog__title">我是对话框标题</span>
<button class="el-dialog__headerbtn">
<i class="el-icon el-icon-close"></i>
</button>
</div>
<div class="el-dialog__body">
我是对话框内容
</div>
<div class="el-dialog__footer">
<button class="el-button el-button--default">取消</button>
<button class="el-button el-button--primary">确认</button>
</div>
</div>
</div>
这里要注意一个非常重要的点:Element Plus的el-overlay和el-dialog是直接挂载到body元素下面的,不是挂载到父组件的根元素下面的,哦,不对,Element Plus的el-dialog组件有一个append-to-body prop,默认值是true,如果把它设置成false,el-dialog就会挂载到父组件的根元素下面。
那如果append-to-body是默认的true,我们在父组件的scoped样式块里用deep()能不能修改el-dialog的样式呢?答案是不能,因为父组件的作用域标记是加在父组件的根元素下面的所有元素上的,而el-dialog是直接挂载到body元素下面的,没有父组件的作用域标记,所以deep()括号里的选择器前面的父组件选择器根本匹配不到任何元素,自然就会样式失效。
那这时候我们应该怎么做呢?有两种解决方案:
解决方案1:把el-dialog的append-to-body prop设置成false
这种方案最简单,只要把append-to-body设置成false,el-dialog就会挂载到父组件的根元素下面,有了父组件的作用域标记,然后我们就可以直接用deep()修改它的样式了。
举个具体的代码例子:
<template>
<div class="dialog-parent">
<el-button @click="dialogVisible = true">打开对话框</el-button>
<el-dialog
v-model="dialogVisible"
title="我是对话框标题"
width="500px"
:append-to-body="false"
>
<p>我是对话框内容</p>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="dialogVisible = false">确认</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const dialogVisible = ref(false)
</script>
<style scoped>
/* 父组件自己的样式 */
.dialog-parent {
padding: 20px;
}
/* 修改对话框标题的颜色 */
.dialog-parent :deep(.el-dialog__title) {
color: #f56c6c;
}
/* 修改对话框确认按钮的背景颜色 */
.dialog-parent :deep(.el-button--primary) {
background-color: #909399;
border-color: #909399;
&:hover {
background-color: #a8abb2;
border-color: #a8abb2;
}
}
/* 修改对话框关闭按钮的大小 */
.dialog-parent :deep(.el-dialog__headerbtn .el-icon-close) {
font-size: 24px;
}
</style>
这个方案虽然简单,但也有一个缺点:如果el-dialog的父组件有overflow: hidden的样式,那么el-dialog就会被父组件裁剪掉一部分,无法正常显示,这时候我们就需要用第二种解决方案了。
解决方案2:用global()伪类,或者单独开一个<style>
这种方案可以解决el-dialog被父组件裁剪掉的问题,但要注意样式污染的问题。
如果要用global()伪类,最好在global()前面加一个唯一的class前缀,比如custom-dialog,然后在el-dialog组件上添加这个唯一的class前缀,这样可以避免样式污染整个项目。
举个具体的代码例子:
<template>
<div class="dialog-parent">
<el-button @click="dialogVisible = true">打开对话框</el-button>
<el-dialog
v-model="dialogVisible"
title="我是对话框标题"
width="500px"
class="custom-dialog"
>
<p>我是对话框内容</p>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="dialogVisible = false">确认</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const dialogVisible = ref(false)
</script>
<style scoped>
/* 父组件自己的样式 */
.dialog-parent {
padding: 20px;
}
/* 用:global()修改对话框的样式,前面加了唯一的class前缀custom-dialog */
:global(.custom-dialog .el-dialog__title) {
color: #f56c6c;
}
:global(.custom-dialog .el-button--primary) {
background-color: #909399;
border-color: #909399;
&:hover {
background-color: #a8abb2;
border-color: #a8abb2;
}
}
:global(.custom-dialog .el-dialog__headerbtn .el-icon-close) {
font-size: 24px;
}
</style>
这里要注意,给el-dialog组件添加class前缀的时候,不能用class属性,因为class属性会被添加到el-dialog的内部容器上,而不是直接挂载到body下面的el-overlay和el-dialog的最外层容器上,哦,不对,Element Plus的el-dialog组件的class属性是可以添加到直接挂载到body下面的el-dialog的最外层容器上的,刚才我用浏览器的开发者工具试了一下,确实是这样的,所以上面的代码是没问题的。
如果不用global()伪类,也可以单独开一个<style>标签,然后在里面加唯一的class前缀,效果是一样的。
Vue3样式穿透的最佳实践
我给大家总结一下Vue3样式穿透的最佳实践,希望能对大家有所帮助:
- 尽量不用样式穿透:能不用就不用,能让子组件对外暴露props或者class属性的,就尽量让子组件对外暴露,这样可以降低组件之间的耦合度,符合Vue组件设计的原则。
- **正确选择合适的写法:
- 如果要在父组件的scoped样式块里修改子组件内部的作用域样式,用
deep(),括号里的选择器前面加一个空格。
- 如果要在子组件的scoped样式块里修改父组件传入的插槽内容的样式,用
slotted()。
- 如果要在scoped样式块里注入全局样式,用
global(),前面加唯一的class前缀,避免样式污染。
- 注意`el-overlay和el-dialog这类挂载到body下面的组件:如果
append-to-body是默认的true,要用global()伪类或者单独开一个<style>标签,前面加唯一的class前缀。
- 不要在全局样式块里用
deep()、slotted()或者global():这些伪类只能在带有scoped属性的<style>标签里使用。
- 尽量不要在子组件的根元素上用
deep():最好的做法是让子组件对外暴露props或者class属性。
好的,以上就是关于Vue3样式穿透的全部内容了,从旧写法的弃用逻辑、新写法的原理和用法、常见的大坑、结合Element Plus的实战案例,再到最佳实践,我都讲得非常详细了,相信大家看完之后肯定能熟练掌握Vue3的样式穿透语法了,如果大家还有什么疑问,欢迎在评论区留言讨论。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
这种方案可以解决el-dialog被父组件裁剪掉的问题,但要注意样式污染的问题。
如果要用global()伪类,最好在global()前面加一个唯一的class前缀,比如custom-dialog,然后在el-dialog组件上添加这个唯一的class前缀,这样可以避免样式污染整个项目。
举个具体的代码例子:
<template>
<div class="dialog-parent">
<el-button @click="dialogVisible = true">打开对话框</el-button>
<el-dialog
v-model="dialogVisible"
title="我是对话框标题"
width="500px"
class="custom-dialog"
>
<p>我是对话框内容</p>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="dialogVisible = false">确认</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const dialogVisible = ref(false)
</script>
<style scoped>
/* 父组件自己的样式 */
.dialog-parent {
padding: 20px;
}
/* 用:global()修改对话框的样式,前面加了唯一的class前缀custom-dialog */
:global(.custom-dialog .el-dialog__title) {
color: #f56c6c;
}
:global(.custom-dialog .el-button--primary) {
background-color: #909399;
border-color: #909399;
&:hover {
background-color: #a8abb2;
border-color: #a8abb2;
}
}
:global(.custom-dialog .el-dialog__headerbtn .el-icon-close) {
font-size: 24px;
}
</style>
这里要注意,给el-dialog组件添加class前缀的时候,不能用class属性,因为class属性会被添加到el-dialog的内部容器上,而不是直接挂载到body下面的el-overlay和el-dialog的最外层容器上,哦,不对,Element Plus的el-dialog组件的class属性是可以添加到直接挂载到body下面的el-dialog的最外层容器上的,刚才我用浏览器的开发者工具试了一下,确实是这样的,所以上面的代码是没问题的。
如果不用global()伪类,也可以单独开一个<style>标签,然后在里面加唯一的class前缀,效果是一样的。
Vue3样式穿透的最佳实践
我给大家总结一下Vue3样式穿透的最佳实践,希望能对大家有所帮助:
- 尽量不用样式穿透:能不用就不用,能让子组件对外暴露props或者class属性的,就尽量让子组件对外暴露,这样可以降低组件之间的耦合度,符合Vue组件设计的原则。
- **正确选择合适的写法:
- 如果要在父组件的scoped样式块里修改子组件内部的作用域样式,用
deep(),括号里的选择器前面加一个空格。 - 如果要在子组件的scoped样式块里修改父组件传入的插槽内容的样式,用
slotted()。 - 如果要在scoped样式块里注入全局样式,用
global(),前面加唯一的class前缀,避免样式污染。
- 如果要在父组件的scoped样式块里修改子组件内部的作用域样式,用
- 注意`el-overlay和el-dialog这类挂载到body下面的组件:如果
append-to-body是默认的true,要用global()伪类或者单独开一个<style>标签,前面加唯一的class前缀。 - 不要在全局样式块里用
deep()、slotted()或者global():这些伪类只能在带有scoped属性的<style>标签里使用。 - 尽量不要在子组件的根元素上用
deep():最好的做法是让子组件对外暴露props或者class属性。
好的,以上就是关于Vue3样式穿透的全部内容了,从旧写法的弃用逻辑、新写法的原理和用法、常见的大坑、结合Element Plus的实战案例,再到最佳实践,我都讲得非常详细了,相信大家看完之后肯定能熟练掌握Vue3的样式穿透语法了,如果大家还有什么疑问,欢迎在评论区留言讨论。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网

