本来为了编译后的css体积大小,我更喜欢写extend胜过mixin,但这次我发现要保证复杂嵌套时各种意义上的正确性,还得是mixin。并且官网更推荐语义相关的情况使用extend,显然我遇到问题的这种情况不属于推荐使用extend的情况(重点:官网提到“大多数web服务器算法擅长压缩CSS相同文本的重复块,尽管mixins可能比extends生成更多的CSS,但可能不会显著增加用户需要下载的内容。所以选择对你的用例最有意义的特性,而不是生成最少CSS的特性!”【详情见下“复习@extend用法“】)
问题概览
- 背景:
vue3项目中 <RewardItem>组件是封装好的,内部根元素有item类,父组件调用该组件时绑定一个动态样式isCrystal ,现在有多个位置都会需要在水晶类型的时候往 <RewardItem>里加一个标签,但不至于加到组件内(毕竟只是活动限定)
- 尝试并发现问题: 此时使用
extend+伪元素做角标,尝试让各个组件继承伪元素时就会发现继承伪元素失败
- 重点总结: 下面会提到官方建议的
extend 和 mixin 使用场景,实在不必为了节省编译后的css体积而盲目偏爱使用 extend ,这里实际应该是用mixin 来处理角标复用,不仅可以完美避开伪元素继承失败的问题,还可以顺便通过传参处理阿语样式相反
- 具体问题场景还原见下“伪元素问题”

复习下 extend / mixin / 公共样式 的区别
回顾下sass里@extend、@mixin、公共样式(公共 class / 全局样式)这三种方式,都能「复用样式」,但它们的 编译机制、作用范围和使用场景完全不同。
已知<RewardItem :class="{ isCrystal: type === 12 }" />,且RewardItem内部根元素有class="item",带着这个前提看看不同的用法
@extend(选择器继承)
官网教你不同场景使用extend还是mixin
- 官网 对于选择
extend还是mixin有很明确的建议:
根据经验法则,当您表达语义类(或其他语义选择器)之间的关系时,扩展是最佳选择。因为具有类 .error--serious 的元素 是 一个错误,所以它扩展 .error 是有意义的。但是对于非语义的样式集合,编写 mixin 可以避免级联问题,并使其更容易在以后进行配置
- 官方案例里用到
extend的是.error和.error--serious,这就非常明显了(这不摆明了说我这种bug的情况如果按照推荐来分流去用mixin写就不会遇到了吗w-w)1 2 3 4 5 6 7 8 9
| .error { border: 1px #f00; background-color: #fdd;
&--serious { @extend .error; border-width: 3px; } }
|
注意:不必为了css体积而偏向使用extend
- 按照官方网站的说法,我们并不需要为了缩小css体积而使用
extend!
💡 有趣的事实:
大多数web服务器使用一种算法压缩它们所提供的CSS,这种算法非常擅长处理相同文本的重复块。这意味着,尽管mixins可能比extends生成更多的CSS,但它们可能不会显著增加用户需要下载的内容。所以选择对你的用例最有意义的特性,而不是生成最少CSS的特性!
语法
1 2 3
| .isCrystal { @extend .crystal-tag; }
|
编译结果
Sass 会把 .isCrystal 和 .crystal-tag 合并成一个选择器组:
1 2 3 4
| .isCrystal, .crystal-tag { position: relative; ... }
|
特点
| 特性 |
说明 |
| ✅ 减少重复 |
样式只编译一次,多个类共享一份声明。 |
| ⚠️ 强耦合 |
.isCrystal 会和 .crystal-tag 永远绑定在一起,任何地方修改 .crystal-tag 都会影响它。 |
| ⚠️ scoped 限制 |
在 Vue <style scoped> 下可能导致伪元素或嵌套选择器不生效。 |
| ⚠️ 调试困难 |
编译后的选择器组很长,不容易看清来源。 |
适用场景
- 语义化的使用
- 小范围组件内部样式复用
- 不涉及 伪类 / 伪元素 的地方
@mixin(样式片段复用)
语法
1 2 3 4 5 6 7 8 9 10 11 12
| @mixin crystal-tag { position: relative;
&::after { content: attr(data-text); color: #fff; } }
.isCrystal { @include crystal-tag; }
|
编译结果
编译时会直接复制内容到使用的地方:
1 2 3 4 5 6 7
| .isCrystal { position: relative; } .isCrystal::after { content: attr(data-text); color: #fff; }
|
特点
| 特性 |
说明 |
| ✅ 灵活可参数化 |
可以接受参数:@include crystal-tag($color: red); |
| ✅ 不受 scoped 限制 |
因为内容是直接展开的。 |
| ⚠️ 可能膨胀 CSS 体积 |
每次 include 都会生成一份完整样式。 |
| ✅ 最稳定、最常用 |
Vue 项目里推荐方式。 |
适用场景
- 可复用的样式片段(按钮、标签、卡片边框)。
- 需要带参数的样式逻辑。
- 有伪元素 / 动画的场景(比 extend 稳定)。
公共样式(全局类 / 公共文件)
写法
1 2 3 4 5 6 7 8
| .isCrystal { position: relative;
&::after { content: attr(data-text); color: #fff; } }
|
在组件中直接使用:
1
| <RewardItem class="isCrystal" />
|
特点
| 特性 |
说明 |
| ✅ 简单直观 |
无需 Sass 特性,直接靠 class 组合。 |
| ✅ 全局复用 |
所有组件都能使用。 |
| ✅ 与 scoped 兼容 |
不受限制。 |
| ⚠️ 命名需规范 |
容易类名冲突。 |
| ⚠️ 可读性依赖命名规范 |
样式与结构分离较远。 |
适用场景
- 样式完全一致的通用模块(按钮、标签、卡片)。
- 有伪元素、动画或跨组件效果。
总结对比表
| 特性 |
@extend |
@mixin |
公共样式(class) |
| 原理 |
合并选择器 |
复制样式 |
通过类名复用 |
| 是否重复生成 CSS |
否 |
是 |
否 |
| 可参数化 |
❌ |
✅ |
❌ |
| scoped 兼容性 |
⚠️ 差 |
✅ 好 |
✅ 好 |
| 优先级控制 |
不可控 |
可控 |
可控 |
| 调试/维护 |
一般 |
简单 |
简单 |
| 适用场景 |
小范围复用 |
通用组件逻辑复用 |
全局样式统一 |
总结建议
| 目标 |
推荐方式 |
| 样式完全一致(无逻辑) |
公共 class |
| 样式逻辑复用、有参数 |
@mixin |
| 小块内部共享(无伪元素) |
@extend(次选) |
回头结合现在的使用场景(需要 ::after 做标签,多个组件复用),最佳实践是:用 mixin 或公共 class,不用 extend
但是我想回顾的是我失败的extend以及失败原因,所以继续/(ㄒoㄒ)/~~
强行 extend 带来的伪元素问题
- 已知: 父组件中有普通的透传控制子组件内嵌套多层的样式,包括
img的宽高1.36rem
- 期望: 给
特殊子组件(水晶) 加样式覆盖 普通透传样式,比如img改成宽高1.05rem,再加一个伪元素做角标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <!-- 父组件 --> <div class="rewards"> <RewardItem v-for="(prize, index) in prizes" :key="index" :row="prize" :data-text="dic.upTo" :class="{ isAr, isCrystal: prize.prizeType === 12, }" /> </div>
<style lang="scss" scoped> @import '@/style/base';
.rewards { :deep(.item) { .content img { width: 1.36rem; height: 1.36rem; } p { font-size: 0.29rem; }
} }
|
1 2 3 4 5 6 7
| <!-- RewardItem --> <div class="item"> <div class="content"> <img> </div> </div>
|
extend 失败!
- 在
extend和mixin里我潜意识就先选择了extend(习惯性觉得应该节省css体积,卒)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| .crystal-reward { .content img { width: 1.05rem; height: 1.05rem; } position: relative; &::after { position: absolute; top: 0.05rem; right: 0.05rem; display: inline-flex; align-items: center; justify-content: center; padding: 0 0.1rem; height: 0.36rem; background: linear-gradient(180deg, #ff6fe7, #ec0f94); border: 0.02rem solid #fefe2c; border-radius: 0 0.1rem; content: attr(data-text); font-weight: 600; font-size: 0.24rem; line-height: 0.24rem; color: #ffffff; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <div class="rewards"> <RewardItem :class="{ isCrystal: prize.prizeType === 12, }" /> </div>
<style lang="scss" scoped> @import '@/style/base';
.rewards { :deep(.item) { .content img { width: 1.36rem; height: 1.36rem; } p { font-size: 0.29rem; } &.isCrystal { @extend .crystal-reward; } } } </style>
|
结果: 可以看到1.05rem宽高是生效的,但是伪元素并未成功绑定isCrystal
查!伪元素绑到哪儿了
- 通过
全局样式直接绑定准备继承的样式crystal-extend来“钓”出isCrystal上应该绑定的伪元素到底绑到哪儿了,很明显这样做伪元素能成功绑定(当然全局样式试图覆盖透传item的样式不生效,优先级的问题这里先不管)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <div class="rewards"> <RewardItem :class="{ isCrystal: prize.prizeType === 12, ['crystal-extend']: prize.prizeType === 12, }" /> </div>
<style lang="scss" scoped> @import '@/style/base';
.rewards { :deep(.item) { .content img { width: 1.36rem; height: 1.36rem; } p { font-size: 0.29rem; } &.isCrystal { @extend .crystal-reward; } } } </style>
|
- 能在f12选中绑定成功的伪元素tag以此看到其他被继承的位置,很明显第一行是生效的,第二行说明编译后的
isCrystal继承有问题导致之前看到的伪元素绑定失败
item和isCrystal这俩 同元素 不同class被识别成 父子关系 了! 能绑定上才有鬼
1 2 3
| .crystal-extend[data-v-af4d0009]::after, .rewardList .rewards .isCrystal[data-v-af4d0009] .item::after { 伪元素 }
|
- 虽然依旧不知道为啥会这样,但是明显sass对于
extend和伪元素的混合处理有点问题,官方也不建议这种非语义相关的情况使用extend,那么问题看到这我也不再纠结为什么会产生这样的错位问题了,老老实实用mixin吧
StackOverflow 提到
- 当
extend伪类/伪元素时,你extend的是基础选择器而不是单独的伪部分:即使你在定义里写了伪元素,extend时也不会自动在所有继承者上创建对应的伪元素
mixin 成功!
- mixin完全没问题,甚至可以传参去控制下
left和right解决下阿语样式角标放左上角的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @mixin crystal-reward() { .content img { width: 1.05rem; height: 1.05rem; } position: relative; &::after { position: absolute; top: 0.05rem; right: 0.05rem; display: inline-flex; align-items: center; justify-content: center; padding: 0 0.1rem; height: 0.36rem; background: linear-gradient(180deg, #ff6fe7, #ec0f94); border: 0.02rem solid #fefe2c; border-radius: 0 0.1rem; content: attr(data-text); font-weight: 600; font-size: 0.24rem; line-height: 0.24rem; color: #fff; } }
|
1 2 3
| &.isCrystal { @include crystal-reward(); }
|