Sass 是一种 CSS 预处理器(CSS preprocessor) ;它在构建时把 SCSS 或缩进语法转换为浏览器可读取的 CSS。
当样式代码需要显式模块边界、带参数的复用规则或基于映射生成有限的 CSS 时,Sass 很合适;运行时主题值仍应交给 CSS 自定义属性。
新代码使用 SCSS、@use、@forward 和内置模块;编译实际入口文件,并审查生成的选择器与声明,而不只检查源文件。
是什么,为什么存在
Sass 是一种在构建阶段运行的样式语言。你编写 .scss 或 .sass 文件,Dart Sass 解析模块、计算值、展开选择器,再输出普通 CSS。浏览器不会执行 Sass,也看不到 $variable、@mixin 或 @use。
SCSS 是目前更常见的语法,它使用 CSS 的花括号和分号。有效 CSS 基本可以直接放进 SCSS 文件,再逐步加入 Sass 功能。缩进语法使用 .sass 扩展名,语义相同,但用缩进代替花括号和分号;同一项目应明确选择一种格式。
Sass 解决的是构建期复用与组织问题。它可以把设计值保存在映射中,通过 混入(mixin) 接收参数,在编译期循环生成一组确定的规则,并用 Sass 模块(Sass module) 控制名称来源。输出仍受普通 CSS 层叠(CSS cascade) 、继承和浏览器支持约束。
原生 CSS 已有自定义属性、嵌套、颜色函数和越来越多的计算能力,因此不是每个项目都需要 Sass。若问题只是在浏览器中切换主题值, CSS 自定义属性(CSS custom property) 通常更直接。若你需要发布一套可配置的样式 API、共享带参数的模式,或从结构化数据生成固定规则,Sass 仍有清楚的用途。
你会在组件库、已有设计系统和构建工具的样式管线中遇到 Sass。判断一段代码时要同时看两个层面:SCSS 是否表达了清楚的模块契约,以及编译后的 CSS 是否具有预期的选择器、顺序和体积。
工作原理
Sass 编译器从一个或多个入口文件开始。它解析 @use 和 @forward 指向的模块,求值变量、函数、条件和循环,随后把嵌套规则与混入展开成 CSS。默认情况下,每个模块在一次编译中只加载一次。
这一过程没有浏览器运行时。Sass 变量在编译后消失,条件分支只保留被选中的结果,循环变成一组实际规则。要在页面运行时响应属性、层叠或 JavaScript,应输出 CSS 自定义属性、类或属性选择器,而不是期待 Sass 变量继续存在。
一次编译可以按下面的顺序理解:
- 从入口文件读取顶层变量、
@use与@forward。 - 定位模块 URL,并以各自命名空间加载模块。
- 计算 Sass 值,执行函数、混入与控制指令。
- 展开嵌套选择器,合并或复制复用的声明。
- 按求值后的顺序输出 CSS,交给后续工具和浏览器。
两种源文件语法
SCSS 使用 .scss 扩展名,外形接近 CSS,也是下面示例采用的格式。缩进语法使用 .sass 扩展名;它不是旧编译器,而是同一语言的另一种表示。文件扩展名决定解析器采用哪种语法。
不要把「Sass」只当作缩进语法的名称。Sass 也是语言与工具链的总称,SCSS 是其中一种语法。团队文档可以写清「项目使用 Dart Sass 编译 SCSS」,避免三个名称混用。
编译期值
Sass 值包括数字、字符串、颜色、列表、映射、布尔值和 null。数字可以带单位,编译器会检查部分单位运算是否合理;映射适合存放一组可枚举的令牌或配置。使用 sass:map、sass:math 等内置模块时,调用来源在代码中很明确。
变量以 $ 开头,表示 Sass 求值期间的绑定。!default 允许模块使用方在首次 @use 时覆盖公开变量。它适合模块配置,不适合模拟浏览器层叠,也不会让值在页面加载后可变。
| 需求 | Sass 值 | CSS 自定义属性 |
|---|---|---|
| 构建时生成有限规则 | 适合 | 不执行循环 |
| 运行时按 DOM 或主题改变 | 编译后不可变 | 适合 |
| 在媒体查询声明内计算 | 可生成结果 | 可由浏览器解析 |
| 被 JavaScript 更新 | 不可直接更新 | 可通过样式 API 更新 |
嵌套与父选择器
嵌套会把内层选择器和外层选择器组合起来。& 表示当前外层选择器,可以形成伪类、伪元素或符合命名约定的后缀。它是源代码缩写,不会在 CSS 中留下层级关系。
嵌套越深,生成选择器越长,特异性与结构耦合也可能越高。最可靠的审查方式是查看输出,而不是规定一个适用于所有代码库的层数上限。若组件类本身已经足够明确,就让它保持顶层规则。
函数、混入与扩展
函数通过 @return 产生一个 Sass 值,适合单位转换、映射查询和经过校验的计算。混入通过 @include 发出声明或规则,还能通过 @content 接收一段内容。两者都可以接收位置参数、命名参数和默认值。
@extend 的机制不同:它改写选择器关系,让一个选择器匹配另一个选择器的样式。占位选择器 %name 只有被扩展时才输出。扩展可以减少重复声明,但跨模块或复杂选择器中的结果不容易从调用点看出,因此必须检查生成的选择器列表。
| 工具 | 产物 | 适合的契约 |
|---|---|---|
@function | 一个 Sass 值 | 可校验的计算或查询 |
@mixin | 声明与规则 | 带参数的样式模式 |
@extend | 合并后的选择器 | 明确的「也是一种」选择器关系 |
模块边界
@use 加载模块,并默认以文件名创建命名空间。模块的变量、函数和混入要通过该命名空间访问,这能说明成员来自哪里。以 _ 或 - 开头的成员属于私有实现,不应从模块外访问。
@forward 把另一个模块的公开成员转交给使用方,常用于建立一个稳定入口。它不会把这些成员自动放进转发文件自己的作用域;转发文件若也要使用成员,还需要 @use。库可以用 show、hide 或前缀缩小公开表面。
@use ... with (...) 只在模块首次加载时配置带 !default 的变量。同一次编译中,后来的代码不能用另一组配置重新加载同一模块。需要两套主题时,通常生成 CSS 自定义属性或分开编译入口,而不是试图实例化同一 Sass 模块两次。
示例
下面四个示例都由 /tmp/codewiki-run/sass/ 中的 Dart Sass 1.104.0 实际编译。输出使用默认展开格式,并关闭 source map,便于逐行对照。
变量、算术与嵌套
第一个入口用两个编译期变量生成卡片规则。&__title 拼接组件后缀,&:focus-within 则把伪类附加到 .card。
$accent: #6750a4;
$space: 0.5rem;
.card {
padding: $space * 3;
border: 1px solid $accent;
&__title {
color: $accent;
}
&:focus-within {
outline: 2px solid $accent;
outline-offset: 2px;
}
}.card {
padding: 1.5rem;
border: 1px solid #6750a4;
}
.card__title {
color: #6750a4;
}
.card:focus-within {
outline: 2px solid #6750a4;
outline-offset: 2px;
}输出中已经没有 $accent 和 $space。两个嵌套块成为独立 CSS 规则,说明源文件的缩进不代表浏览器中的组件封装。
带校验的函数与混入
这个入口通过 sass:math 完成有意的除法,通过 sass:map 查找断点。函数拒绝非 px 输入,混入拒绝未知名称,让调用错误在构建阶段暴露。
@use "sass:map";
@use "sass:math";
$breakpoints: (
compact: 36rem,
wide: 64rem,
);
@function rem($pixels) {
@if math.unit($pixels) != "px" {
@error "rem() expects a px value";
}
@return math.div($pixels, 16px) * 1rem;
}
@mixin from($name) {
$width: map.get($breakpoints, $name);
@if $width == null {
@error "Unknown breakpoint: #{$name}";
}
@media (min-width: $width) {
@content;
}
}
.product-grid {
display: grid;
gap: rem(12px);
@include from(wide) {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}.product-grid {
display: grid;
gap: 0.75rem;
}
@media (min-width: 64rem) {
.product-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}@content 中的声明被放进生成的媒体查询。rem(12px) 已经求值为 0.75rem,浏览器不会再次运行该函数。
配置本地模块
模块示例有两个局部模块和一个入口。_tokens.scss 声明可配置值与混入;文件名前导下划线表示它通常作为局部文件使用,而引用时仍写作 "tokens"。
$brand: #0f766e !default;
$radius: 0.375rem !default;
$focus-width: 3px !default;
@mixin focus-ring {
outline: $focus-width solid $brand;
outline-offset: 2px;
}_button.scss 只公开一个混入。它通过 tokens 命名空间读取模块成员,不依赖全局注入。
@use "tokens";
@mixin base {
padding: 0.5rem 0.875rem;
border: 0;
border-radius: tokens.$radius;
background: tokens.$brand;
color: white;
&:focus-visible {
@include tokens.focus-ring;
}
}入口先配置 tokens,再加载间接依赖它的 button。这个顺序保证 button 看到同一个已配置模块。
@use "tokens" with (
$brand: #7c3aed,
$radius: 0.75rem,
);
@use "button";
.button {
@include button.base;
}.button {
padding: 0.5rem 0.875rem;
border: 0;
border-radius: 0.75rem;
background: #7c3aed;
color: white;
}
.button:focus-visible {
outline: 3px solid #7c3aed;
outline-offset: 2px;
}配置改变了品牌色与圆角,没有修改模块源文件。$focus-width 保留默认值,因此输出仍是 3px。
生成运行时主题令牌
最后一个入口用 Sass 映射枚举主题,但输出 CSS 自定义属性。枚举发生在构建时,主题选择则由浏览器根据 data-theme 在运行时完成。
@use "sass:meta";
$themes: (
light: (
surface: #ffffff,
text: #1f2937,
),
dark: (
surface: #111827,
text: #f9fafb,
),
);
@each $name, $tokens in $themes {
[data-theme="#{$name}"] {
@each $token, $value in $tokens {
--#{$token}: #{meta.inspect($value)};
}
}
}
.panel {
background: var(--surface);
color: var(--text);
}[data-theme=light] {
--surface: #ffffff;
--text: #1f2937;
}
[data-theme=dark] {
--surface: #111827;
--text: #f9fafb;
}
.panel {
background: var(--surface);
color: var(--text);
}这里的插值把 Sass 值写入自定义属性值。.panel 保留 var(),所以 DOM 上的主题属性变化后无需重新编译。
陷阱
继续使用 @import 和全局内置函数
修复方法: 用 @use 和 @forward 建立模块边界,并改用 map.get()、color.adjust() 等命名空间 API。迁移时运行编译器,逐项处理弃用警告,不要只做字符串替换。
把 Sass 变量当成运行时变量
修复方法: 把构建期结构留给 Sass,把需要继承、主题切换或脚本更新的值输出为 CSS 自定义属性。审查最终 CSS,确认需要动态变化的位置仍包含 var(--name)。
让嵌套复制 DOM 结构
修复方法: 以组件或职责作为规则边界,只在伪类、状态和确有父级语义时嵌套。每次改动后搜索编译结果中的长选择器,并在另一个容器中渲染组件。
无条件展开混入与循环
修复方法: 先限定允许生成的名称,只输出产品实际使用的变体。把共同声明放进基础规则,让混入负责真正变化的部分,并在构建产物上比较规则数与体积。
隐式接受错误配置
修复方法: 在公共函数和混入入口检查允许的键、单位与范围,并用 @error 给出领域名称。测试至少覆盖一个有效值、一个未知键和一个单位错误。
编译期值与浏览器值
Sass 值只活在编译器的求值阶段。变量可以持有映射、函数可以检查单位、循环可以决定输出多少规则,但这些结构不会进入浏览器。最终 CSS 才是浏览器样式系统的输入。
CSS 自定义属性是 CSS 声明,参与层叠与继承。它的值可以因选择器、媒体条件和元素关系而变化,也可以由 JavaScript 的样式 API 修改。浏览器在需要时解析 var(),因此它能表达 Sass 变量不能表达的运行时关系。
两者可以配合,但边界要清楚。Sass 映射适合保存构建阶段已知的主题集合,并生成一组自定义属性声明;组件消费这些属性时继续写 var()。如果在组件规则里把映射值直接插入颜色声明,运行时覆盖入口就消失了。
自定义属性的值对 Sass 来说常像不透明的 CSS 文本。确实要把 Sass 值写入自定义属性时,可以显式插值;随后检查输出是否保留需要的空格、引号与函数。不要假设 Sass 的颜色对象、带单位数字和任意字符串在插值后都有相同语义。
模块加载与配置顺序
模块 URL 标识一次编译中的模块实例。第一个 @use 负责加载和求值;其他 @use 取得同一个实例及其公开成员。这避免了旧式导入反复输出同一模块 CSS 的常见问题。
配置必须发生在首次加载处。如果 button 先 @use "tokens",入口随后再写 @use "tokens" with (...),编译器会拒绝重新配置。入口应先配置底层模块,再加载依赖它的模块,或者由库提供一个清楚的配置入口。
@forward 适合设计库的公开表面。一个索引文件可以转发颜色、排版和混入,同时隐藏内部辅助成员或加前缀。使用方只依赖这个入口,库内部文件移动时就不必修改所有调用点。
命名空间通配形式 as * 会把公开成员带入当前作用域。小型受控入口中可能方便,但多个模块使用它时又会失去来源信息并产生冲突。默认保留短而明确的命名空间,只有在公开 API 已严格控制时才考虑通配。
选择器展开与产物体积
Sass 嵌套通过父选择器组合生成实际选择器。一个内层逗号列表会与外层列表形成组合,循环又可能重复整个结构。源代码行数因此不是产物复杂度的可靠指标。
混入复制的是生成内容。十个变体调用一个包含布局、排版与状态规则的混入,会得到十份相关声明。若只有颜色变化,把不变部分移到基础类,再让变体设置自定义属性或少量声明,通常更容易审查。
扩展不会简单复制声明,它会统一选择器可匹配关系。复杂扩展可能产生调用点没有写出的组合选择器,尤其当被扩展选择器带上下文时。占位选择器能避免输出无用基础类,却不能消除组合复杂度。
产物检查应以真实入口为单位。记录压缩前后的文件体积可以发现趋势,但不能说明运行时成本;规则匹配、未使用 CSS 和缓存还需要各自的工具与数据。没有测量时,只描述生成结构,不声称某种写法更快。
数字、单位与 CSS 计算
Sass 数字保存数值与单位信息。兼容单位可以参与部分运算,不兼容单位会让编译器拒绝没有明确意义的计算。公共函数应先检查输入单位,再返回调用方期望的单位。
斜杠在现代 CSS 中承担分隔语义,Sass 不再把所有 / 都当作除法。需要在 Sass 中计算商时使用 math.div(),需要让浏览器计算时保留 calc()。这两个选择分别表达构建期与运行时意图。
CSS 数学函数可以混合 Sass 已知值与浏览器上下文值。例如,Sass 能替换固定间距,而百分比或视口单位仍由浏览器求值。检查输出时要确认编译器没有提前折叠本应依赖布局的表达式。
颜色运算也应使用 sass:color 的命名空间函数,并明确操作通道与色彩空间。机械地用固定比例变亮或变暗不能证明文字对比度合格。可访问配色需要在最终前景与背景组合上单独验证。
迁移与构建边界
从旧 Sass 迁移时,先锁定编译器版本并收集完整警告。@import、全局内置函数和斜杠除法可能同时出现,批量修改其中一种后应重新编译所有入口。只有入口覆盖完整,间接模块中的警告才不会漏掉。
构建工具只是把文件交给 Sass,并处理生成 CSS。加载路径、附加源码和实现选项会改变模块如何解析,因此命令行单文件成功不等于应用构建成功。最终验证必须使用项目自己的生产构建配置。
不要用全局注入重新制造旧式全局命名空间。把变量和混入自动附加到每个文件会隐藏依赖,还可能让同一成员在测试与生产中来自不同路径。显式 @use 多写一行,却能把依赖留在源文件中。
升级完成后,把弃用警告当作构建信号保存下来。新警告应在引入它的变更中处理,不能长期淹没在日志里。对于发布给其他项目的 Sass 库,还要从最小使用方入口测试公开模块 URL、配置变量和生成 CSS。
4个问题 · 1 道输出预测题 · 1 道找错题