主题
CSS 预处理器
为什么需要预处理器
原生 CSS 在大型项目中面临几个核心痛点:
- 没有变量(CSS 自定义属性
var()出现之前)—— 颜色、间距等重复值散落各处,修改困难 - 没有嵌套(CSS Nesting 规范出现之前)—— 选择器重复书写,结构不直观
- 没有复用机制 —— 重复的样式片段无法抽象封装
- 没有计算能力 —— 缺少函数、循环、条件判断等编程能力
- 没有模块化 —— 所有样式在全局命名空间,容易冲突
CSS 预处理器(Preprocessor)通过扩展 CSS 语法,在编译时将增强语法转换为标准 CSS,解决了上述问题。
编写阶段 编译阶段 运行阶段
.scss / .less → CSS 预处理器 → 标准 .css → 浏览器渲染
(增强语法) (编译转换) (标准语法)主流预处理器:
| 预处理器 | 语言 | 文件扩展名 | 特点 |
|---|---|---|---|
| Sass/SCSS | Ruby → Dart | .scss / .sass | 功能最强大,社区最活跃 |
| Less | JavaScript | .less | 语法简单,Ant Design 使用 |
| Stylus | JavaScript | .styl | 语法最灵活,可省略括号/冒号/分号 |
本文以 Sass(SCSS 语法) 为主讲解——它是当前使用最广泛的预处理器。Less 的语法大同小异,差异部分会单独说明。
SCSS vs Sass:SCSS(Sassy CSS)使用大括号和分号(兼容 CSS 语法),Sass 使用缩进。现在几乎所有项目都使用 SCSS 语法。
变量
Sass 变量
scss
// 定义变量
$primary-color: #3498db;
$font-size-base: 16px;
$spacing-unit: 8px;
$border-radius: 4px;
$font-stack: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
// 使用变量
.button {
color: $primary-color;
font-size: $font-size-base;
padding: $spacing-unit * 2 $spacing-unit * 3;
border-radius: $border-radius;
font-family: $font-stack;
}Less 变量
less
@primary-color: #3498db;
@font-size-base: 16px;
.button {
color: @primary-color;
font-size: @font-size-base;
}变量作用域
Sass 变量有块级作用域(大括号内定义的变量只在块内可见):
scss
$color: red; // 全局
.container {
$color: blue; // 局部,只在 .container 内有效
color: $color; // blue
}
.other {
color: $color; // red(不受 .container 内的 $color 影响)
}使用 !global 标志可以在块内修改全局变量:
scss
$theme: light;
.dark-mode {
$theme: dark !global; // 修改全局变量
}Sass 变量 vs CSS 自定义属性
| 维度 | Sass 变量($) | CSS 自定义属性(var()) |
|---|---|---|
| 处理时机 | 编译时(构建阶段) | 运行时(浏览器中) |
| 动态性 | 不能在运行时改变 | 可以通过 JS 动态修改 |
| 作用域 | 块级作用域 | CSS 级联和继承 |
| 计算能力 | 支持运算、函数 | 有限(calc() 内使用) |
| 调试 | 编译后消失,无法在 DevTools 看到 | 可在 DevTools 中查看和修改 |
| 主题切换 | 需要编译多套 CSS | JS 修改变量即可实时切换 |
最佳实践:两者配合使用——Sass 变量用于编译时的计算和配置,CSS 自定义属性用于运行时需要动态变化的值(如主题色)。
scss
// Sass 变量:编译时配置
$spacing-unit: 8px;
$breakpoint-md: 768px;
// CSS 自定义属性:运行时主题
:root {
--color-primary: #3498db;
--color-bg: #ffffff;
}
[data-theme="dark"] {
--color-primary: #5dade2;
--color-bg: #1a1a2e;
}
.card {
padding: $spacing-unit * 3; // 编译时计算
background: var(--color-bg); // 运行时主题
border: 1px solid var(--color-primary);
}嵌套
基本嵌套
scss
.nav {
background: #333;
ul {
list-style: none;
display: flex;
}
li {
margin: 0 16px;
}
a {
color: white;
text-decoration: none;
}
}编译为:
css
.nav { background: #333; }
.nav ul { list-style: none; display: flex; }
.nav li { margin: 0 16px; }
.nav a { color: white; text-decoration: none; }父选择器引用 &
& 代表外层选择器本身,常用于伪类、伪元素和 BEM 命名:
scss
.button {
background: #3498db;
transition: all 0.3s;
// 伪类
&:hover {
background: #2980b9;
}
&:active {
transform: scale(0.98);
}
// 伪元素
&::before {
content: '';
}
// BEM 修饰符
&--primary {
background: #3498db;
}
&--danger {
background: #e74c3c;
}
// BEM 子元素
&__icon {
margin-right: 8px;
}
&__text {
font-weight: 500;
}
}编译为:
css
.button { background: #3498db; transition: all 0.3s; }
.button:hover { background: #2980b9; }
.button:active { transform: scale(0.98); }
.button::before { content: ''; }
.button--primary { background: #3498db; }
.button--danger { background: #e74c3c; }
.button__icon { margin-right: 8px; }
.button__text { font-weight: 500; }属性嵌套
Sass 支持对有共同前缀的属性进行嵌套(较少使用):
scss
.element {
font: {
family: Arial;
size: 16px;
weight: bold;
}
margin: {
top: 10px;
bottom: 20px;
}
}嵌套的深度控制
嵌套不要超过 3-4 层——过深的嵌套会生成过于具体的选择器,增加特异性(specificity),降低可维护性。
scss
// ❌ 嵌套过深
.page {
.header {
.nav {
.list {
.item {
.link {
color: red; // 生成 .page .header .nav .list .item .link
}
}
}
}
}
}
// ✅ 控制在 2-3 层
.nav-list {
&__item { }
&__link { color: red; }
}Mixin 混入
Mixin 是可复用的样式片段,类似于函数——可以接收参数,返回一组样式规则。
基本用法
scss
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.hero {
@include flex-center;
height: 100vh;
}
.modal {
@include flex-center;
position: fixed;
inset: 0;
}带参数的 Mixin
scss
@mixin button-variant($bg, $color: white, $radius: 4px) {
background: $bg;
color: $color;
border-radius: $radius;
border: none;
padding: 8px 16px;
cursor: pointer;
&:hover {
background: darken($bg, 10%);
}
}
.btn-primary {
@include button-variant(#3498db);
}
.btn-danger {
@include button-variant(#e74c3c, white, 8px);
}内容块 @content
Mixin 可以接收一个内容块,用 @content 占位:
scss
@mixin respond-to($breakpoint) {
@if $breakpoint == 'mobile' {
@media (max-width: 480px) { @content; }
} @else if $breakpoint == 'tablet' {
@media (max-width: 768px) { @content; }
} @else if $breakpoint == 'desktop' {
@media (min-width: 769px) { @content; }
}
}
.sidebar {
width: 240px;
@include respond-to('tablet') {
width: 100%;
position: fixed;
}
@include respond-to('mobile') {
display: none;
}
}常用 Mixin 示例
scss
// 文本省略
@mixin text-ellipsis($lines: 1) {
@if $lines == 1 {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} @else {
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
// 滚动条样式
@mixin custom-scrollbar($width: 6px, $track: #f1f1f1, $thumb: #c1c1c1) {
&::-webkit-scrollbar {
width: $width;
}
&::-webkit-scrollbar-track {
background: $track;
border-radius: $width;
}
&::-webkit-scrollbar-thumb {
background: $thumb;
border-radius: $width;
}
}
// 使用
.title {
@include text-ellipsis(2); // 两行省略
}
.container {
@include custom-scrollbar(8px, transparent, #999);
}Mixin vs @extend vs 函数
| 特性 | Mixin | @extend | 函数 |
|---|---|---|---|
| 用途 | 输出一组样式规则 | 继承另一个选择器的样式 | 计算并返回一个值 |
| 参数 | 支持 | 不支持 | 支持 |
| 输出 | 每次调用都复制样式 | 合并选择器(逗号分隔) | 返回值,不输出样式 |
| 编译结果 | 可能有冗余代码 | 更紧凑,但选择器可能很长 | 不产生选择器 |
| 推荐程度 | ⭐⭐⭐⭐⭐ | ⭐⭐(慎用) | ⭐⭐⭐⭐ |
@extend 的问题:在复杂项目中容易产生意想不到的选择器组合,且在 @media 块内无法跨作用域 extend。大多数团队规范建议避免使用 @extend,用 Mixin 替代。
函数
内置函数
Sass 提供了丰富的内置函数:
scss
// 颜色函数
$base: #3498db;
color: lighten($base, 20%); // 提亮
color: darken($base, 15%); // 加深
color: saturate($base, 20%); // 增加饱和度
color: desaturate($base, 20%); // 降低饱和度
color: adjust-hue($base, 30deg); // 调整色相
color: rgba($base, 0.5); // 设置透明度
color: mix($base, #e74c3c, 50%); // 混合两种颜色
color: complement($base); // 互补色
// 数值函数
width: percentage(0.5); // 50%
width: round(3.7px); // 4px
width: ceil(3.2px); // 4px
width: floor(3.7px); // 3px
width: abs(-10px); // 10px
width: max(100px, 20vw); // 取较大值
width: min(300px, 100%); // 取较小值
// 字符串函数
content: to-upper-case("hello"); // "HELLO"
content: str-length("hello"); // 5
content: str-index("hello", "ell"); // 2
// 列表函数
$list: 10px 20px 30px;
margin-top: nth($list, 2); // 20px
$new-list: append($list, 40px); // 10px 20px 30px 40px
$count: length($list); // 3自定义函数
scss
@function rem($px, $base: 16) {
@return ($px / $base) * 1rem;
}
@function spacing($multiplier) {
$unit: 8px;
@return $unit * $multiplier;
}
@function z-index($layer) {
$layers: (
'base': 0,
'dropdown': 100,
'sticky': 200,
'modal': 300,
'toast': 400,
);
@return map-get($layers, $layer);
}
// 使用
.title {
font-size: rem(24); // 1.5rem
margin-bottom: spacing(3); // 24px
}
.modal {
z-index: z-index('modal'); // 300
}控制流
条件判断 @if / @else
scss
@mixin theme-color($theme) {
@if $theme == 'dark' {
background: #1a1a2e;
color: #f0f0f0;
} @else if $theme == 'light' {
background: #ffffff;
color: #333333;
} @else {
@error "Unknown theme: #{$theme}";
}
}循环 @for
scss
@for $i from 1 through 12 {
.col-#{$i} {
width: percentage($i / 12);
}
}
// 生成 .col-1 { width: 8.33% } ... .col-12 { width: 100% }循环 @each
scss
$colors: (
'primary': #3498db,
'success': #2ecc71,
'warning': #f39c12,
'danger': #e74c3c,
);
@each $name, $color in $colors {
.text-#{$name} { color: $color; }
.bg-#{$name} { background: $color; }
.border-#{$name} { border-color: $color; }
}循环 @while
scss
$columns: 4;
$i: 1;
@while $i <= $columns {
.grid-#{$i} {
width: 100% / $columns * $i;
}
$i: $i + 1;
}模块化
@import(已废弃)
传统方式,存在多个问题:
scss
// ❌ @import 的问题
@import 'variables';
@import 'mixins';
@import 'base';
// 1. 所有变量都是全局的,容易命名冲突
// 2. 无法确定变量/mixin 来自哪个文件
// 3. 同一文件被多次 @import 时会重复编译
// 4. 无法做到 tree-shaking@use 和 @forward(推荐)
Dart Sass 引入的模块化系统:
@use — 导入模块并使用
scss
// _variables.scss
$primary: #3498db;
$spacing: 8px;
// _mixins.scss
@use 'variables' as vars;
@mixin card {
padding: vars.$spacing * 3;
border-radius: 8px;
}
// main.scss
@use 'variables' as vars;
@use 'mixins';
.card {
color: vars.$primary;
@include mixins.card;
}@use 的特点:
- 模块有命名空间(默认使用文件名),避免命名冲突
- 同一模块只编译一次,即使被多个文件 @use
- 可以用
as *去掉命名空间(不推荐,失去了命名空间的好处)
@forward — 转发模块
当你想创建一个统一的入口文件,把多个模块的成员暴露出去:
scss
// abstracts/_index.scss
@forward 'variables';
@forward 'mixins';
@forward 'functions';
// 其他文件只需导入入口
// components/_card.scss
@use '../abstracts' as *;可以选择性转发:
scss
@forward 'variables' show $primary, $secondary;
@forward 'variables' hide $internal-var;Partial 文件
以 _ 开头的文件是 Partial(片段文件),不会被单独编译为 CSS,只能被其他文件导入:
styles/
├── abstracts/
│ ├── _variables.scss
│ ├── _mixins.scss
│ ├── _functions.scss
│ └── _index.scss ← @forward 转发
├── base/
│ ├── _reset.scss
│ ├── _typography.scss
│ └── _index.scss
├── components/
│ ├── _button.scss
│ ├── _card.scss
│ ├── _modal.scss
│ └── _index.scss
├── layout/
│ ├── _header.scss
│ ├── _sidebar.scss
│ ├── _footer.scss
│ └── _index.scss
└── main.scss ← 唯一的入口文件scss
// main.scss
@use 'abstracts';
@use 'base';
@use 'layout';
@use 'components';这就是 7-1 架构模式(7 个文件夹 + 1 个入口文件)的简化版,是 Sass 项目最经典的组织方式。
Sass vs Less 对比
| 维度 | Sass (SCSS) | Less |
|---|---|---|
| 变量语法 | $variable | @variable |
| Mixin 定义 | @mixin name { } | .name() { } |
| Mixin 调用 | @include name | .name() |
| 函数 | @function | 无自定义函数(用 Mixin 模拟) |
| 条件判断 | @if / @else | when guard |
| 模块化 | @use / @forward | @import(无命名空间) |
| 继承 | @extend | :extend() |
| 编译器 | Dart Sass(官方推荐) | less.js |
| 生态 | Bootstrap 5、Tailwind | Ant Design |
| 功能丰富度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
选择建议:新项目推荐 Sass(SCSS),功能更强大、社区更活跃、模块化支持更好。Less 主要在使用 Ant Design 的项目中仍有市场。
预处理器 vs 原生 CSS 新特性
CSS 原生能力在持续增强,很多预处理器的功能已经有了原生替代:
| 预处理器功能 | CSS 原生替代 | 状态 |
|---|---|---|
| 变量 | CSS Custom Properties (var()) | ✅ 全面支持 |
| 嵌套 | CSS Nesting (& { }) | ✅ 现代浏览器支持 |
| 颜色函数 | color-mix()、oklch() | ✅ 现代浏览器支持 |
| 数学计算 | calc()、min()、max()、clamp() | ✅ 全面支持 |
| Mixin | 无直接替代 | ❌ |
| 循环/条件 | 无直接替代 | ❌ |
| 模块化/命名空间 | @layer(层叠层) | 部分替代 |
结论:预处理器不会完全被替代——Mixin、循环、条件判断、项目架构等编程能力仍然是原生 CSS 缺失的。但简单项目可以考虑只使用原生 CSS + PostCSS。
PostCSS:后处理器
PostCSS 不是预处理器,而是一个 CSS 转换工具平台。它通过插件体系对 CSS 进行处理。
预处理器 PostCSS
.scss → [Sass 编译] → .css → [PostCSS 插件] → .css(最终)常用 PostCSS 插件:
| 插件 | 作用 |
|---|---|
autoprefixer | 自动添加浏览器厂商前缀 |
postcss-preset-env | 使用未来 CSS 特性,自动降级 |
postcss-pxtorem | px 自动转换为 rem |
postcss-px-to-viewport | px 自动转换为 vw |
cssnano | CSS 压缩和优化 |
javascript
// postcss.config.js
module.exports = {
plugins: {
autoprefixer: {},
'postcss-preset-env': {
stage: 2,
features: {
'nesting-rules': true
}
},
cssnano: process.env.NODE_ENV === 'production' ? {} : false
}
}预处理器 + PostCSS 是最佳组合——预处理器负责编写阶段的效率,PostCSS 负责编译后的兼容性和优化。
经典面试题解析
题目一:Sass 和 Less 的主要区别是什么?
- 语法:Sass 用
$定义变量,Less 用@;Sass Mixin 用@mixin/@include,Less 用.mixin() - 编程能力:Sass 支持自定义函数(
@function),Less 不支持 - 模块化:Sass 有
@use/@forward命名空间系统,Less 只有@import - 编译器:Sass 用 Dart Sass(Rust 编写的嵌入版更快),Less 用 JavaScript
- 生态:Bootstrap 用 Sass,Ant Design 用 Less
题目二:@import 和 @use 的区别是什么?
| 维度 | @import | @use |
|---|---|---|
| 命名空间 | 无(全局可见) | 有(默认文件名) |
| 重复编译 | 每次 import 都编译 | 只编译一次 |
| 私有成员 | 全部暴露 | _ 前缀为私有 |
| 跨文件可见 | A import B 后,C import A 也能看到 B 的内容 | 每个文件必须显式 @use |
| 状态 | 已废弃(Dart Sass 会警告) | 推荐使用 |
题目三:Mixin 和 @extend 应该用哪个?
推荐 Mixin,原因:
@extend会产生不可预测的选择器组合@extend不能在@media块内跨作用域使用@extend的输出顺序取决于选择器在文件中的位置- Mixin 支持参数,更灵活
@extend 唯一的优势是编译后代码更紧凑(合并选择器而非复制样式),但在 gzip 压缩后这个优势可以忽略不计。
题目四:CSS 预处理器还有必要用吗?原生 CSS 不是已经有变量和嵌套了吗?
仍然有必要,原因:
- Mixin —— 原生 CSS 没有替代品,是最有价值的预处理器功能
- 循环和条件 —— 批量生成工具类、网格系统
- 模块化架构 ——
@use/@forward的命名空间和私有成员 - 编译时计算 —— 有些值在编译时就能确定,不需要推到运行时
- 团队规范 —— 预处理器提供了一套成熟的项目组织方式
但如果项目很简单、不需要这些高级功能,纯原生 CSS + PostCSS 也完全可行。
题目五:如何组织一个大型项目的 Sass 架构?
推荐 7-1 架构模式(简化版):
styles/
├── abstracts/ # 变量、Mixin、函数(不输出 CSS)
├── base/ # 重置、排版、全局基础样式
├── components/ # 独立组件样式
├── layout/ # 布局组件(Header、Footer、Sidebar)
├── pages/ # 页面级样式(可选)
├── themes/ # 主题(可选)
├── vendors/ # 第三方库样式覆盖(可选)
└── main.scss # 唯一入口,按顺序 @use 所有模块关键原则:
abstracts/只定义不输出——变量、Mixin、函数- 每个组件一个文件,文件名与组件名一致
- 用
_index.scss+@forward建立目录入口 main.scss控制全局的引入顺序
追问思考
- 如果一个项目同时使用了 Sass 变量和 CSS 自定义属性,应该怎么划分它们的职责?
@use 'module' as *和@import 'module'看起来效果相似,底层有什么关键区别?- PostCSS 的
postcss-preset-env和 Babel 的@babel/preset-env设计理念有什么相似之处? - 在组件化框架(React/Vue)中使用 CSS Modules 后,Sass 的模块化优势是否被削弱?
- 如果要从 Less 迁移到 Sass,迁移的关键步骤和注意事项有哪些?