CSS 预处理器与后处理器
CSS 预处理器与后处理器
CSS 预处理器和后处理器扩展了 CSS 的功能,提高了开发效率和代码可维护性。它们是现代前端工程化体系里最基础、也最常被低估的一环:几乎所有大型项目都在用,但很多人只会写变量和嵌套,浪费了大部分能力。
什么是预处理器与后处理器
用一个类比理解两者的分工:
关键区别在于"作用时机"和"输入内容":
| 维度 | 预处理器 | 后处理器 |
| --- | --- | --- |
| 作用时机 | 编译前,处理自定义语法 | 编译后,处理标准/近标准 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。
变量:
嵌套:
混合(Mixins):
继承:
函数:
控制指令:
模块化:
Less
Less 语法上更贴近 CSS,学习成本低,Bootstrap 3 曾用它,Ant Design 早期也基于 Less,因此在企业项目里存量很大。
变量:
混合:
嵌套:
函数:
导入:
PostCSS
概念:
常用插件:
配置:
代码示例
Sass 变量、map 与嵌套
// 基础变量
$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
@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 循环与函数:批量生成工具类
@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
// _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 混合与运算
@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 配置
// 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 效果对比
/* 你写的源码 */
.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),两者不冲突、职责互补。
常见坑
最佳实践
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) | 键值对,主题系统核心 |
@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 | 反射/内省 |
@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` 的变量,实现"模块参数化",这是搭建可配置组件库的关键。
// _theme.scss —— 用 !default 声明可被外部覆盖的配置
$primary: #007bff !default;
$radius: 4px !default;
$font: system-ui !default;
.btn {
background: $primary;
border-radius: $radius;
font-family: $font;
}// main.scss —— 导入时定制,无需改模块源码
@use 'theme' with (
$primary: #e91e63,
$radius: 12px
);完整案例:Sass 生成响应式栅格系统
综合运用 map、循环、math 模块,几十行生成一整套 Bootstrap 风格栅格。
@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 变量运行时换肤能力。
@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 的斜率计算,业务里一行拿到流式字号。
@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 结构不同,选错会带来意料之外的优先级或体积问题。
// 占位符 %:只有被 @extend 才输出,会把选择器分组
%card-base {
padding: 16px;
border-radius: 8px;
}
.card-a { @extend %card-base; color: blue; }
.card-b { @extend %card-base; color: red; }/* @extend 输出:选择器被合并到一处,声明不重复 */
.card-a, .card-b { padding: 16px; border-radius: 8px; }
.card-a { color: blue; }
.card-b { color: red; }// mixin:每次调用都复制一份声明
@mixin card-base { padding: 16px; border-radius: 8px; }
.card-a { @include card-base; color: blue; }
.card-b { @include card-base; color: red; }/* 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
写库时用这三个指令做参数校验和调试,让错误在编译期暴露而非上线后。
@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 除了基础变量嵌套,还有一些独特能力。
// 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 语法最自由,可省略大括号、分号甚至冒号,函数与循环能力强,但生态较小众,新项目已较少选用。
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 解析) |
/* 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; } }// 一个更完整的 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 处理复杂组件)。
// 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`)。
/* Button.module.scss */
.button { padding: 10px 20px; background: #007bff; }
.button:hover { background: #0069d9; }// 组件里按对象引用,构建后类名变成 Button_button__x3f9a,天然不冲突
import styles from './Button.module.scss';
function Button() {
return <button className={styles.button}>点我</button>;
}构建集成与 source map
现代构建工具对预/后处理器开箱即用。
// 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 能落地生产的前提。
迁移策略
两条最常见的迁移路径:
# 官方迁移工具:自动把 @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 里同时解构多个值。
@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)
生产级项目常同时需要"以上""以下""区间"三种断点查询,一次封装好复用全站。
@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 颜色函数完整对比
同一个基色,不同函数产出不同效果,做主题时要选对。
@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,写一个结构清晰、可扩展的卡片组件。
@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` 值加注释标注原始像素。
// 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 变量换肤/交互。
@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 变量。
常见坑(补充)
最佳实践(补充)
总结
| 你的需求 | 推荐方案 |
| --- | --- |
| 新项目、要强逻辑能力 | Sass/SCSS + dart-sass |
| 存量 Less 项目(如老 AntD) | 继续用 Less,逐步迁移 |
| 需要浏览器前缀/兼容 | PostCSS + autoprefixer |
| 想现在写未来 CSS 语法 | postcss-preset-env |
| 生产环境压缩体积 | cssnano |
| 原子化/工具类优先 | Tailwind(PostCSS 插件) |
| 组件级样式隔离 | CSS Modules / CSS-in-JS |
一句话记忆:预处理器让你"写得爽",后处理器让你"跑得稳"。两者不是二选一,而是现代 CSS 工程链上前后衔接的两道工序。掌握变量、嵌套只是入门,真正拉开差距的是循环、函数、模块系统与后处理器的组合使用。