Sass 嵌套与模块化

一、嵌套

为什么需要嵌套

在原生 CSS 中,书写后代选择器需要重复父级选择器:

1
2
3
4
.nav { ... }
.nav .nav-item { ... }
.nav .nav-item .nav-link { ... }
.nav .nav-item .nav-link:hover { ... }

Sass 的嵌套规则允许将子选择器写在父选择器的花括号内,直接对应 HTML 的层级结构:

1
2
3
4
5
6
7
.nav {
.nav-item {
.nav-link {
&:hover { ... }
}
}
}

& — 父选择器引用

& 代表当前嵌套层级的外层选择器,用于生成复合选择器或伪类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.btn {
padding: 8px 16px;

// & 展开为 .btn:hover
&:hover { background: darken($bg, 10%); }

// & 展开为 .btn.active
&.active { border-color: blue; }

// & 展开为 .btn--large
&--large { font-size: 18px; }

// & 可以拼接后缀
&-icon { margin-right: 4px; }
}

编译结果:

1
2
3
4
5
.btn { padding: 8px 16px; }
.btn:hover { background: ...; }
.btn.active { border-color: blue; }
.btn--large { font-size: 18px; }
.btn-icon { margin-right: 4px; }

& 在嵌套中的特殊用法

1
2
3
4
5
// 生成 .dark .card 或 .dark.card 取决于 HTML 结构
.card {
.dark & { background: #222; } // .dark .card(祖先选择器在前)
&.dark { background: #222; } // .card.dark(自身复合)
}

属性嵌套

相同命名空间的属性可以嵌套书写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 传统写法
.card {
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
}

// 属性嵌套
.card {
font: {
family: sans-serif;
size: 14px;
weight: bold;
}
}
1
2
3
4
5
6
7
8
9
10
// 更复杂的 border 嵌套
.card {
border: 1px solid #ccc {
left: none; // 覆盖 border-left
bottom: {
width: 3px;
color: blue;
}
}
}

嵌套的陷阱

过度嵌套导致选择器过深

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ❌ 不好的做法:嵌套 5 层
.aside {
.widget {
.sidebar {
.list {
.item {
a { color: blue; }
}
}
}
}
}
// 编译为 .aside .widget .sidebar .list .item a { ... }
// → 权重过高、不利于覆盖、耦合过强

建议:嵌套不超过 3 层。如果需要很深的选择器,重新审视 HTML 结构或使用 BEM 命名。

避免生成无意义的复合选择器

1
2
3
4
5
6
7
8
9
// ❌ 没必要嵌套
.container {
margin: 0 auto;
.container-inner { width: 100%; }
}

// ✅ 直接用类名
.container { margin: 0 auto; }
.container-inner { width: 100%; }

@at-root

@at-root 将内部样式提升到文档根级别,脱离当前嵌套层级:

1
2
3
4
5
6
7
8
9
10
11
.block {
color: red;

@at-root {
.block-wrapper { padding: 10px; } // 不嵌套在 .block 下
}

@at-root .block-outside { // 单行简写
border: 1px solid;
}
}

编译结果:

1
2
3
.block { color: red; }
.block-wrapper { padding: 10px; }
.block-outside { border: 1px solid; }

典型用途:配合 BEM 生成 modifier

1
2
3
4
5
6
7
8
9
10
11
.card {
background: white;

&__title { font-size: 16px; }

// 将 & 提升到根再拼接,避免 .card .card--dark 这种多余层级
@at-root #{&}--dark {
background: #333;
color: white;
}
}

二、模块化

@import 已被弃用

1
2
3
// ❌ 旧语法(将逐步移除)
@import "variables";
@import "mixins";

@import 的问题:

  • 全局命名冲突——所有变量和 mixin 都暴露在全局作用域
  • 重复导入——多次 @import 同一文件导致重复输出
  • 无私有成员——无法控制哪些内容对外暴露

@use — 模块化方案(推荐)

1
2
3
// _variables.scss
$primary: #409eff !default;
$danger: #f56c6c !default;
1
2
3
4
5
6
7
8
9
// style.scss
@use 'variables'; // 默认命名空间为文件名
@use 'variables' as v; // 自定义命名空间
@use 'variables' as *; // 合并到当前作用域(不推荐,失去命名空间优势)

.button {
color: variables.$primary; // ✅ 通过命名空间访问
color: v.$primary; // ✅ 自定义命名空间
}

使用 @use 时,变量必须通过命名空间访问,避免了全局污染。

私有成员

以下划线 _- 开头的变量/mixin/函数视为私有:

1
2
3
// _config.scss
$-private-var: 10px; // ❌ 外部无法通过 @use 访问
$public-var: 20px; // ✅ 外部可访问

@forward — 转发/聚合导出

@forward 把一个模块的成员”透传”出去,方便组织入口文件:

1
2
3
4
5
6
7
8
9
10
// _mixins.scss
@mixin flex-center { ... }
@mixin text-ellipsis { ... }

// _variables.scss
$primary: #409eff;

// _index.scss(统一出口)
@forward 'variables';
@forward 'mixins';
1
2
3
4
5
6
7
// 业务代码只需导入一个入口
@use 'theme' as t;

.button {
color: t.$primary;
@include t.flex-center;
}

使用 @forward 的成员控制

1
2
3
4
5
6
7
8
9
10
11
12
// 隐藏某些成员(不对外暴露)
@forward 'variables' hide $-internal, $temp;

// 只暴露指定成员
@forward 'variables' show $primary, $danger;

// 给所有成员加前缀(避免命名冲突)
@forward 'variables' as theme-*;

// 使用加过前缀的模块
@use 'theme' as t;
// → t.$theme-primary, t.$theme-danger

内置模块

Dart Sass 提供了 7 个内置模块,通过 @use 导入使用(不再全局可用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@use 'sass:math';
@use 'sass:color';
@use 'sass:list';
@use 'sass:map';
@use 'sass:string';
@use 'sass:selector';
@use 'sass:meta';

// 示例
$result: math.div(10, 3); // 3.33333(数学除法)
$darker: color.adjust(#409eff, $lightness: -10%);
$second: list.nth((a b c), 2); // b
$exists: map.has-key($map, 'key');
$upper: string.to-upper-case('hello'); // "HELLO"

历史背景:在旧版 Sass 中,lighten()darken()rgba() 等函数是全局可用的。从 Dart Sass 开始,这些函数被移入 sass:color 模块。虽然为了兼容性全局函数仍可使用,但建议新代码改用模块化语法。

使用 @use 避免重复编译

对于 @mixin@function 文件,使用 @use 而不是 @import 确保它们只被编译一次,并且不污染全局作用域。

三、Partials(局部文件)

以下划线 _ 开头的 SCSS 文件称为 partial,Sass 编译器不会将其编译为独立的 CSS 文件,仅在 @use 时按需加载。

1
2
3
4
5
styles/
├── _variables.scss ← partial(不会输出 _variables.css)
├── _mixins.scss ← partial
├── _reset.scss ← partial
└── main.scss ← 入口文件,编译为 main.css

在入口文件引入:

1
2
3
4
// main.scss
@use 'variables';
@use 'mixins';
@use 'reset';

@use 'variables' 会自动查找 _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
26
27
// _config.scss
$prefix: 'el' !default;

// _variables.scss
@use 'config';
@use 'sass:map';

$colors: (
primary: #409eff,
success: #67c23a,
) !default;

$-internal-gray: #ccc; // 私有变量

// _mixins.scss
@use 'variables' as v;

@mixin button-theme($type) {
background: map.get(v.$colors, $type);
border: none;
color: white;
}

// _index.scss(统一出口)
@forward 'config';
@forward 'variables';
@forward 'mixins';
1
2
3
4
5
6
// app.scss
@use 'theme' as t;

.#{t.$prefix}-button {
@include t.button-theme(primary);
}