第二章:编写可复用的模式:@mixin 与 @extend 的战略

第二章:编写可复用的模式:@mixin@extend 的战略

摘要: 在上一章中,我们掌握了 SCSS 的核心语法与模块化组织方式。现在,我们将进入一个更高级的领域:代码复用。本章将深度剖析 SCSS 中两种强大的代码复用机制——混合宏 (@mixin)继承 (@extend)。我们将不仅仅学习它们的语法,更重要的是,通过深度的对比与实战,理解它们在编译结果、性能和维护性上的本质区别,并最终形成一套符合 2025 年架构思想的战略选型方案。


在上一章中,我们已经搭建好了清晰的 7-1 Pattern 项目结构。本章我们将要学习的 @mixin 等抽象工具,其最佳实践位置正是在 scss/abstracts/ 目录下。

1
2
3
4
5
6
7
8
9
10
scss/
├── abstracts/
│ ├── _variables.scss
│ └── _mixins.scss # <-- 本章的核心代码将主要存放于此
├── base/
│ ...
├── components/
│ ├── _button.scss # <-- 我们将在这里调用 Mixin
│ └── _card.scss
└── main.scss

2.1. 混合宏 (@mixin):动态的样式“函数”

`@mixin` 是 SCSS 中最重要、最灵活的代码复用机制。您可以将它理解为一个可以重复调用的“样式函数”,它能够接收参数,并动态生成样式代码。

2.1.1. 定义 (@mixin) 与调用 (@include)

痛点背景: 在 CSS 中,我们经常需要编写一些重复的样式片段,比如清除浮动、处理浏览器厂商前缀等。每次都手动复制粘贴不仅效率低下,而且难以维护。

解决方案: 使用 @mixin 将这些重复的样式块封装起来,在需要的地方通过 @include 调用。

文件路径: scss/abstracts/_mixins.scss

1
2
3
4
5
6
7
8
// 定义一个清除浮动的 mixin
@mixin clearfix {
&::after {
content: "";
display: table;
clear: both;
}
}

文件路径: scss/components/_card.scss

1
2
3
4
5
6
7
8
9
@use '../abstracts/mixins' as m; // 导入 mixins 模块,并设置别名为 m

.card {
border: 1px solid #ccc;
// ... 其他样式

// 在这里调用 clearfix mixin
@include m.clearfix;
}

@include 的工作方式非常直观:它会将 @mixin 中的代码 原封不动地复制 到调用的位置。

2.1.2. 传递参数:默认值与任意参数 (...)

@mixin 的真正威力在于其接收参数的能力,这让我们的样式封装变得极具动态性。

痛点背景: 假设我们需要创建不同尺寸、不同颜色的按钮。如果为每一种组合都写一个类,代码会变得非常冗余。

解决方案: 创建一个灵活的 button-style mixin,通过传递参数来动态生成按钮样式。

文件路径: scss/abstracts/_mixins.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义一个强大的按钮样式 mixin
// $bg: 按钮背景色,默认为蓝色
// $color: 文本颜色,默认为白色
// $padding: 内边距,默认为 10px 20px
@mixin button-style($bg: #3498db, $color: #fff, $padding: 10px 20px) {
display: inline-block;
padding: $padding;
background-color: $bg;
color: $color;
border: none;
border-radius: 5px;
cursor: pointer;
text-align: center;
text-decoration: none;

&:hover {
// 使用 SCSS 内置的 darken 函数让背景色加深 10%
background-color: darken($bg, 10%);
}
}

文件路径: scss/components/_button.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@use '../abstracts/mixins' as m;

.btn-primary {
// 直接使用默认值
@include m.button-style;
}

.btn-danger {
// 传入参数,覆盖默认值
@include m.button-style($bg: #e74c3c);
}

.btn-large {
// 按顺序传入多个参数
@include m.button-style(#2ecc71, #fff, 15px 30px);
}

.btn-special {
// 使用命名参数,无需关心顺序
@include m.button-style($padding: 8px 16px, $bg: #9b59b6);
}

我们可以在 index.html 中引入我们编译好的 main.css,在页面上看到效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="scss/main.css">
</head>
<body>
<button class="btn-primary">按钮</button>
<button class="btn-danger">按钮</button>
<button class="btn-large">按钮</button>
<button class="btn-special">按钮</button>
</body>
</html>

2.1.3. 传递内容块 (@content):创建高阶混合宏

@content 指令允许我们在调用 @mixin 时,向其内部传递一整个样式块。这在创建用于媒体查询、状态嵌套等场景的“包装型” mixin 时极为有用。

痛点背景: 在编写响应式样式时,我们常常需要将同一个组件的样式分散在多个 @media 查询块中,这破坏了组件样式的内聚性。

解决方案: 创建一个响应式断点的 mixin,使用 @content 将特定断点的样式包裹起来。

文件路径: scss/abstracts/_mixins.scss

1
2
3
4
5
6
// 定义一个用于处理小于指定宽度的媒体查询 mixin
@mixin screen-below($breakpoint) {
@media (max-width: $breakpoint) {
@content; // 将传递进来的样式块放在这里
}
}

文件路径: scss/components/_card.scss

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@use "../abstracts/mixins" as m;

.card {
border: 1px solid #ccc;
@include m.clearfix;
width: 600px;
margin: 20px auto;
border-radius: 8px;

// 使用 screen-below mixin
// 当屏幕宽度小于 768px 时,应用内部的样式
@include m.screen-below(768px) {
width: 90%;
border-color: #e74c3c; // 边框变红色,效果更明显
background-color: #fdf2e8; // 背景色变化
}

&__header {
background-color: #3498db;
color: white;
padding: 20px;
text-align: center;
font-size: 24px;

@include m.screen-below(768px) {
background-color: #e74c3c; // 蓝色变红色
font-size: 18px; // 字体变小
}
}

&__body {
padding: 20px;
font-size: 16px;
line-height: 1.5;

@include m.screen-below(768px) {
background-color: #f9f9f9; // 小屏幕时添加背景色
font-size: 14px; // 字体变小
}
}

&__footer {
background-color: #95a5a6;
color: white;
padding: 15px 20px;
text-align: center;

@include m.screen-below(768px) {
background-color: #f39c12; // 灰色变橙色
font-weight: bold;
}
}
}

我们可以在 index.html 中引入我们编译好的 main.css,在页面上看到效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="scss/main.css">
</head>
<body>
<div class="card">
<div class="card__header">
<h1>卡片标题</h1>
</div>
<div class="card__body">
<p>卡片内容</p>
</div>
<div class="card__footer">
<button class="btn-primary">按钮</button>
</div>
</div>
</body>
</html>

2.2. 继承 (@extend):共享样式的选择器分组

`@extend` 是另一种代码复用机制,但它的工作原理与 `@mixin` 完全不同。它不是复制代码,而是让一个选择器去**继承**另一个选择器的所有样式,并通过 "选择器分组" 的方式来实现。

2.2.1. 基础继承语法

痛点背景: 在开发中,我们经常需要创建一系列相似的组件,它们共享基础样式但又有各自的特色。比如不同类型的提示框(成功、警告、错误、信息),它们的基本结构相同,只是颜色不同。如果每个都单独写样式,会产生大量重复代码。

解决方案: 使用 @extend 让多个选择器继承同一个基础选择器的样式,SCSS 会智能地将它们组合成选择器组,避免代码重复。

文件路径: scss/components/_alert.scss (新增)

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
31
32
33
34
35
.alert {
padding: 15px;
border: 1px solid #ccc;
border-radius: 4px;
margin: 10px 0;
font-size: 14px;
}

.alert-success {
@extend .alert; // 继承 .alert 的所有样式
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
}

.alert-danger {
@extend .alert; // 继承 .alert 的所有样式
background-color: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}

.alert-warning {
@extend .alert; // 继承 .alert 的所有样式
background-color: #fff3cd;
border-color: #ffeaa7;
color: #856404;
}

.alert-info {
@extend .alert; // 继承 .alert 的所有样式
background-color: #d1ecf1;
border-color: #bee5eb;
color: #0c5460;
}

文件路径: scss/main.scss

1
2
// 在 main.scss 中添加导入
@use 'components/alert';

@extend 的工作方式:SCSS 会将所有继承同一个选择器的类组合成一个 选择器组,基础样式只出现一次,这样既避免了代码重复,又保持了 CSS 的简洁性。

我们可以在 index.html 中测试这些 alert 组件的效果:

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
31
32
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SCSS @extend 继承演示</title>
<link rel="stylesheet" href="scss/main.css">
</head>
<body>
<!-- @extend 继承示例 -->
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h2>Alert 组件继承示例</h2>
<p>以下四个 alert 都通过 <code>@extend .alert</code> 继承了基础样式:</p>

<div class="alert-success">
✅ 成功信息 - 继承了 .alert 的 padding、border、border-radius 等样式
</div>

<div class="alert-danger">
❌ 危险信息 - 继承了 .alert 的 padding、border、border-radius 等样式
</div>

<div class="alert-warning">
⚠️ 警告信息 - 继承了 .alert 的 padding、border、border-radius 等样式
</div>

<div class="alert-info">
ℹ️ 提示信息 - 继承了 .alert 的 padding、border、border-radius 等样式
</div>
</div>
</body>
</html>

注意: @extend 会将调用者的选择器(如 .alert-success)“附加” 到被继承选择器(.alert)出现的所有地方,这可能导致意料之外的样式级联问题,并使编译后的 CSS 选择器关系变得复杂。因此,直接继承一个具体的类(.class)被认为是一种 高风险 的做法。


2.3. 占位符选择器 (%):最佳实践

为了解决 @extend 的滥用风险,SCSS 提供了一种特殊的选择器:占位符选择器(%placeholder)。

核心特性:

  1. % 开头。
  2. 它本身 不会 被编译到 CSS 文件中,除非它被 @extend
  3. 它就像一个 “虚拟” 的规则集,专门用于被继承。

这使得 @extend 变得安全可控,因为它不会污染全局的类选择器。

2.3.1. 对比普通类选择器 vs 占位符选择器

痛点背景: 在之前的例子中,我们使用 .alert 作为基础类,但这意味着即使我们不需要单独使用 .alert,它也会出现在编译后的 CSS 中,增加了文件大小,并可能在 HTML 中被误用。

解决方案: 使用占位符选择器 %alert-base 替代普通的 .alert 类,它只在被继承时才会生成 CSS 代码。

使用普通类选择器(不推荐)

文件路径: scss/components/_alert.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.alert {
padding: 15px;
border: 1px solid #ccc;
border-radius: 4px;
margin: 10px 0;
font-size: 14px;
}

.alert-success {
@extend .alert;
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
}

使用占位符选择器(推荐)

文件路径: scss/components/_alert.scss

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
31
32
33
34
35
36
// 使用占位符选择器定义基础样式
%alert-base {
padding: 15px;
border: 1px solid #ccc;
border-radius: 4px;
margin: 10px 0;
font-size: 14px;
}

.alert-success {
@extend %alert-base; // 继承占位符选择器
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
}

.alert-danger {
@extend %alert-base; // 继承占位符选择器
background-color: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}

.alert-warning {
@extend %alert-base; // 继承占位符选择器
background-color: #fff3cd;
border-color: #ffeaa7;
color: #856404;
}

.alert-info {
@extend %alert-base; // 继承占位符选择器
background-color: #d1ecf1;
border-color: #bee5eb;
color: #0c5460;
}

最佳实践: 始终使用占位符选择器 (%placeholder) 配合 @extend,而不是直接继承普通的类选择器。这样可以确保你的 CSS 更加精简、安全,并且不会产生意外的副作用。


2.4. 核心战略抉择:

现在我们已经了解了两种复用机制,是时候进行一场终极对决了。理解它们的本质区别,是成为 SCSS 高手的关键。

@mixin@extend 最核心的区别在于它们编译后的产物。

SCSS 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用 @mixin
@mixin base-style {
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.widget { @include base-style; }
.panel { @include base-style; }

// 使用 @extend 与占位符
%base-style {
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.widget { @extend %base-style; }
.panel { @extend %base-style; }

@mixin 的编译结果:

1
2
3
4
5
6
7
8
.widget {
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.panel {
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

@extend 的编译结果 (选择器分组):

1
2
3
4
.widget, .panel {
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
特性@mixin@extend
核心机制代码复制选择器分组
优点1. 接收参数,高度灵活
2. 安全可预测,不改变选择器结构
3. 清晰独立,每个规则集都是完整的
1. 极致压缩,生成的 CSS 文件体积更小
2. 语义关联,将被继承的元素在逻辑上关联起来
缺点1. 代码冗余,可能导致最终的 CSS 文件体积较大
2. 性能 (在 Gzip 压缩下,体积差异通常不显著)
1. 无法传参,只能继承静态样式块
2. 破坏源顺序,所有被继承的选择器会被提到文件的最前面
3. 级联风险,可能产生意料之外的样式关系
适用场景绝大多数场景:组件样式、工具类、响应式媒体查询、任何需要动态参数的复用。极少数场景:多个完全不相关的选择器需要共享一个 完全静态 的样式块,且压缩文件体积是首要目标。
  1. 默认并优先使用 @mixin。在现代前端工程中,Gzip 压缩已经极大地缓解了 @mixin 带来的代码冗余问题。而 @mixin 带来的 安全性、可预测性、可传参的灵活性 以及对 组件化思想的贴合,其价值远超 @extend 在体积上带来的微小优势。

  2. 仅在特定场景下,谨慎使用 @extend %placeholder。当你明确知道,有多个 毫无关联 的、分散在各处的选择器需要共享一个 完全静态(无需参数)的样式定义,并且你的项目对最终产出的 CSS 大小有着极其严苛的要求时,可以考虑使用 @extend 配合占位符。

  3. 永远不要 @extend .class。直接继承一个具体的类,是导致样式表混乱和难以维护的根源。请将这条视为团队的编码红线。

总而言之,将 @mixin 作为你的瑞士军刀,将 @extend %placeholder 视为一把仅在特殊情况下使用的手术刀。