第三章:解锁编程之力:用逻辑与函数生成 CSS

第三章:解锁编程之力:用逻辑与函数生成 CSS

摘要: 在前两章中,我们已经学会了如何编写、组织和复用样式。本章将是您从“SCSS 使用者”蜕变为“SCSS 开发者”的关键一步。我们将把 SCSS 真正作为一门编程语言来使用,深入学习其核心的数据结构(特别是 Map)、控制流指令@if, @for, @each)以及函数@function)。读完本章,您将有能力编写出能自我生成、智能适应的样式代码,将重复劳动降至最低,并构建出真正可扩展、系统化的样式系统。


3.1. 核心数据类型与结构

痛点背景: 当项目变大时,我们需要管理的“设计令牌”(Design Tokens)会越来越多,例如,一个品牌有多种主色、多种辅色;一套设计系统有固定的字号阶梯、外边距阶梯。如果将它们声明为几十个独立的 $variable,不仅难以管理,而且无法进行遍历等程序化操作。

解决方案: 使用 Map 来组织这些关联的数据,将它们结构化地存放在 _variables.scss 文件中。

文件路径: scss/abstracts/_variables.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
// 使用 Map 定义主题色
// 键 (key) 是颜色的语义化名称,值 (value) 是具体的色值
$theme-colors: (
"primary": #3498db,
"secondary": #95a5a6,
"success": #2ecc71,
"danger": #e74c3c,
"warning": #f1c40f,
"info": #3498db,
"light": #f8f9fa,
"dark": #343a40
);

// 使用 Map 定义间距系统
$spacings: (
"0": 0,
"1": 4px,
"2": 8px,
"3": 16px,
"4": 24px,
"5": 48px
);

// 使用 List 定义字体栈
$font-stack: Helvetica, sans-serif;

3.2. 控制流指令:让样式“活”起来

控制流指令让我们的 SCSS 代码拥有了判断和循环的能力,这是实现样式自动化的核心。

3.2.1. 实战:使用 @if 实现智能文本配色

痛点背景: 当我们创建一个背景颜色可变的组件(如标签、按钮)时,如何确保无论背景是深色还是浅色,文本颜色都能自动调整以保持清晰可读?

解决方案: 创建一个 @mixin,利用 SCSS 内置的 lightness() 函数获取颜色亮度,并通过 @if 指令判断,如果背景色是浅色(亮度 > 50%),则应用深色文本,反之则应用浅色文本。

文件路径: scss/abstracts/_mixins.scss (新增)

1
2
3
4
5
6
7
8
9
10
11
// 导入 sass 的内置颜色模块
@use "sass:color";

// 一个根据背景色自动设置文本颜色的 mixin
@mixin auto-text-color($bg-color) {
@if color.lightness($bg-color) > 50% {
color: #333; // 浅色背景 -> 深色文本
} @else {
color: #fff; // 深色背景 -> 浅色文本
}
}

关于 @use "sass:color":这是 2025 年 Dart Sass 的标准语法,用于导入官方的内置模块(如 color, math 等)。这里的 sass: 是必需的命名空间,它代表的是 Sass 这门技术,与旧的 .sass 缩进语法无关。这是 SCSS 现代化模块体系的一部分。

现在,我们创建一个徽章(Badge)组件来应用这个 mixin。

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

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

.badge {
display: inline-block;
padding: 4px 8px;
font-size: 12px;
border-radius: 4px;
}
.badge-light-yellow {
background-color: #f1c40f;
@include m.auto-text-color(#f1c40f);
}
.badge-dark-blue {
background-color: #2c3e50;
@include m.auto-text-color(#2c3e50);
}

文件路径: scss/main.scss (添加导入)

1
2
// ...
@use 'components/badge';

文件路径: index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SCSS @if 智能配色</title>
<link rel="stylesheet" href="scss/main.css">
</head>
<body style="padding: 20px;">
<h2>@if 智能配色徽章示例</h2>
<p><span class="badge badge-light-yellow">亮色背景 (Yellow)</span> &lt;-- SCSS 自动判断应使用深色文本</p>
<p><span class="badge badge-dark-blue">深色背景 (Blue)</span> &lt;-- SCSS 自动判断应使用浅色文本</p>
</body>
</html>

3.2.2. 循环指令 (@for, @while, @each)

SCSS 提供了三种循环指令,每种都有其特定的应用场景。

  • @for: 用于执行固定次数的循环,通常与数字索引相关。
  • @while: 在满足某个条件时持续执行循环,使用较少。
  • @each: 遍历 ListMap 中的每一项,是迄今为止最常用、最强大的循环指令。

@for 实战:快速生成栅格系统

痛点背景: 我们需要一套 12 列的栅格系统,包含从 .col-1.col-12 的类,手动编写这 12 个类非常重复。

解决方案: 使用 @for 循环,从 1 到 12 动态生成所有列的样式。

文件路径: scss/base/_grid.scss (新增)

1
2
3
4
5
6
7
8
9
10
11
// 导入 sass 的内置数学模块
@use "sass:math";

// 生成 12 列栅格系统
@for $i from 1 through 12 {
.col-#{$i} {
// 最简洁的写法:直接用 math.div()
// i / 12 * 100% 即可得到百分比
width: math.div($i, 12) * 100%;
}
}

文件路径: scss/main.scss (添加导入)

1
2
//...
@use 'base/grid';

@each 实战:批量生成工具类 (核心实战)

现在,我们回到本章的核心任务:使用最强大的 @each 循环,结合 Map 批量生成工具类。

痛点背景: 我们需要 .text-primary, .bg-primary, .text-danger 等全套工具类。手动编写不仅工作量巨大,而且每当设计系统增加新颜色,就必须手动补充,极易遗漏。

解决方案: 遍历 $theme-colors Map,自动生成所有颜色相关的工具类。

文件路径: scss/base/_utilities.scss (新增)

1
2
3
4
5
6
7
8
9
10
11
12
@use '../abstracts/variables' as v;
@use '../abstracts/mixins' as m;

@each $color-name, $color-value in v.$theme-colors {
.text-#{$color-name} {
color: $color-value;
}
.bg-#{$color-name} {
background-color: $color-value;
@include m.auto-text-color($color-value);
}
}

文件路径: scss/main.scss (添加导入)

1
2
// ...
@use 'base/utilities';

文件路径: index.html

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SCSS @each 生成工具类</title>
<link rel="stylesheet" href="scss/main.css">
<style> .demo-box { padding: 16px; margin-bottom: 10px; border-radius: 4px; border: 1px solid #ddd;} </style>
</head>
<body style="padding: 20px;">
<h2>@each + Map 自动生成的工具类</h2>
<div class="demo-box bg-light">
<p class="text-primary">这段文字是 primary 颜色。</p>
<p class="text-success">这段文字是 success 颜色。</p>
<p class="text-danger">这段文字是 danger 颜色。</p>
<p class="text-warning">这段文字是 warning 颜色。</p>
<p class="text-info">这段文字是 info 颜色。</p>
<p class="text-light">这段文字是 light 颜色。</p>
<p class="text-dark">这段文字是 dark 颜色。</p>
</div>
<div class="demo-box">
<span class="bg-warning">Warning Badge</span> |
<span class="bg-dark">Dark Badge</span>
<span class="bg-primary">Primary Badge</span>
<span class="bg-secondary">Secondary Badge</span>
<span class="bg-success">Success Badge</span>
<span class="bg-danger">Danger Badge</span>
<span class="bg-info">Info Badge</span>
<span class="bg-light">Light Badge</span>
<span class="bg-dark">Dark Badge</span>

</div>
</body>
</html>

3.3. 函数 (@function)

@mixin 用于输出样式块,而 @function 则用于计算并返回一个值。这是两者最核心的区别。

3.3.1. 实战:编写 pxrem 的函数

痛点背景: 在现代响应式设计中,我们推荐使用 rem 单位。但设计师提供的设计稿通常以 px 为单位,每次手动计算 px / 16 非常繁琐且容易出错。

解决方案: 编写一个通用的 px-to-rem 函数来自动化这个计算过程。

文件路径: scss/abstracts/_functions.scss (新增)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@use "sass:math";

// 基础字体大小,用于 rem 单位转换
$base-font-size: 16px;

/**
* @param {Number} $pixels - 要转换的像素值,可以是无单位数字或带 px 单位的值
* @return {Number} 转换后的 rem 值
* @example
* to-rem(16) => 1rem
* to-rem(16px) => 1rem
* to-rem(24) => 1.5rem
*/
@function to-rem($pixels) {
@if math.is-unitless($pixels) {
@return math.div($pixels, 16) * 1rem;
} @else if math.unit($pixels) == "px" {
@return math.div($pixels, $base-font-size) * 1rem;
}
@error "传入的单位不正确,必须是无单位数字或 px。";
}

文件路径: scss/main.scss (添加导入)

1
@use 'abstracts/functions';

现在,我们创建一个对比案例,看看使用 rem 的优势。

文件路径: scss/components/_widget.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
@use '../abstracts/functions' as f;

// 基础样式
.widget {
width: 300px;
margin-bottom: 20px;
border: 1px solid #ccc;
font-family: sans-serif;
}
.widget-title {
color: white;
padding: 10px;
}
.widget-body {
padding: 15px;
line-height: 1.5;
}

// 使用固定 PX 单位的 Widget
.widget-px {
.widget-title {
background-color: #e74c3c;
font-size: 24px;
}
.widget-body {
font-size: 16px;
}
}

// 使用 REM 函数的 Widget
.widget-rem {
.widget-title {
background-color: #3498db;
font-size: f.to-rem(24px); // 使用函数
}
.widget-body {
font-size: f.to-rem(16px); // 使用函数
}
}

文件路径: scss/main.scss (添加导入)

1
2
// ...
@use 'components/widget';

文件路径: index.html

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SCSS @function px-to-rem</title>
<link rel="stylesheet" href="scss/main.css">
<style>
html { font-size: 16px; /* 默认根字号 */ }
body { padding: 20px; display: flex; gap: 20px; }
.control { position: fixed; top: 10px; right: 10px; background: #eee; padding: 10px; border: 1px solid #ccc; }
</style>
</head>
<body>
<div class="widget widget-px">
<div class="widget-title">PX Widget</div>
<div class="widget-body">其 `font-size` 使用 `px` 单位,是固定的,不会随根字体大小变化而缩放。</div>
</div>
<div class="widget widget-rem">
<div class="widget-title">REM Widget</div>
<div class="widget-body">其 `font-size` 使用 `rem` 单位,会随根字体大小变化而缩放。</div>
</div>

<div class="control">
<label>调整根字体大小:</label>
<button onclick="document.documentElement.style.fontSize='10px'">10px</button>
<button onclick="document.documentElement.style.fontSize='16px'">16px</button>
<button onclick="document.documentElement.style.fontSize='20px'">20px</button>
</div>
</body>
</html>

操作提示: 点击页面右上角的按钮改变根字体大小。您会发现,“REM Widget” 的所有文字大小都会等比缩放,而 “PX Widget” 则毫无变化。这就是使用 @function 封装 rem 计算带来的巨大维护性和灵活性优势。