CSS 预处理器与后处理器

中等 🟡CSS 布局
8 个标签
预计阅读时间:51 分钟
CSSSassLessPostCSSStylusautoprefixercssnano工程化

CSS 预处理器与后处理器

CSS 预处理器和后处理器扩展了 CSS 的功能,提高了开发效率和代码可维护性。它们是现代前端工程化体系里最基础、也最常被低估的一环:几乎所有大型项目都在用,但很多人只会写变量和嵌套,浪费了大部分能力。

什么是预处理器与后处理器

用一个类比理解两者的分工:

预处理器(Preprocessor) 像"写作时的高级编辑器"。你用它提供的加强版语法(变量、嵌套、循环、函数)写源码,它在编译前把这些语法翻译成浏览器能懂的普通 CSS。代表:Sass、Less、Stylus。
后处理器(Postprocessor) 像"排版印刷厂"。你交给它的已经是(接近)标准 CSS,它在编译后做加工:自动补前缀、压缩、把未来语法降级到当前浏览器能用的写法。代表:PostCSS 及其插件生态。

关键区别在于"作用时机"和"输入内容":

| 维度 | 预处理器 | 后处理器 |

| --- | --- | --- |

| 作用时机 | 编译前,处理自定义语法 | 编译后,处理标准/近标准 CSS |

| 输入 | .scss / .less / .styl | .css(或预处理器输出) |

| 核心价值 | 扩展语法、提升可维护性 | 兼容性、优化、面向未来 |

| 典型代表 | Sass、Less、Stylus | PostCSS + autoprefixer/cssnano |

| 能否运行时改 | 否,编译时固化 | 否,构建时固化 |

为什么重要:原生 CSS 长期缺少变量、嵌套、模块化能力,导致大型项目样式重复、难以维护。预处理器在 2010 年前后填补了这个空白,直接改变了行业写 CSS 的方式。即便到 2024 年原生 CSS 已经支持变量和嵌套,预处理器的循环、函数、@use 模块系统依旧不可替代;而后处理器则解决了"我想用新特性但要兼容老浏览器"的现实矛盾。

Sass/SCSS

Sass 是目前使用最广的预处理器,有两种语法:老的缩进语法(.sass,靠缩进和换行)和主流的 SCSS 语法(.scss,完全兼容 CSS,加大括号和分号)。下面统一用 SCSS。

变量:

`$primary-color: #007bff;`
支持颜色、数字、字符串、列表、映射(map)等多种数据类型
作用域:全局和局部,`!default` 可设默认值,`!global` 可提升为全局

嵌套:

选择器嵌套,减少重复书写父级
属性嵌套(如 `font: { size: 16px; weight: 700; }`)
`&` 父选择器引用,配合 BEM 非常顺手

混合(Mixins):

`@mixin` 定义,`@include` 调用
支持参数、默认值、可变参数(`$args...`)
`@content` 可把一整块样式传进去(常用于媒体查询封装)

继承:

`@extend` 继承选择器,共享同一组声明
`%placeholder` 占位符只有被 `@extend` 时才输出,避免死代码
目的是避免代码重复,但会改变选择器分组,需谨慎

函数:

内置函数:颜色(`lighten`、`darken`、`mix`)、字符串、数学、列表、map
自定义函数:`@function`,用 `@return` 返回值
新版推荐用 `sass:color`、`sass:math` 等内置模块

控制指令:

`@if`、`@else if`、`@else`
`@for`、`@each`、`@while`
让样式生成具备编程能力,批量生成栅格、颜色阶梯等

模块化:

`@import` 导入(已废弃,2022 年官方明确逐步移除)
`@use` 使用模块,带命名空间,避免全局污染
`@forward` 转发模块,用于搭建组件库的统一入口

Less

Less 语法上更贴近 CSS,学习成本低,Bootstrap 3 曾用它,Ant Design 早期也基于 Less,因此在企业项目里存量很大。

变量:

`@primary-color: #007bff;`
延迟加载(lazy loading):变量可以先用后定义
作用域规则遵循就近原则

混合:

`.mixin()` 定义和调用,任何类都可当作 mixin 复用
支持参数、默认值
带条件的混合(guard,用 `when` 关键字)

嵌套:

选择器嵌套、`&` 父选择器
媒体查询也可嵌套

函数:

丰富的内置函数(颜色、数学、类型判断)
命名空间(`#namespace { .mixin() }`)
可通过插件执行 JavaScript 表达式

导入:

`@import` 导入文件
导入选项:`(reference)`、`(inline)`、`(optional)` 等控制导入行为

PostCSS

概念:

用 JavaScript 工具和插件转换 CSS,本身不定义语法,是一个"平台"
解析 CSS 为 AST(抽象语法树),插件遍历并改写节点,再序列化回 CSS
拥有庞大的插件生态系统,Tailwind CSS 就是一个 PostCSS 插件

常用插件:

`autoprefixer`:根据 Can I Use 数据自动添加浏览器前缀
`postcss-preset-env`:让你现在就能写未来的 CSS 语法
`cssnano`:CSS 压缩,去空格、合并规则、优化颜色
`postcss-import`:内联 `@import` 的文件
`tailwindcss`:原子化实用工具类框架

配置:

`postcss.config.js`
插件顺序很重要(import 要在最前,cssnano 要在最后)
各插件有独立的选项配置

代码示例

Sass 变量、map 与嵌套

scssCode
// 基础变量
$primary-color: #007bff;
$secondary-color: #6c757d;
$font-size-base: 16px;
$spacing-unit: 8px;

// 用 map 管理一整套颜色(比一堆散变量更好维护)
$theme-colors: (
  'primary': #007bff,
  'success': #28a745,
  'danger':  #dc3545,
  'warning': #ffc107
);

// 嵌套选择器 + BEM
.card {
  background-color: white;
  border-radius: 8px;
  padding: $spacing-unit * 2;

  &__header {
    padding: $spacing-unit;
    border-bottom: 1px solid #eee;

    &--highlight {
      background-color: $primary-color;
      color: white;
    }
  }

  &__body {
    padding: $spacing-unit * 2;

    p {
      margin: 0 0 $spacing-unit 0;
      line-height: 1.6;
    }
  }

  &__footer {
    padding: $spacing-unit;
    border-top: 1px solid #eee;
    text-align: right;

    .button {
      margin-left: $spacing-unit;
    }
  }

  &:hover {
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  }
}

Sass 混合(Mixins)与 @content

scssCode
@use 'sass:color';

// 带默认参数的按钮混合
@mixin button($bg-color: $primary-color, $text-color: white) {
  padding: 10px 20px;
  background-color: $bg-color;
  color: $text-color;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s ease;

  &:hover {
    background-color: color.adjust($bg-color, $lightness: -10%);
  }

  &:active {
    transform: translateY(1px);
  }
}

.button-primary   { @include button(); }
.button-secondary { @include button($secondary-color); }
.button-success   { @include button(#28a745); }

// 用 @content 封装响应式断点,业务里只写"我要在小屏做什么"
@mixin respond-to($breakpoint) {
  @if $breakpoint == 'small' {
    @media (max-width: 640px) { @content; }
  } @else if $breakpoint == 'medium' {
    @media (max-width: 768px) { @content; }
  } @else if $breakpoint == 'large' {
    @media (max-width: 1024px) { @content; }
  } @else {
    @error 'Unknown breakpoint: #{$breakpoint}';
  }
}

.container {
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;

  @include respond-to('small')  { padding: 10px; }
  @include respond-to('medium') { padding: 15px; }
}

Sass 循环与函数:批量生成工具类

scssCode
@use 'sass:math';

// @each 遍历 map 批量生成主题色类
$theme-colors: (
  'primary': #007bff,
  'success': #28a745,
  'danger':  #dc3545
);

@each $name, $color in $theme-colors {
  .text-#{$name} { color: $color; }
  .bg-#{$name}   { background-color: $color; }
}

// @for 生成 12 栅格
@for $i from 1 through 12 {
  .col-#{$i} {
    width: math.percentage(math.div($i, 12));
  }
}

// 自定义函数:根据背景色自动返回可读的文字色
@function contrast-color($bg) {
  @if (lightness($bg) > 50%) {
    @return #000;
  } @else {
    @return #fff;
  }
}

.badge {
  $bg: #007bff;
  background: $bg;
  color: contrast-color($bg);
}

Sass 模块化:@use 与 @forward

scssCode
// _variables.scss
$primary: #007bff;
$radius: 4px;

// _mixins.scss
@mixin center {
  display: flex;
  align-items: center;
  justify-content: center;
}

// index.scss —— 统一入口,@forward 把子模块转发出去
// @forward 'variables';
// @forward 'mixins';

// main.scss —— 业务里带命名空间使用,不会污染全局
@use 'variables' as v;
@use 'mixins' as m;

.modal {
  @include m.center;
  border-radius: v.$radius;
  background: v.$primary;
}

Less 变量、guard 混合与运算

lessCode
@primary-color: #007bff;
@spacing: 8px;

// 普通混合
.rounded(@radius: 4px) {
  border-radius: @radius;
}

// 带 guard(条件)的混合
.text-mode(@mode) when (@mode = dark) {
  color: #fff;
  background: #1a1a1a;
}
.text-mode(@mode) when (@mode = light) {
  color: #212529;
  background: #fff;
}

.card {
  .rounded(8px);
  .text-mode(light);
  padding: @spacing * 2;

  &:hover {
    // 内置颜色函数
    background: lighten(@primary-color, 40%);
  }
}

PostCSS 配置

javascriptCode
// postcss.config.js
module.exports = {
  plugins: [
    // 1) 先内联 @import 引入的文件
    require('postcss-import'),

    // 2) 让未来的 CSS 语法现在就能写(嵌套、自定义媒体查询等)
    require('postcss-preset-env')({
      stage: 3,
      features: {
        'nesting-rules': true,
        'custom-media-queries': true
      }
    }),

    // 3) 自动添加浏览器前缀
    require('autoprefixer')({
      overrideBrowserslist: ['> 1%', 'last 2 versions', 'not dead']
    }),

    // 4) 最后压缩(生产环境)
    process.env.NODE_ENV === 'production'
      ? require('cssnano')({ preset: 'default' })
      : false
  ].filter(Boolean)
};

autoprefixer 效果对比

cssCode
/* 你写的源码 */
.box {
  display: flex;
  user-select: none;
  backdrop-filter: blur(8px);
}

/* autoprefixer 编译后(示意,前缀取决于目标浏览器) */
.box {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-user-select: none;
     -moz-user-select: none;
      -ms-user-select: none;
          user-select: none;
  -webkit-backdrop-filter: blur(8px);
          backdrop-filter: blur(8px);
}

真实案例

案例 1:主题换肤系统。 某后台管理系统需要 5 套主题色。早期用硬编码,每套主题复制一份 CSS,共 5 份、维护地狱。改造为 Sass map + `@each` 循环后,只维护一份 map,新增主题只加一行配置,样式文件从约 2400 行降到约 600 行,新增一套主题的工时从半天降到 10 分钟。

案例 2:组件库前缀兼容。 某开源组件库要兼容 iOS Safari 12 和部分安卓 WebView。人工补 `-webkit-` 前缀漏补严重、线上频繁样式错乱。接入 autoprefixer + browserslist 后,前缀由构建自动生成,兼容性 bug 下降约 90%,开发者不再关心前缀这件事。

案例 3:产物体积优化。 某营销站 CSS 打包后 320KB。接入 cssnano(去重、合并、压缩)后降到约 210KB(约减 34%),配合 gzip 后传输体积进一步降到约 32KB,首屏 CSS 阻塞时间明显下降。

数据与对比

| 特性 | Sass/SCSS | Less | Stylus | PostCSS |

| --- | --- | --- | --- | --- |

| 变量 | 支持 | 支持 | 支持 | 靠插件/原生变量 |

| 嵌套 | 支持 | 支持 | 支持 | 靠插件 |

| 循环/条件 | 强 | 一般(guard) | 强 | 靠插件 |

| 函数 | 强,内置模块丰富 | 一般 | 强 | 靠插件 |

| 模块系统 | @use/@forward | @import | @import | postcss-import |

| 学习曲线 | 中 | 低 | 中 | 中(需懂插件) |

| 典型生态 | Bootstrap4/5、大量项目 | Bootstrap3、AntD 早期 | 较小众 | Tailwind、现代构建 |

一个常见的现代组合:Sass(写源码)+ PostCSS(autoprefixer + cssnano),两者不冲突、职责互补。

常见坑

1.嵌套过深:嵌套 5、6 层会生成又长又高优先级的选择器,难覆盖、难维护。经验法则:不超过 3 层。
2.@extend 的隐蔽副作用:`@extend` 会把选择器合并到同一组,可能改变源码顺序带来的优先级与输出位置,跨文件 extend 尤其危险。多数场景优先用 mixin 或占位符 `%`。
3.still using @import:Sass 的 `@import` 已废弃且会造成变量全局污染、重复输出。新项目一律用 `@use`/`@forward`。
4.PostCSS 插件顺序错:cssnano 放在 autoprefixer 前、或 postcss-import 没放最前,都会导致产物异常。
5.忘配 browserslist:autoprefixer 和 preset-env 都依赖 browserslist 目标。不配就用默认值,可能补出一堆没用的老前缀,反而增大体积。
6.把 CSS 变量当 Sass 变量用:`$var` 编译时固化,`--var` 运行时可变,两者用途不同,不能互相替代(详见 CSS 自定义属性一节)。
7.Node Sass 已弃用:早期的 `node-sass`(LibSass)已停止维护,请改用官方 `dart-sass`(npm 包名 `sass`)。

最佳实践

建立变量命名规范(如 `$color-primary`、`$space-md`),用 map 集中管理主题
合理使用嵌套(不超过 3 层),配合 `&` 写 BEM
明确 mixin 与 @extend 的取舍:需要传参/带媒体查询用 mixin,纯静态共享用占位符
新项目用 `@use`/`@forward` 做模块化,杜绝 `@import`
预处理器 + PostCSS 配合使用,前缀和压缩交给构建,不手写
配好 browserslist,让兼容目标可控、可复现
版本控制里只提交源文件(.scss/.less),产物 CSS 加入 .gitignore
关键 mixin/function 写注释文档,方便团队复用

Sass 数据类型详解

Sass 的强大来自它有真正的数据类型系统,不像 CSS 只有字符串和数字。掌握类型才能写出健壮的函数与批量生成逻辑。

| 类型 | 示例 | 说明 |

| --- | --- | --- |

| number | 16px、1.5、100% | 带或不带单位,可运算 |

| string | 'Roboto'、bold | 带引号或不带引号 |

| color | #007bff、rgba(0,0,0,.1) | 参与颜色函数运算 |

| boolean | true、false | 用于 @if 判断 |

| null | null | 空值,输出时该声明被忽略 |

| list | 10px 20px、(a, b, c) | 空格或逗号分隔 |

| map | ('key': value) | 键值对,主题系统核心 |

scssCode
@use 'sass:list';
@use 'sass:map';

// list 操作
$sizes: 8px, 16px, 24px, 32px;
$first: list.nth($sizes, 1);          // 8px(索引从 1 开始!)
$len: list.length($sizes);            // 4
$more: list.append($sizes, 40px);     // 追加

// map 操作
$config: (
  'radius': 8px,
  'shadow': 0 2px 4px rgba(0,0,0,.1),
  'z-modal': 1050,
);
$r: map.get($config, 'radius');       // 8px
$has: map.has-key($config, 'shadow'); // true
$merged: map.merge($config, ('radius': 12px)); // 覆盖式合并

// null 的妙用:条件性输出声明
@mixin maybe-border($color: null) {
  border: if($color, 1px solid $color, null); // color 为 null 时不输出 border
}

Sass 内置模块系统

新版 Sass 把内置函数拆进模块,需要先 `@use 'sass:xxx'` 再用 `模块.函数()` 调用,避免全局命名冲突。

| 模块 | 常用函数 | 用途 |

| --- | --- | --- |

| sass:math | div、percentage、round、ceil、abs、max | 数学运算 |

| sass:color | adjust、scale、change、mix、complement | 颜色处理 |

| sass:string | unquote、quote、to-upper-case、index | 字符串 |

| sass:list | nth、length、append、join、index | 列表 |

| sass:map | get、set、merge、keys、values、has-key | 映射 |

| sass:meta | type-of、inspect、call、module-variables | 反射/内省 |

scssCode
@use 'sass:math';
@use 'sass:color';
@use 'sass:string';

.demo {
  // 除法必须用 math.div(Sass 2.0 后 / 不再是除法)
  width: math.percentage(math.div(8, 12));  // 66.6667%
  // 颜色处理:scale 按比例,adjust 按绝对量
  background: color.scale(#007bff, $lightness: 20%);
  border-color: color.adjust(#007bff, $lightness: -10%);
  // 混色
  color: color.mix(#fff, #000, 25%);        // 75% 黑 + 25% 白
}

关于 `lighten/darken` 与 `color.scale/adjust`:老函数 `lighten($c, 10%)` 是把亮度绝对加 10%,接近白/黑时会失真;`color.scale($c, $lightness: 10%)` 是把剩余空间按比例缩放,更平滑,新代码推荐用后者。

@use ... with:可配置模块

`@use` 可以在导入时用 `with` 覆盖模块里带 `!default` 的变量,实现"模块参数化",这是搭建可配置组件库的关键。

scssCode
// _theme.scss —— 用 !default 声明可被外部覆盖的配置
$primary: #007bff !default;
$radius: 4px !default;
$font: system-ui !default;

.btn {
  background: $primary;
  border-radius: $radius;
  font-family: $font;
}
scssCode
// main.scss —— 导入时定制,无需改模块源码
@use 'theme' with (
  $primary: #e91e63,
  $radius: 12px
);

完整案例:Sass 生成响应式栅格系统

综合运用 map、循环、math 模块,几十行生成一整套 Bootstrap 风格栅格。

scssCode
@use 'sass:math';
@use 'sass:map';

$grid-columns: 12;
$gutter: 24px;
$breakpoints: (
  'sm': 576px,
  'md': 768px,
  'lg': 992px,
  'xl': 1200px,
);

.row {
  display: flex;
  flex-wrap: wrap;
  margin: 0 math.div(-$gutter, 2);
}
[class^='col-'] {
  padding: 0 math.div($gutter, 2);
  box-sizing: border-box;
}

// 基础列(移动优先,默认全宽)
@for $i from 1 through $grid-columns {
  .col-#{$i} {
    flex: 0 0 math.percentage(math.div($i, $grid-columns));
    max-width: math.percentage(math.div($i, $grid-columns));
  }
}

// 各断点响应式列:col-md-6 之类
@each $name, $width in $breakpoints {
  @media (min-width: $width) {
    @for $i from 1 through $grid-columns {
      .col-#{$name}-#{$i} {
        flex: 0 0 math.percentage(math.div($i, $grid-columns));
        max-width: math.percentage(math.div($i, $grid-columns));
      }
    }
  }
}

完整案例:Sass 主题系统(嵌套 map)

用嵌套 map 管理多套主题,一个循环生成所有主题的 CSS 变量输出,兼得 Sass 编译期组织能力与 CSS 变量运行时换肤能力。

scssCode
@use 'sass:map';

$themes: (
  'light': (
    'bg': #ffffff,
    'text': #212529,
    'accent': #007bff,
  ),
  'dark': (
    'bg': #1a1a1a,
    'text': #f5f5f5,
    'accent': #4dabf7,
  ),
);

// 遍历主题,输出成 CSS 自定义属性
@each $theme-name, $tokens in $themes {
  $selector: if($theme-name == 'light', ':root', '[data-theme="#{$theme-name}"]');
  #{$selector} {
    @each $key, $value in $tokens {
      --color-#{$key}: #{$value};
    }
  }
}

// 业务用 CSS 变量,运行时可切主题
body {
  background: var(--color-bg);
  color: var(--color-text);
}
.link { color: var(--color-accent); }

Sass @function 进阶:流式字号函数

用自定义函数封装 clamp 的斜率计算,业务里一行拿到流式字号。

scssCode
@use 'sass:math';

// 去掉单位,方便运算
@function strip($n) { @return math.div($n, ($n * 0 + 1)); }

// 生成 clamp():min~max 字号随视口 minVW~maxVW 线性变化
@function fluid($min, $max, $min-vw: 320px, $max-vw: 1280px) {
  $slope: math.div(strip($max) - strip($min), strip($max-vw) - strip($min-vw));
  $intercept: strip($min) - $slope * strip($min-vw);
  @return clamp(
    #{$min},
    #{$intercept * 1px} + #{$slope * 100vw},
    #{$max}
  );
}

h1 { font-size: fluid(28px, 56px); }
h2 { font-size: fluid(22px, 36px); }
p  { font-size: fluid(16px, 18px); }

@extend、mixin、占位符:输出对比

三者都能复用样式,但产出的 CSS 结构不同,选错会带来意料之外的优先级或体积问题。

scssCode
// 占位符 %:只有被 @extend 才输出,会把选择器分组
%card-base {
  padding: 16px;
  border-radius: 8px;
}
.card-a { @extend %card-base; color: blue; }
.card-b { @extend %card-base; color: red; }
cssCode
/* @extend 输出:选择器被合并到一处,声明不重复 */
.card-a, .card-b { padding: 16px; border-radius: 8px; }
.card-a { color: blue; }
.card-b { color: red; }
scssCode
// mixin:每次调用都复制一份声明
@mixin card-base { padding: 16px; border-radius: 8px; }
.card-a { @include card-base; color: blue; }
.card-b { @include card-base; color: red; }
cssCode
/* mixin 输出:声明被复制,未 gzip 前体积更大,但选择器独立、更可控 */
.card-a { padding: 16px; border-radius: 8px; color: blue; }
.card-b { padding: 16px; border-radius: 8px; color: red; }

| 维度 | @extend / % | mixin | 说明 |

| --- | --- | --- | --- |

| 是否复制声明 | 否(合并选择器) | 是(复制) | extend 未 gzip 前更省 |

| 能否传参 | 否 | 能 | 需动态值必用 mixin |

| 能否带媒体查询 | 不能跨 @media | 能(配 @content) | 断点封装必用 mixin |

| 选择器优先级 | 可能意外提升 | 不变 | extend 有隐蔽副作用 |

| 推荐场景 | 纯静态共享 | 参数化/响应式 | gzip 后差距很小,优先 mixin |

Sass 调试指令:@error / @warn / @debug

写库时用这三个指令做参数校验和调试,让错误在编译期暴露而非上线后。

scssCode
@use 'sass:meta';

@mixin size($w) {
  @if meta.type-of($w) != 'number' {
    @error 'size() 需要 number,收到了 #{meta.type-of($w)}: #{$w}';
  }
  @if $w < 0 {
    @warn 'size() 收到负值 #{$w},已按 0 处理';
    $w: 0;
  }
  @debug '最终宽度: #{$w}';   // 只在编译日志打印,不进产物
  width: $w;
}

Less 进阶特性

Less 除了基础变量嵌套,还有一些独特能力。

lessCode
// 1) detached ruleset:把一整块规则存进变量,延迟调用
@card-styles: {
  padding: 16px;
  border-radius: 8px;
};
.card { @card-styles(); }

// 2) 属性合并 merge:+ 逗号合并,+_ 空格合并
.shadow {
  box-shadow+: 0 2px 4px rgba(0,0,0,.1);
  box-shadow+: inset 0 0 2px #fff;   // 两条合并成一个 box-shadow
}

// 3) 映射:从命名空间/mixin 里按键取值
@sizes: {
  small: 8px;
  large: 24px;
};
.box { padding: @sizes[large]; }

// 4) 递归实现循环(Less 无 @for,用带 guard 的递归 mixin)
.gen-cols(@n, @i: 1) when (@i =< @n) {
  .col-@{i} { width: (@i * 100% / @n); }
  .gen-cols(@n, (@i + 1));
}
.gen-cols(12);

// 5) JS 表达式(需插件/旧版本,新版默认禁用,谨慎使用)
// @now: `Date.now()`;

Stylus 简介

Stylus 语法最自由,可省略大括号、分号甚至冒号,函数与循环能力强,但生态较小众,新项目已较少选用。

stylusCode
primary = #007bff
radius = 4px

button()
  padding 10px 20px
  border-radius radius
  background arguments

.btn
  button(primary)
  &:hover
    background darken(primary, 10%)

// 内建循环
for i in 1..3
  .m-{i}
    margin (i * 8px)

PostCSS 插件生态深入

PostCSS 的价值全在插件。除了前面提到的四大件,还有一批高频插件。

| 插件 | 作用 |

| --- | --- |

| postcss-nested | 支持类 Sass 的选择器嵌套 |

| postcss-custom-media | 支持 @custom-media 复用媒体查询条件 |

| postcss-custom-properties | 为不支持 CSS 变量的环境降级 |

| postcss-logical | 逻辑属性(margin-inline 等)与物理属性互转 |

| postcss-pxtorem | px 自动转 rem,移动端适配 |

| @fullhuman/postcss-purgecss | 摇掉未使用的 CSS(配 Tailwind 常用) |

| stylelint | CSS 代码风格/错误检查(走 PostCSS 解析) |

cssCode
/* postcss-custom-media:媒体查询条件也能复用,弥补原生缺失 */
@custom-media --md (min-width: 768px);
@custom-media --lg (min-width: 1024px);

.sidebar { display: none; }
@media (--md) { .sidebar { display: block; } }
@media (--lg) { .sidebar { width: 280px; } }
javascriptCode
// 一个更完整的 postcss.config.js(含嵌套、自定义媒体、逻辑属性)
module.exports = {
  plugins: [
    require('postcss-import'),
    require('postcss-custom-media'),
    require('postcss-nested'),
    require('postcss-logical'),
    require('autoprefixer'),
    process.env.NODE_ENV === 'production'
      ? require('cssnano')({ preset: 'default' })
      : false,
  ].filter(Boolean),
};

Tailwind 与 PostCSS 的关系

Tailwind CSS 本质是一个 PostCSS 插件:构建时扫描模板里用到的类名,按需生成对应的原子工具类,再交给 autoprefixer/cssnano。它代表了"原子化/工具类优先"的另一种 CSS 组织思路,与 Sass 的"语义类 + 预处理"是两条路线,也可以共存(Tailwind 处理布局工具类,Sass 处理复杂组件)。

javascriptCode
// tailwind.config.js —— content 决定扫描范围,摇树掉没用到的类
module.exports = {
  content: ['./src/**/*.{html,js,ts,jsx,tsx,vue}'],
  theme: {
    extend: {
      colors: { brand: '#007bff' },   // 扩展设计令牌
      spacing: { 18: '4.5rem' },
    },
  },
  plugins: [],
};

CSS Modules:作用域隔离方案

CSS Modules 不是预/后处理器,而是构建期把类名哈希化实现"局部作用域",解决全局类名冲突,常与 Sass 配合(`.module.scss`)。

scssCode
/* Button.module.scss */
.button { padding: 10px 20px; background: #007bff; }
.button:hover { background: #0069d9; }
javascriptCode
// 组件里按对象引用,构建后类名变成 Button_button__x3f9a,天然不冲突
import styles from './Button.module.scss';
function Button() {
  return <button className={styles.button}>点我</button>;
}

构建集成与 source map

现代构建工具对预/后处理器开箱即用。

Vite:内置支持,安装 `sass` 即可直接 import `.scss`;PostCSS 通过 `postcss.config.js` 自动生效。
webpack:`sass-loader` → `postcss-loader` → `css-loader` → `style-loader/MiniCssExtractPlugin`,注意 loader 从下往上执行。
source map:开发环境务必开启,DevTools 才能定位到 `.scss` 源码行,而不是编译后的 `.css`。
javascriptCode
// webpack 中的 loader 链(从后往前执行)
{
  test: /\.scss$/,
  use: [
    'style-loader',
    { loader: 'css-loader', options: { sourceMap: true } },
    { loader: 'postcss-loader', options: { sourceMap: true } },
    { loader: 'sass-loader', options: { sourceMap: true } },
  ],
}

产物优化数据对比

| 优化手段 | 典型体积变化 | 说明 |

| --- | --- | --- |

| cssnano 压缩 | 减 25%~40% | 去空格、合并规则、优化颜色 |

| PurgeCSS 摇树 | 减 70%~95% | 删掉模板未用到的类(Tailwind 项目尤其明显) |

| gzip | 再减 70%~80% | 服务端传输压缩,与上面叠加 |

| Brotli | 比 gzip 再减 10%~20% | 现代浏览器优先 |

举例:某 Tailwind 项目开发态生成的完整工具类约 3.5MB,经 PurgeCSS 摇树后剩约 12KB,再 gzip 后约 4KB——这也是原子化 CSS 能落地生产的前提。

迁移策略

两条最常见的迁移路径:

1.node-sass → dart-sass:`node-sass`(基于 LibSass)已停止维护,卸载后安装官方 `sass` 包即可,API 基本兼容;注意除法要改成 `math.div`、颜色函数迁到 `sass:color`。
2.@import → @use/@forward:`@import` 会重复输出、污染全局。迁移时把每个被导入文件加命名空间用 `@use`,共享入口用 `@forward`;变量引用改成 `namespace.$var`。可借助官方的 `sass-migrator` 工具自动改写。
bashCode
# 官方迁移工具:自动把 @import 改成 @use 并加命名空间
npx sass-migrator module --migrate-deps src/main.scss

更多真实案例

案例 4:栅格系统去重。 某项目手写了 4 个断点 × 12 列共约 500 行重复的列样式。改用上文的 `@each + @for` 生成后,源码降到约 40 行,改栅格列数或断点只需改配置,维护成本大幅下降。

案例 5:设计令牌单一来源。 某团队设计稿的颜色/间距与代码经常对不上。把 design tokens 存成 Sass map,构建时同时产出 CSS 变量(给运行时换肤)和一份 JSON(给设计工具/文档站消费),实现"一处定义,多端同源",设计走查返工减少约 50%。

案例 6:Stylelint 拦截错误。 某项目上线后频繁出现"颜色值写错、属性拼错但静默失效"的问题。接入 stylelint(走 PostCSS 解析)并在 CI 卡关后,这类低级样式错误在提交阶段即被拦截,线上样式类缺陷下降约 70%。

数据与对比:编译性能

| 工具 | 相对编译速度 | 备注 |

| --- | --- | --- |

| dart-sass | 快 | 官方实现,纯 Dart 编译为 JS/原生 |

| node-sass(LibSass) | 曾更快但已弃用 | 停止维护,不再支持新语法 |

| Less | 快 | 逻辑能力弱,编译负担小 |

| PostCSS | 取决于插件数量 | 插件越多越慢,按需启用 |

| Stylus | 中 | 生态小,工具链支持一般 |

经验上,大型项目的构建耗时更多花在 PostCSS 插件链(尤其 autoprefixer 遍历、cssnano 压缩)上,而非 Sass 编译本身;开发态可关掉 cssnano、生产态才启用。

Sass 控制流进阶:@while 与嵌套遍历

除了 @for/@each,还有 @while,以及在 @each 里同时解构多个值。

scssCode
@use 'sass:math';

// @while:生成斐波那契式间距(较少用,但能表达任意迭代逻辑)
$i: 1;
$val: 4px;
@while $i <= 5 {
  .fib-#{$i} { padding: $val; }
  $val: $val * 1.5;
  $i: $i + 1;
}

// @each 解构:一次取多个值,批量生成定位工具类
$positions: (
  ('top', top, 0),
  ('bottom', bottom, 0),
  ('left', left, 0),
  ('right', right, 0),
);
@each $name, $prop, $value in $positions {
  .stick-#{$name} { position: absolute; #{$prop}: $value; }
}

响应式断点 mixin 完整封装(up / down / between)

生产级项目常同时需要"以上""以下""区间"三种断点查询,一次封装好复用全站。

scssCode
@use 'sass:map';

$breakpoints: (
  'sm': 576px,
  'md': 768px,
  'lg': 992px,
  'xl': 1200px,
);

@function bp($name) {
  @if not map.has-key($breakpoints, $name) {
    @error 'Unknown breakpoint: #{$name}';
  }
  @return map.get($breakpoints, $name);
}

// 大于等于某断点(移动优先)
@mixin up($name) {
  @media (min-width: bp($name)) { @content; }
}
// 小于某断点(减 0.02px 避免边界重叠)
@mixin down($name) {
  @media (max-width: bp($name) - 0.02px) { @content; }
}
// 区间
@mixin between($lower, $upper) {
  @media (min-width: bp($lower)) and (max-width: bp($upper) - 0.02px) { @content; }
}

.nav {
  display: none;
  @include up('lg') { display: flex; }
}
.only-tablet {
  display: none;
  @include between('md', 'lg') { display: block; }
}

Sass 颜色函数完整对比

同一个基色,不同函数产出不同效果,做主题时要选对。

scssCode
@use 'sass:color';
$base: #3b82f6;

.swatches {
  --darken:   #{color.adjust($base, $lightness: -15%)}; // 绝对减亮度
  --lighten:  #{color.adjust($base, $lightness: 15%)};  // 绝对加亮度
  --scale-d:  #{color.scale($base, $lightness: -30%)};  // 按剩余空间比例变暗(更自然)
  --desat:    #{color.adjust($base, $saturation: -40%)};// 降饱和
  --mix:      #{color.mix(#fff, $base, 20%)};           // 混入 20% 白
  --alpha:    #{color.change($base, $alpha: 0.5)};      // 直接改透明度
  --comp:     #{color.complement($base)};               // 补色(色相 +180)
}

完整组件案例:Sass + BEM 写一个可配置卡片

综合变量、mixin、嵌套、@content,写一个结构清晰、可扩展的卡片组件。

scssCode
@use 'sass:color';

$card-radius: 12px !default;
$card-pad: 20px !default;
$card-accent: #3b82f6 !default;

@mixin elevation($level: 1) {
  @if $level == 1 { box-shadow: 0 1px 3px rgba(0,0,0,.1); }
  @else if $level == 2 { box-shadow: 0 4px 12px rgba(0,0,0,.12); }
  @else { box-shadow: 0 12px 32px rgba(0,0,0,.18); }
}

.card {
  border-radius: $card-radius;
  padding: $card-pad;
  background: #fff;
  border-top: 3px solid $card-accent;
  @include elevation(1);
  transition: box-shadow .2s;

  &:hover { @include elevation(2); }

  &__title {
    margin: 0 0 8px;
    font-size: 1.125rem;
    color: #111;
  }
  &__body {
    color: #555;
    line-height: 1.6;
  }
  &__footer {
    margin-top: $card-pad;
    display: flex;
    justify-content: flex-end;
    gap: 8px;
  }

  // 变体
  &--danger { border-top-color: #ef4444; }
  &--flat   { box-shadow: none; border: 1px solid #eee; }
  &--accent {
    background: color.mix(#fff, $card-accent, 92%);
  }
}

编写一个最小的 PostCSS 插件

理解 PostCSS 是"AST 遍历器"最好的方式是自己写一个插件。下面这个插件把所有 `px` 值加注释标注原始像素。

javascriptCode
// postcss-annotate-px.js —— 教学用极简插件
module.exports = () => ({
  postcssPlugin: 'postcss-annotate-px',
  Declaration(decl) {
    if (decl.value.includes('px') && !decl.raws.value) {
      // 在声明后追加一条注释(真实插件会做值转换)
      decl.after({ text: ' from: ' + decl.value });
    }
  },
});
module.exports.postcss = true;

真实插件(如 autoprefixer、pxtorem)就是在这些 `Declaration`/`Rule`/`AtRule` 钩子里读写 AST 节点,再由 PostCSS 序列化回 CSS。

Sass 与 CSS 变量桥接(深入)

预处理器与 CSS 变量并非二选一。最佳组合是:Sass 负责编译期组织与批量生成,把最终值"注入"CSS 变量,运行时再靠 CSS 变量换肤/交互。

scssCode
@use 'sass:map';

$tokens: (
  'space-1': 4px, 'space-2': 8px, 'space-3': 12px, 'space-4': 16px,
  'radius': 8px, 'accent': #3b82f6,
);

// 编译期把 Sass map 批量输出为运行时 CSS 变量
:root {
  @each $k, $v in $tokens {
    --#{$k}: #{$v};
  }
}

// 组件同时享受两者:Sass 做条件/循环,CSS 变量做运行时可变
@mixin pad($token) {
  padding: var(--#{$token});   // 引用运行时变量
}
.box { @include pad('space-4'); border-radius: var(--radius); }

判断口诀:要在浏览器里变(主题、用户设置、交互)→ CSS 变量;只在构建时算(循环、函数、条件)→ Sass 变量。

常见坑(补充)

8.Sass 除法用了 /:Sass 2.0 起 `/` 不再表示除法,会当作分隔符(如 `font: 16px/1.5`),除法必须用 `math.div()`。
9.@use 不能重复/带命名空间冲突:同一文件多次 `@use` 会报错;多个模块用同名命名空间也冲突,用 `as` 重命名。
10.!default 位置错:`!default` 只在变量"尚未赋值"时生效,若在 `@use ... with` 之后又硬赋值会覆盖配置。
11.PostCSS 插件顺序颠倒:postcss-import 必须最前,cssnano 必须最后,nested/custom-media 要在 autoprefixer 前。
12.PurgeCSS 误删动态类名:JS 里拼接的类名(如 `'btn-' + type`)会被摇树误删,需在 safelist 中声明。
13.source map 未开导致定位到编译产物:开发态忘开 sourceMap,DevTools 只能看到 .css 而非 .scss 源码行。

最佳实践(补充)

用 `sass:math`/`sass:color` 等模块函数替代已弱化的全局函数(`/`、`lighten`)。
组件库用 `!default` + `@use ... with` 暴露可配置项,避免使用者改源码。
设计令牌用 Sass map 单一来源,同时产出 CSS 变量与 JSON,多端同源。
生产才开 cssnano/PurgeCSS,开发态关闭以加快构建。
用 stylelint 在 CI 卡关,把样式错误挡在提交前。
迁移旧项目用官方 `sass-migrator` 自动把 `@import` 改为 `@use`。
优先 mixin,谨慎 @extend(注意其跨文件的隐蔽优先级副作用)。

总结

| 你的需求 | 推荐方案 |

| --- | --- |

| 新项目、要强逻辑能力 | Sass/SCSS + dart-sass |

| 存量 Less 项目(如老 AntD) | 继续用 Less,逐步迁移 |

| 需要浏览器前缀/兼容 | PostCSS + autoprefixer |

| 想现在写未来 CSS 语法 | postcss-preset-env |

| 生产环境压缩体积 | cssnano |

| 原子化/工具类优先 | Tailwind(PostCSS 插件) |

| 组件级样式隔离 | CSS Modules / CSS-in-JS |

一句话记忆:预处理器让你"写得爽",后处理器让你"跑得稳"。两者不是二选一,而是现代 CSS 工程链上前后衔接的两道工序。掌握变量、嵌套只是入门,真正拉开差距的是循环、函数、模块系统与后处理器的组合使用。