在实践中总是碰到这几个关键字,遂写文记录,介绍 Vue 单文件组件中的 scoped 样式作用机制、常见问题与解决方案,并配合实战示例说明何时使用 :deep、:global、:slotted 以及更推荐的替代方案。
什么是 scoped?它如何工作
- scoped 的作用:让当前
<style scoped>中的样式只作用于当前组件的模板,避免样式互相污染。 - 编译原理:编译器会给当前组件模板节点添加一个唯一属性(例如
data-v-xxxxxx),同时为样式选择器追加对应属性选择器,从而实现“只选中带此属性的节点”。 - 示例(编译后的效果):
<!-- 模板 -->
<div class="title">Hello</div>
<!-- 编译后(简化) -->
<div class="title" data-v-abc123>Hello</div>
<!-- 样式选择器也会被重写 -->
.title[data-v-abc123] { color: red; }
scoped 的常见“失效”场景
- v-html 渲染的内容:动态插入的 HTML 不会自动带上
data-v-xxxxxx,scoped 无法命中。 - 子组件内部结构:父组件的 scoped 样式不会“穿透”到子组件的内部 DOM。
- 第三方组件/库的内部 DOM:同上,不可直接命中内部结构。
- Teleport 到 body 等全局容器的内容:因为不在当前组件 DOM 树内,scoped 选择器无法生效。
- 具备更高优先级/内联样式:即使 scoped 命中,可能被更高优先级的规则覆盖。
解决方案总览
- :deep():仅取消括号内选择器的作用域限制,适合“穿透”到子组件或 v-html 内容。
- :global():将括号内选择器声明为“全局规则”,不追加
data-v-xxxxxx。 - :slotted():在子组件中,给通过插槽传入的“宿主内容”加样式(注意是作用于插槽内容的外层)。
- 替代思路(更推荐):通过组件 props/class 暴露样式入口、使用 CSS 变量,避免强耦合的“样式穿透”。
深度选择器 :deep 的用法
- 函数式语法(推荐):
:deep(.selector),只让括号内选择器脱离作用域限制。 - 组合器语法:
::v-deep .selector(等价,历史写法,建议优先用函数式)。 - SCSS 中常见写法:
/* 父组件的 scoped 样式 */
.block {
/* 只让 .inner 脱离作用域,.block 仍在作用域内 */
:deep(.inner) {
color: #7c0044;
}
}
/* 同理:深度选择器可以嵌套在任意层级 */
.container {
.card {
:deep(.lib-button.primary) {
border-color: #7c0044;
}
}
}
全局选择器 :global 的用法
- 将括号内选择器标记为全局(不会追加 data-v 属性),适合覆盖全站通用样式或第三方库的“顶层类名”:
:global(.el-message) {
z-index: 9999;
}
插槽选择器 :slotted 的用法
- 只影响通过“插槽”传入的宿主内容外层(无法深入到更里层 DOM):
/* 子组件内 */
:slotted(.slot-item) {
margin: 8px;
}
示例 1:给 v-html 内容加样式(随便举个例子😄)
- 模板:
<!-- 父组件模板 -->
<p class="content" @click="handleClick">
<span v-html="label('passenger.termsAndConditions')"></span>
</p>
- 翻译文案(已包含 class):
termsAndConditions:
'Please read and agree to the <span class="terms-of-service-link">"Conditions".</span>',
- 样式(scoped + :deep):
.terms-of-service {
.content {
:deep(.terms-of-service-link) {
color: #7c0044;
text-decoration: none;
font-weight: 500;
}
}
}
- 事件委托(只在目标类名上触发):
const handleClick = (event: Event) => {
const target = event.target as HTMLElement;
// 硬编码在翻译中的 class
if (target.classList.contains('terms-of-service-link')) {
handleClickTermsOfService();
}
};
示例 2:穿透到子组件内部元素
<!-- 父组件 -->
<ChildComponent class="host"/>
<style scoped lang="scss">
.host {
/* 只对 ChildComponent 内部 .btn 生效 */
:deep(.btn) {
padding: 8px 12px;
}
}
</style>
示例 3:覆盖第三方库的内部类名
/* 尽量在业务容器下做局部覆盖,避免污染全局 */
.home-page {
:deep(.el-dialog__headerbtn) {
top: 10px;
}
}
示例 4:Teleport 到 body 的内容
/* 通过 :global 直接覆盖全局容器 */
:global(.teleported-layer) {
position: fixed;
inset: 0;
}
实践建议
- 优先暴露接口:让子组件通过 props/class/CSS 变量暴露样式入口,避免父组件用 :deep 穿透强耦合到内部结构。
- 局部约束覆盖范围:若必须覆盖第三方样式,用“业务容器类”包裹后再 :deep,降低影响面。
- 控制选择器优先级:尽量避免使用
!important;需要时提高结构层级选择器优先级。 - 谨慎 v-html:仅用于可信内容,并配合 :deep 控制样式。对于文本和链接,优先用组件拼装而非 v-html。
- 按需使用 :global:只在需要全局覆盖时使用,避免造成样式污染。
- 多语言/动态内容:把需要点击的元素 class 放入文案中,并使用事件委托绑定(就像当前的实现)。
常见问题
- :deep 只“解 scoped”,不自动提升优先级:若库本身使用了更强选择器,仍需手动提高权重。
- 过度使用 :deep / :global:会削弱 scoped 的隔离价值,建议收敛在局部容器下使用。
- :slotted 能力有限:只能命中插槽的“外层”,不能深入更多层级。
- 预处理器嵌套:SCSS 中注意 :deep 的位置,
.a { :deep(.b) { ... } }与:deep(.a .b)命中范围不同。