示例:同名类的 scoped 隔离
这个案例来自 docs/demo:一个 Demo 组件里渲染了四张类名完全相同
(class="card")的卡片,分别来自四种样式来源:
| 卡片 | 写法 | 样式来源 | 作用域 |
|---|---|---|---|
| 1 | 结构直接写在父组件里 | demo.scoped.scss | 父组件 hash |
| 2 | 子组件 ChildCard | child-card.scoped.css | 子组件自身 hash |
| 3 | 子组件 InheritCard | 无自带样式,逐层绑定父 scopedId | 继承父组件 hash |
| 4 | 子组件 GlobalCard | 父组件引入的 global.css | 全局(选择器不改写) |
style scoped 的含义:当前文件里类名的样式只属于当前文件,不污染全局。
类名相同没关系 —— 每条规则都会被改写成带本文件 hash 的选择器
(.card[data-v-{hash}]),DOM 上也只带属于自己文件的 data-v-{hash},两者
一一对应、互不干扰。若隔离不生效,四张同名卡会互相覆盖、渲染错乱;隔离生效
时,谁命中谁不命中,一眼就能看出来。
下方是真实渲染:页面 <script> 里 import Demo from './demo/demo.tsx',直接
把 Demo 当作页面组件渲染出来。
同名 .card:scoped / 继承 / 全局
1. 父组件内直写 card
结构写在 demo.tsx 中,DOM 带父组件 hash;样式来自父的 demo.scoped.scss 里同名 .card,只作用于本文件。
2. ChildCard 自带 scoped
同名 .card,样式来自本组件的 child-card.scoped.css (.card[data-v-子hash]),父的 .card 规则命中不了这里。
3. InheritCard 全元素绑定父 scopedId
每个标签都带上父级 data-v,父的 .card__name / .card__desc 等内层规则也能命中。
4. GlobalCard(global.css · 无 scoped)
global.css 不带 scoped 后缀:选择器不改写、全站生效 —— 别处出现同名 .card 也会被它影响;上面三张卡因 [data-v-*] 特异性更高而把它覆盖。
完整源码(docs/demo)
以下源码按组件从父到子排列,用 vitepress snippet(<<< @/…)直接引入
docs/demo 的真实文件。
Demo(父组件)
import ChildCard from './components/ChildCard'
import GlobalCard from './components/GlobalCard'
import InheritCard from './components/InheritCard'
// 父组件的外部 scoped scss(命名约定 *.scoped.scss)——hash 由 demo.tsx 的绝对路径生成
import './demo.scoped.scss'
// 全局样式(不带 scoped 后缀):由父组件引入,选择器不改写、全站生效。
// 对照看:它其实会命中本页所有 .card,但带 scoped 的卡被各自更高特异性的
// .card[data-v-*] 覆盖;只有不带任何 scoped 身份的 GlobalCard 会显示它。
import './global.css'
export default function Demo() {
return (
<section className="demo">
<h2 className="demo__title">同名 .card:scoped / 继承 / 全局</h2>
<div className="demo__row">
{/* 卡片 1:结构直接写在父组件里 —— 命中父的 .card[data-v-父hash] */}
<div className="card">
<p className="card__name">1. 父组件内直写 card</p>
<p className="card__desc">
结构写在 demo.tsx 中,DOM 带父组件 hash;样式来自父的 demo.scoped.scss
里同名 .card,只作用于本文件。
</p>
</div>
{/* 卡片 2/3/4:子组件。父组件会在大写组件标签上注入 scopedId="data-v-<父hash>",
是否使用(绑定到自己的 DOM)由子组件决定 */}
<ChildCard />
<InheritCard />
<GlobalCard />
</div>
</section>
)
}$card-radius: 12px;
$brand: #4f46e5;
.demo {
max-width: 760px;
padding: 24px;
border: 1px solid rgba($brand, 0.25);
border-radius: 16px;
background: #ffffff;
&__title {
margin: 0 0 16px;
color: $brand;
font-size: 18px;
font-weight: 800;
}
&__row {
display: flex;
gap: 14px;
flex-wrap: wrap;
margin-top: 16px;
}
}
// 父组件自己的 .card:编译后为 .card[data-v-父hash],
// 只命中带父 hash 的 DOM(卡片 1 与手动绑定了父 scopedId 的卡片 3)。
.card {
width: 210px;
padding: 14px;
border-radius: $card-radius;
border: 2px solid rgba($brand, 0.4);
background: #eef2ff;
&__name {
margin: 0 0 6px;
color: $brand;
font-weight: 700;
}
&__desc {
margin: 0;
color: #475569;
font-size: 12px;
line-height: 1.7;
}
}/* global.css —— 不带 scoped 后缀的普通全局样式:
选择器不会被追加 [data-v-*],规则全站生效(同名类都会被命中)。 */
.card {
width: 210px;
padding: 14px;
border-radius: 12px;
border: 2px dashed rgba(234, 88, 12, 0.5);
background: #fff7ed;
}
.card__name {
margin: 0 0 6px;
color: #ea580c;
font-weight: 700;
}
.card__desc {
margin: 0;
color: #475569;
font-size: 12px;
line-height: 1.7;
}卡片 1 的结构直接写在父组件里:它和页面上的其它 DOM 一样带上父组件 hash,
命中父的 .card[data-v-父hash];父的 scoped 样式到此为止,进不了任何子组件。
父组件还同时 import 了不带 scoped 后缀的 global.css(全局对照,见下方
GlobalCard)。
ChildCard(子组件 · 同名 .card + 自带 scoped)
import './child-card.scoped.css'
export default function ChildCard() {
return (
// 类名与父组件相同(.card),但 hash 属于本文件 —— 同名类各自隔离
<div className="card">
<p className="card__name">2. ChildCard 自带 scoped</p>
<p className="card__desc">
同名 .card,样式来自本组件的 child-card.scoped.css
(.card[data-v-子hash]),父的 .card 规则命中不了这里。
</p>
</div>
)
}/* 与父组件同名的 .card:作用域属于 ChildCard.tsx(data-v-子hash),
与父组件 .card[data-v-父hash] 互不干扰 —— 同名类各自隔离 */
.card {
width: 210px;
padding: 14px;
border-radius: 12px;
border: 2px solid rgba(34, 197, 94, 0.4);
background: #f0fdf4;
}
.card__name {
margin: 0 0 6px;
color: #16a34a;
font-weight: 700;
}
.card__desc {
margin: 0;
color: #475569;
font-size: 12px;
line-height: 1.7;
}与父同名 .card,但样式来自本组件自己的 scoped 文件
(.card[data-v-子hash]):两条规则各自命中自己文件产生的 DOM,同名类互不污染
—— 父的样式命中不了这张卡,这张卡的样式也影响不到父组件。
InheritCard(子组件 · 同名 .card + 手动继承父 scopedId)
interface InheritCardProps {
/** 组件 scoped:父组件自动注入 scopedId="data-v-<父hash>" */
scopedId?: string
}
export default function InheritCard({ scopedId }: InheritCardProps) {
// 需要被父 scoped 样式命中的每个标签都带上父级 scopedId(逐层绑定原因见
// 示例页 InheritCard 小节里的 ::: tip 警示)
const inherit = scopedId ? { [scopedId]: '' } : {}
return (
<div className="card" {...inherit}>
<p className="card__name" {...inherit}>3. InheritCard 全元素绑定父 scopedId</p>
<p className="card__desc" {...inherit}>
每个标签都带上父级 data-v,父的 .card__name / .card__desc 等内层规则也能命中。
</p>
</div>
)
}与父同名 .card,且没有自己的 scoped 文件;想让父的 scoped 样式命中自己,
就把父组件注入的 scopedId 绑到自己的 DOM 上(继承父组件的外观)。
警示:继承需要逐层绑定
组件 scoped 下,父样式会编译成带 [data-v-父hash] 的选择器。只把 scopedId
绑到根元素时,只有根元素继承父样式:父样式里形如 .card__name /
.card__desc 的内层选择器(编译后 .card__name[data-v-父])命中不了不带该
属性的子元素。因此要把 scopedId 展开到每一个希望被父样式命中的标签上
(结构有多少层,就需要继承多少层)。
GlobalCard(父组件中渲染 · 同名 .card · 只吃全局样式)
export default function GlobalCard() {
return (
// 与其它卡片同名的 .card,但没有任何 scoped 规则命中它:
// - 没有自己的 *.scoped.* 文件(本组件的 hash 上没有对应规则);
// - 也没有把父组件注入的 scopedId 绑到 DOM 上(不继承父样式)。
// 于是能命中它的只剩父组件引入的全局 global.css(选择器不带 [data-v-*])。
<div className="card">
<p className="card__name">4. GlobalCard(global.css · 无 scoped)</p>
<p className="card__desc">
global.css 不带 scoped 后缀:选择器不改写、全站生效 —— 别处出现同名 .card
也会被它影响;上面三张卡因 [data-v-*] 特异性更高而把它覆盖。
</p>
</div>
)
}第四张卡同样由父组件渲染、类名依然是 .card,但它既没有自己的 scoped 文件,
也没有绑定父 scopedId —— 能命中它的只剩父组件引入的global.css:
不带 scoped 后缀的选择器不会被改写,全站生效。对照即见差异:这份全局规则
其实也命中了前三张卡,但它们各自带更高特异性的 [data-v-*] 规则(
.card[data-v-父] / .card[data-v-子]),把全局样式覆盖掉了;只有 GlobalCard
没有任何 scoped 身份,全局样式才最终可见。站点别处若出现同名 .card,同样会
被 global.css 影响 —— 这正是 scoped 要防的“污染”。