Skip to content

CSS 预处理器

为什么需要预处理器

原生 CSS 在大型项目中面临几个核心痛点:

  1. 没有变量(CSS 自定义属性 var() 出现之前)—— 颜色、间距等重复值散落各处,修改困难
  2. 没有嵌套(CSS Nesting 规范出现之前)—— 选择器重复书写,结构不直观
  3. 没有复用机制 —— 重复的样式片段无法抽象封装
  4. 没有计算能力 —— 缺少函数、循环、条件判断等编程能力
  5. 没有模块化 —— 所有样式在全局命名空间,容易冲突

CSS 预处理器(Preprocessor)通过扩展 CSS 语法,在编译时将增强语法转换为标准 CSS,解决了上述问题。

编写阶段          编译阶段          运行阶段
.scss / .less  →  CSS 预处理器  →  标准 .css  →  浏览器渲染
 (增强语法)        (编译转换)       (标准语法)

主流预处理器:

预处理器语言文件扩展名特点
Sass/SCSSRuby → Dart.scss / .sass功能最强大,社区最活跃
LessJavaScript.less语法简单,Ant Design 使用
StylusJavaScript.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 中查看和修改
主题切换需要编译多套 CSSJS 修改变量即可实时切换

最佳实践:两者配合使用——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 / @elsewhen guard
模块化@use / @forward@import(无命名空间)
继承@extend:extend()
编译器Dart Sass(官方推荐)less.js
生态Bootstrap 5、TailwindAnt 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-pxtorempx 自动转换为 rem
postcss-px-to-viewportpx 自动转换为 vw
cssnanoCSS 压缩和优化
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 的主要区别是什么?

  1. 语法:Sass 用 $ 定义变量,Less 用 @;Sass Mixin 用 @mixin/@include,Less 用 .mixin()
  2. 编程能力:Sass 支持自定义函数(@function),Less 不支持
  3. 模块化:Sass 有 @use/@forward 命名空间系统,Less 只有 @import
  4. 编译器:Sass 用 Dart Sass(Rust 编写的嵌入版更快),Less 用 JavaScript
  5. 生态:Bootstrap 用 Sass,Ant Design 用 Less

题目二:@import 和 @use 的区别是什么?

维度@import@use
命名空间无(全局可见)有(默认文件名)
重复编译每次 import 都编译只编译一次
私有成员全部暴露_ 前缀为私有
跨文件可见A import B 后,C import A 也能看到 B 的内容每个文件必须显式 @use
状态已废弃(Dart Sass 会警告)推荐使用

题目三:Mixin 和 @extend 应该用哪个?

推荐 Mixin,原因:

  1. @extend 会产生不可预测的选择器组合
  2. @extend 不能在 @media 块内跨作用域使用
  3. @extend 的输出顺序取决于选择器在文件中的位置
  4. Mixin 支持参数,更灵活

@extend 唯一的优势是编译后代码更紧凑(合并选择器而非复制样式),但在 gzip 压缩后这个优势可以忽略不计。

题目四:CSS 预处理器还有必要用吗?原生 CSS 不是已经有变量和嵌套了吗?

仍然有必要,原因:

  1. Mixin —— 原生 CSS 没有替代品,是最有价值的预处理器功能
  2. 循环和条件 —— 批量生成工具类、网格系统
  3. 模块化架构 —— @use/@forward 的命名空间和私有成员
  4. 编译时计算 —— 有些值在编译时就能确定,不需要推到运行时
  5. 团队规范 —— 预处理器提供了一套成熟的项目组织方式

但如果项目很简单、不需要这些高级功能,纯原生 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,迁移的关键步骤和注意事项有哪些?

用心学习,用代码说话 💻