在 Vue 单文件组件中,<style scoped> 是一个高频使用的特性。它能让当前组件的样式只作用于自身模板内的元素,避免“改了一个按钮的样式,结果全站的按钮都变了”。但如果理解不深,它也会带来“样式加不上”的困惑,或者不经意间造成全局污染。
scoped 的实现原理
当你给 <style> 标签添加 scoped 属性时,Vue 的编译器会做两件事:
- 给当前组件模板内的每个 HTML 元素添加一个唯一的自定义属性,例如
data-v-f3f3eg9。 - 在每个 CSS 选择器的末尾追加一个属性选择器
[data-v-f3f3eg9]。
编译前后的对比如下:
<!-- 你写的模板 -->
<template>
<div class="box">
<p>文本</p>
</div>
</template>
<style scoped>
.box {
color: red;
}
.box p {
font-size: 16px;
}
</style>
实际渲染到页面上的 DOM 和样式会变成:
<div class="box" data-v-f3f3eg9>
<p data-v-f3f3eg9>文本</p>
</div>
.box[data-v-f3f3eg9] {
color: red;
}
.box p[data-v-f3f3eg9] {
font-size: 16px;
}
这种方式天然形成了一种 “样式隔离”:本组件的样式只会匹配到带有对应 data-v-xxx 属性的元素,不会影响其他组件。但如果要跨过这个作用域去修改子组件或外部组件的样式,就需要用到 样式穿透。
样式穿透的三种写法
假设你使用了一个第三方组件 <el-input>,它的内部渲染了很多 <div> 和 <input>,你想在当前组件中修改它内部的某个 input 的背景色。直接写 el-input input { background: #f0f0f0; } 会失效,因为编译后会变成 el-input input[data-v-xxx],而 <el-input> 内部的 input 没有你的 scoped 属性。
这时需要使用穿透,让规则作用到子组件的深层元素,同时保持选择器开头的组件选择器仍受 scoped 限定。Vue 提供了三种语法,实质相同:
:deep(.inner-selector)(Vue 3 推荐,标准写法)::v-deep(.inner-selector)(Vue 2 过渡写法,Vue 3 仍兼容)/deep/或>>>(已废弃,不推荐)
示例:
<style scoped>
/* 让 el-input 组件内部的 input 元素背景变色 */
:deep(.el-input__inner) {
background-color: #f5f5f5;
}
</style>
编译后,选择器会变成:
[data-v-f3f3eg9] .el-input__inner {
background-color: #f5f5f5;
}
可以看到,穿透只作用于选择器中 :deep() 包裹的部分,外层的 scoped 限定仍然保留。也就是说,这个规则只会影响当前组件根元素下发现的 .el-input__inner,不会作用到其他组件里的同样类名。
什么时候该用穿透
理论上,任何希望从父组件改变子组件内部样式的场景都可能用到穿透。典型场景包括:
- 调整第三方 UI 组件库(Element Plus、Ant Design Vue 等)的内部样式。
- 覆盖自己封装的组件内部类名,但不想污染全局。
- 在使用 插槽内容 时,插槽内的元素由父组件提供,但仍渲染在子组件的 DOM 中。如果子组件没有把穿透的样式插入到插槽内容的对应元素上,父组件的 scoped 样式可能无效,因为插槽内容带的是父组件的 scoped 属性,而子组件的容器才带子组件的属性。这时可能需要用
:deep()从父组件突破边界。
样式污染:隐藏的全局影响
scoped 本身是为了防止样式污染,但如果使用不当,穿透可能导致全局污染。关键点在于:当你穿透了一连串选择器,直到最后不再包含当前组件的 scoped 属性时,样式就可能散落到其他组件。
例如:
<style scoped>
/* 危险:穿透后直接选了全局选择器 */
:deep(body) {
margin: 0;
}
</style>
编译后变成 [data-v-f3f3eg9] body,这仍然受限于“当前组件根元素下的 body 后代”。但当前组件极少会包含 <body>,所以规则不会生效,不算真正的全局污染。真正需要警惕的是 深度穿透后没有 scoped 限定的情况,或者滥用 :global() 语法。
Vue 3 提供了 :global() 来显式声明全局样式,它通常与 scoped 搭配使用:
<style scoped>
/* 只在这个组件内生效 */
.box { color: red; }
/* 显式声明为全局样式,影响整个应用 */
:global(.global-class) {
font-size: 20px;
}
</style>
在大型项目中,全局样式的误用会导致难以追踪的样式覆盖问题。所以要在组件内写全局样式时,建议采用如下规范:
- 尽量在一个集中的全局样式文件中管理全局规则,比如
reset.css、global.scss,并在main.js中引入。 - 如果必须在组件内写全局样式,确保选择器具有足够特异性,最好使用唯一的、BEM 风格的长命名,或者放在一个可以预见的命名空间下(如项目统一的
app-前缀)。 - 配合 CSS Modules 或 CSS-in-JS 做更彻底的隔离,但 Vue 的 scoped 在大部分场景下已足够。
常见踩坑:穿透后样式不生效
很多开发者会遇到 :deep() 写了但样式不生效的情况。排查步骤:
- 确认选择器是否正确。打开浏览器开发者工具,检查目标元素的实际 DOM 结构和类名,而不是依赖文档。第三方组件库的内部类名有时会在小版本更新中变化。
- 确认穿透没有写反。
deep()应该包裹你想穿透的那部分选择器,而不是写在开头。例如:deep(.el-input__inner)是对的,el-input :deep(.inner)语法无效。 - 检查是否被更高优先级的选择器覆盖。浏览器的“计算样式”面板可以告诉你最终生效的样式来源。
- 注意 Vue 3 中
scoped不会穿透到插槽内容(父组件的 scoped 对子组件的插槽内容不生效)。此时如果确实需要控制插槽内容的样式,需要在子组件中用:deep()暴露可覆盖的类,父组件使用全局样式或通过 props 传递类名。
最佳实践总结
- 能用 scoped 就不用全局:保持组件内样式自封闭。
- 穿透要克制:仅当确实需要改变子组件内部样式时使用,并尽量用最小范围的选择器(如
:deep(.specific-class)),而非无限制的标签选择器或通配符。 - 全局样式集中管理:避免在组件内零散声明全局类,让样式架构清晰可控。
- 善用 CSS 变量:很多组件库支持通过 CSS 变量定制主题,这比强行穿透修改内部结构更稳定、更易维护。例如
--el-color-primary就可以全局改变 Element Plus 的主题色,而不需要去修改组件的每个类。