Sass 混合宏、继承与函数

Sass 提供了三种代码复用机制:混合宏(Mixin)继承/占位符(@extend / %placeholder)函数(@function)。理解它们的区别和适用场景,是写出高质量、可维护 SCSS 的关键。

一、@mixin 和 @include

基本用法

Mixin 将一组 CSS 声明封装为一个可复用的块,通过 @include 引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}

.modal-overlay {
@include flex-center;
position: fixed;
inset: 0;
}

.card {
@include flex-center;
flex-direction: column; // 在 mixin 基础上补充
}

带参数的 Mixin

1
2
3
4
5
6
7
8
9
10
11
12
@mixin size($width, $height: $width) {
width: $width;
height: $height;
}

.avatar {
@include size(40px); // 40x40 正方形
}

.cover {
@include size(100%, 200px); // 拉伸
}

关键字参数

参数较多时使用关键字参数,可读性更好,且无需按顺序传参:

1
2
3
4
5
6
7
8
9
10
11
12
@mixin card($bg: white, $radius: 4px, $shadow: 0 2px 4px rgba(0,0,0,.1)) {
background: $bg;
border-radius: $radius;
box-shadow: $shadow;
}

.promotion-card {
@include card(
$bg: linear-gradient(135deg, #667eea, #764ba2),
$shadow: 0 4px 15px rgba(102, 126, 234, .4)
);
}

可变参数(…)

当参数数量不确定时,使用 ... 收集多个值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@mixin box-shadow($shadows...) {
box-shadow: $shadows;
}

.card {
@include box-shadow(
0 1px 3px rgba(0,0,0,.1),
0 1px 2px rgba(0,0,0,.06)
);
}

// 也可以将列表展开传入
$shadows: (0 1px 3px rgba(0,0,0,.1), 0 1px 2px rgba(0,0,0,.06));
.card {
@include box-shadow($shadows...);
}

@content — 内容块传递

Mixin 可以接收一段自定义样式块,在内部通过 @content 插入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@mixin respond-to($breakpoint) {
@if $breakpoint == mobile {
@media (max-width: 767px) { @content; }
} @else if $breakpoint == tablet {
@media (min-width: 768px) and (max-width: 1023px) { @content; }
} @else if $breakpoint == desktop {
@media (min-width: 1024px) { @content; }
}
}

.sidebar {
width: 300px;

@include respond-to(mobile) {
width: 100%; // 此段样式通过 @content 注入到 @media 中
display: none;
}
}

编译结果:

1
2
3
4
.sidebar { width: 300px; }
@media (max-width: 767px) {
.sidebar { width: 100%; display: none; }
}

@content 多次使用

可以在一个 mixin 中使用多次 @content,每次传入不同的内容块:

1
2
3
4
5
6
7
8
9
10
@mixin hover-active {
&:hover { @content; }
&:active { @content; }
}

.btn {
@include hover-active {
opacity: 0.8;
}
}

二、@extend 和 %placeholder

@extend 的基本用法

@extend 让一个选择器继承另一个选择器的样式——编译时会通过选择器分组(,)来实现:

1
2
3
4
5
6
7
8
9
.error {
border: 1px solid red;
color: red;
}

.serious-error {
@extend .error;
border-width: 3px;
}

编译结果:

1
2
.error, .serious-error { border: 1px solid red; color: red; }
.serious-error { border-width: 3px; }

%placeholder 占位符

%placeholder 定义只用于继承的样式块——它本身不会生成任何 CSS,只在被 @extend 时才输出:

1
2
3
4
5
6
7
8
%ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.title { @extend %ellipsis; width: 200px; }
.desc { @extend %ellipsis; width: 100%; }

编译结果:

1
2
3
.title, .desc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.title { width: 200px; }
.desc { width: 100%; }

@extend 嵌套选择器

@extend 可以继承一个复杂选择器:

1
2
3
4
5
6
7
8
9
.icon {
transition: transform .2s;
.dark & { filter: brightness(.8); }
}

.close-icon {
@extend .icon;
font-size: 20px;
}

编译结果中,.dark .icon 的规则也会被复制为 .dark .close-icon

@extend 的陷阱

  1. 不能跨 @media@extend 只能在同层使用,媒体查询内外不可互相 extend。
1
2
3
4
5
// ❌ 编译错误
.base { color: red; }
@media screen {
.derived { @extend .base; }
}
  1. 可能产生意外的选择器分组:当 .a extend 了 .b,所有用到 .b 的规则都会带上 .a。在大项目中这可能造成 CSS 体积膨胀和意外覆盖。

  2. 不要 extend 未定义在项目中的第三方 class:会导致第三方样式也被连带引入。

三、@mixin vs @extend 对比

对比维度@mixin@extend + %placeholder
输出方式每次 @include 复制一份样式代码选择器分组,共用一份样式
CSS 体积较大(代码重复)较小(合并选择器)
传参能力✅ 支持参数❌ 不支持
逻辑能力✅ 可配合 @if/@for 等控制指令❌ 纯样式复用
跨媒体查询✅ 可以❌ 不可以
适用场景需要传参、带逻辑、有变化的复用无参数的纯粹样式复用,如 %clearfix%ellipsis

经验法则

  • 需要传参 → 用 @mixin
  • 需要在 media query 中使用 → 用 @mixin
  • 纯粹复用一组固定样式,不需要变化 → 用 %placeholder + @extend
  • 不确定时默认用 @mixin

四、@function 和 @return

函数与 mixin 的区别:mixin 输出样式块,函数返回一个值

1
2
3
4
5
6
7
8
@function px-to-rem($px, $base: 16px) {
@return math.div($px, $base) * 1rem;
}

.card {
padding: px-to-rem(16px); // 1rem
font-size: px-to-rem(14px); // 0.875rem
}

实用函数示例

1. z-index 层级管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$z-layers: (
modal: 1000,
dropdown: 100,
header: 10,
default: 1,
);

@function z($layer) {
@if map-has-key($z-layers, $layer) {
@return map-get($z-layers, $layer);
} @else {
@error "未知层级 #{$layer},可用值: #{map-keys($z-layers)}";
}
}

.modal { z-index: z(modal); } // 1000
.header { z-index: z(header); } // 10

2. 颜色对比度计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@function contrasting-color($bg) {
@if lightness($bg) > 50 {
@return #333;
} @else {
@return #fff;
}
}

.dark-bg {
background: #333;
color: contrasting-color(#333); // #fff
}

.light-bg {
background: #eee;
color: contrasting-color(#eee); // #333
}

内置函数概览

Sass 提供了丰富的内置函数,全部可通过 @use 'sass:*' 使用:

数学函数

1
2
3
4
5
6
7
8
9
10
@use 'sass:math';

math.ceil(4.2); // 5
math.floor(4.8); // 4
math.round(4.5); // 5
math.min(1, 3, 2); // 1
math.max(1, 3, 2); // 3
math.random(); // 0~1 随机数
math.random(100); // 1~100 随机整数
math.div(10, 3); // 3.33333(推荐写法,避免 / 歧义)

字符串函数

1
2
3
4
5
6
7
8
@use 'sass:string';

string.to-upper-case('hello'); // "HELLO"
string.to-lower-case('WORLD'); // "world"
string.length('hello'); // 5
string.slice('hello', 2, 4); // "ell"(索引从 1 开始)
string.insert('hello', 'xx', 3); // "hexxllo"
string.index('hello', 'l'); // 3(第一个匹配位置)

颜色函数

1
2
3
4
5
6
7
8
9
@use 'sass:color';

color.adjust(#409eff, $lightness: 10%); // 调亮
color.adjust(#409eff, $lightness: -10%); // 调暗
color.adjust(#409eff, $alpha: -0.3); // 调透明

color.mix(#409eff, #67c23a, 50%); // 混合两种颜色
color.complement(#409eff); // 补色
color.grayscale(#409eff); // 灰度化

历史背景:旧版 Sass 中 lighten($color, 10%)darken() 等是全局函数;Dart Sass 将它们移入 sass:color 模块。虽然全局调用仍然兼容,建议新代码通过 @use 'sass:color' 访问。

列表函数

1
2
3
4
5
6
7
8
9
@use 'sass:list';

list.nth((a b c), 2); // b(索引从 1 开始)
list.length((a b c)); // 3
list.index((a b c), b); // 2
list.append((a b), c); // a b c
list.join((a b), (c d)); // a b c d
list.separator((a b)); // space
list.is-bracketed([a b]); // true

Map 函数

1
2
3
4
5
6
7
8
9
@use 'sass:map';

map.get((a: 1, b: 2), a); // 1
map.has-key((a: 1), a); // true
map.keys((a: 1, b: 2)); // a, b
map.values((a: 1, b: 2)); // 1, 2
map.merge((a: 1), (b: 2)); // (a: 1, b: 2)
map.remove((a: 1, b: 2), a); // (b: 2)
map.deep-merge((a: (x: 1)), (a: (y:2))); // (a: (x:1, y:2)) 深合并

选择器函数

1
2
3
4
5
6
@use 'sass:selector';

selector.nest('.a', '.b'); // .a .b
selector.append('.a', '.b'); // .a.b
selector.replace('.a .b', '.b', '.c'); // .a .c
selector.parse('.a > .b'); // 解析为选择器列表

五、插值 #{}

插值允许在 CSS 选择器、属性名、属性值中使用 SassScript 表达式:

选择器插值

1
2
3
4
$prefix: 'el';

.#{$prefix}-button { ... } // .el-button
.#{$prefix}-input { ... } // .el-input

属性名插值

1
2
3
4
5
6
7
8
9
10
$direction: 'margin';

.#{$direction}-top { ... } // 允许动态生成属性名

// 更实用的场景:生成 flex 栅格
@for $i from 1 through 12 {
.col-span-#{$i} {
grid-column: span $i;
}
}

属性值插值

1
2
3
4
5
$icon: '★';

.icon-star::before {
content: '#{$icon}'; // ★
}

在注释中插值

1
2
3
$version: '1.0.0';

/*! Sass 版本 #{$version} */ // 编译后保留:/*! Sass 版本 1.0.0 */

在 @mixin 和 @function 名称中插值

1
2
3
4
5
6
7
8
9
10
@mixin color-modifier($color) {
&--#{$color} {
border-color: $color;
}
}

.card {
@include color-modifier(primary); // .card--primary
@include color-modifier(success); // .card--success
}

六、选择建议总结

需求方案
复用一段无参的静态样式%placeholder + @extend
复用带参数、逻辑的样式@mixin + @include
计算并返回一个值(px→rem、z-index)@function + @return
条件渲染@if / @else 配合 mixin 或 function
批量生成类名@for@each 配合插值 #{}
动态拼接选择器/属性名插值 #{}