CSS 预处理器,你还是只会嵌套么?
Category(分类): CSS Status: 已整理
CSS 预处理器可以在浏览器理解 CSS 之前,先通过构建工具把带有额外语法的源文件编译成标准 CSS。常见的预处理器包括 Sass/SCSS、Less 和 Stylus。
它们通常提供:
- 变量;
- 嵌套;
- 混入(mixin);
- 选择器扩展;
- 函数;
- 条件和循环;
- 文件组织和模块化能力。
现代 CSS 已经提供了自定义属性、calc()、原生嵌套、媒体查询嵌套、容器查询和级联层等能力,因此预处理器不再是所有项目的必需品。但 Sass/Less 的编译期变量、混入、循环和函数仍然适合设计系统、主题生成和兼容旧项目。

一、Sass 基础
Sass 是较早出现、生态成熟的 CSS 预处理器。现在通常使用 Dart Sass,旧的 Ruby Sass 已经停止维护。Sass 有两种常见语法:
.scss:接近 CSS,使用大括号和分号;.sass:使用缩进,不写大括号和分号。
下面统一使用 SCSS 语法。
1. 变量
Sass 变量以 $ 开头:
$color: #8a8787;
.card {
$width: 100px;
width: $width;
color: $color;
background: #fff;
}
在规则块内部定义的变量通常是局部变量,可以遮蔽同名的外部变量。变量名也可以用于属性名或选择器插值:
$side: left;
.box {
border-#{$side}: 1px solid #ddd;
}
2. 嵌套
选择器可以嵌套,& 表示当前父选择器:
<div class="parent-container">
<div class="parent-container-header">标题</div>
</div>
.parent-container {
&-header {
background: {
color: red;
}
&:hover {
color: green;
}
}
}
编译后大致为:
.parent-container-header {
background-color: red;
}
.parent-container-header:hover {
color: green;
}
嵌套应控制深度,过深的选择器会增加特异性和维护成本。
3. 继承与 @extend
@extend 不是把另一个选择器的属性复制到当前选择器,而是让 Sass 合并选择器,使它们共享原有声明:
.message {
border: 1px solid #ddd;
}
.success {
@extend .message;
color: green;
}
通常会生成类似:
.message,
.success {
border: 1px solid #ddd;
}
.success {
color: green;
}
更常见的做法是使用占位选择器,避免生成不需要的基础类:
%message {
border: 1px solid #ddd;
}
.success {
@extend %message;
color: green;
}
@extend 可能产生复杂的选择器组合,跨组件扩展时需要谨慎。需要传递参数或明确复用一组声明时,通常优先考虑 @mixin。
4. 混入 @mixin
混入可以复用样式,并支持参数和默认值:
@mixin float-left($value: 10px) {
float: left;
margin-left: $value;
}
.box {
@include float-left(4px);
}
5. 模块化与导入
传统 Sass 使用:
@import 'variables';
但 Dart Sass 已经弃用 Sass 的 @import,现代项目建议使用模块系统:
// _variables.scss
$primary-color: #168bf5;
// app.scss
@use 'variables' as vars;
.button {
color: vars.$primary-color;
}
// _index.scss
@forward 'variables';
@forward 'mixins';
需要区分 Sass 的构建期导入和浏览器原生 CSS 的 @import。Sass 文件通常在构建阶段合并,不会直接让浏览器逐个下载 Sass 源文件;原生 CSS 的 @import 才是浏览器运行时导入样式的规则。
6. 注释
/* 会保留在生成的 CSS 中,除非压缩工具移除 */
// 通常不会输出到生成的 CSS
7. 颜色函数
Sass 提供颜色处理能力。现代 Sass 更推荐通过模块调用颜色函数:
@use 'sass:color';
$base: #cc3;
.box {
color: color.adjust($base, $lightness: 10%);
background: color.adjust($base, $lightness: -10%);
}
旧版全局函数如 lighten()、darken()、grayscale() 和 complement() 在旧项目中仍可能见到,但应关注 Dart Sass 的弃用提示和模块化 API。
二、Sass 运算
Sass 可以进行数字、颜色和字符串运算,但单位必须符合 CSS 和 Sass 的计算规则:
@use 'sass:math';
$height: 10px;
.box {
width: 10px + 10px;
height: $height + 10px;
margin-top: 20px - 10px;
margin-left: 10px * 2;
margin-right: math.div(10px, 2);
}
相加和相减时,单位通常需要兼容;乘法和除法也不能随意组合两个带单位的值。现代 Sass 推荐使用 math.div() 表示除法,避免 / 同时作为 CSS 斜杠分隔符和 Sass 除法运算符。
字符串运算
.box::before {
content: 'hello' + 'world';
}
实际输出和引号形式会受到 Sass 字符串规则影响,涉及复杂字符串时可以使用插值:
$name: 'world';
.box::before {
content: 'hello #{$name}';
}
三、Sass 控制命令
1. @if
@mixin visibility($hidden: true) {
@if $hidden {
display: none;
} @else {
display: block;
}
}
.hidden {
@include visibility();
}
2. @for
@for $i from 1 through 10 {
.margin-#{$i} {
margin-left: #{$i}px;
}
}
to 不包含结束值,through 包含结束值:
@for $i from 1 to 10 {
// 生成 1 到 9
}
3. @while
$i: 6;
@while $i > 0 {
.item-#{$i} {
left: #{$i}px;
}
$i: $i - 1;
}
4. @each
@each $item in a, b, c, d {
.#{$item} {
background-image: url('/image/#{$item}.jpg');
}
}
四、Sass 函数
可以定义返回值函数:
@function double($value) {
@return $value * 2;
}
.box {
width: double(2px);
}
Sass 内置函数按能力可以分为字符串、数字、列表、Map、颜色、选择器和内省函数等。常见数字函数包括 round()、ceil()、floor()、abs();使用前可查阅当前 Dart Sass 文档,避免使用已弃用的旧 API。
五、Sass、Less 与 Stylus
常见预处理器的特点可以简单概括为:
| 工具 | 特点 |
|---|---|
| Sass/SCSS | 生态成熟,模块系统、函数和控制命令丰富 |
| Less | 语法接近 CSS,入门简单,适合许多旧项目和基于 Less 的组件库 |
| Stylus | 语法灵活,支持省略部分符号,但团队协作时需要统一风格 |
Sass、Less 和 Stylus 都需要经过构建步骤生成 CSS。选择时应考虑项目现有技术栈、团队经验、依赖库和构建工具,不必把某一种预处理器视为所有项目的唯一答案。
六、Less 的使用方式
1. 浏览器运行时编译
Less 可以通过 Less.js 在浏览器中编译:
<link rel="stylesheet/less" href="styles.less">
<script src="less.min.js"></script>
样式链接通常需要在 Less.js 之前出现。这种方式适合快速演示或开发调试,不建议作为生产方案,因为浏览器需要额外下载 Less.js 和 Less 源文件并执行编译。
2. 构建阶段编译
现代项目通常将 Less 安装为本地开发依赖:
pnpm add -D less
pnpm exec lessc styles.less styles.css
使用 npm 时也可以执行:
npm install -D less
npx lessc styles.less styles.css
Webpack、Vite、Nuxt 等构建工具可以通过相应配置处理 Less。具体 loader 或插件版本应与构建工具版本匹配。
七、Less 变量
Less 变量以 @ 开头:
@color: #999;
@bg-color: skyblue;
@width: 50%;
#wrap {
color: @color;
background: @bg-color;
width: @width;
}
Less 变量支持作用域和延迟求值,可以在引用后定义,局部作用域会优先于外层作用域:
@var: @a;
@a: 100%;
#wrap {
width: @var;
@a: 9%;
}
在 Less 的作用域规则下,#wrap 中的 @a: 9% 会影响 @var 的求值。复杂项目中仍建议按照从依赖到使用的顺序组织变量,减少隐式行为。
1. 选择器变量
@selector: #wrap;
@name: wrap;
@{selector} {
color: #999;
width: 50%;
}
.@{name} {
color: #ccc;
}
#@{name} {
color: #666;
}
2. 属性变量
@property-name: border-style;
@style: solid;
#wrap {
@{property-name}: @style;
}
3. URL 变量
@images: '../img';
body {
background-image: url('@{images}/dog.png');
}
4. Detached ruleset(分离规则集)
@background: {
background: red;
};
#main {
@background();
}
5. 变量运算
Less 可以进行数字运算,但变量名中的连字符可能与减法混淆,运算符两侧应保留空格并适当使用括号:
@width: 300px;
@color: #222;
#wrap {
width: @width - 20px;
height: (@width - 20px) * 5;
margin: (@width - 20px) * 5;
color: lighten(@color, 10%);
}
不要把 @width-20 写成没有空格的形式,因为 Less 可能把它解析为名为 @width-20 的变量。
6. 变量变量
@fnord: 'I am fnord.';
@var: fnord;
#wrap::after {
content: @@var;
}
这里的 @@var 会先取出 @var 的值,再把它当作变量名继续查找。
八、Less 嵌套和媒体查询
1. & 的用法
#header {
&::after {
content: 'Less is more!';
}
.title {
font-weight: bold;
}
&_content {
margin: 20px;
}
}
大致生成:
#header::after {
content: 'Less is more!';
}
#header .title {
font-weight: bold;
}
#header_content {
margin: 20px;
}
&::after会把伪元素拼接到当前选择器;.title前没有&,表示后代选择器;&_content会直接把&替换为#header。
2. 嵌套媒体查询
#main {
width: 500px;
@media screen and (max-width: 768px) {
width: 100px;
}
@media tv {
width: 2000px;
}
}
Less 会把媒体查询提升到合适的位置,并保留 #main 选择器。是否合并相同媒体查询,还可能取决于后续的 CSS 优化工具。
3. 根据状态组织样式
#main {
&.show {
display: block;
}
}
.show {
display: none;
}
const main = document.getElementById('main')
main.classList.add('show')
当元素同时具有 id="main" 和 show 类时,#main.show 的特异性高于 .show,因此元素会显示。
九、Less 混入方法
1. 无参数混入
建议使用括号,避免混入同时输出同名 CSS 规则:
.card() {
background: #f6f6f6;
box-shadow: 0 1px 2px rgba(151, 151, 151, 0.58);
}
#wrap {
.card();
}
.card {} 也可以作为混入使用,但它同时可能输出 .card 规则,因此不完全等同于 .card()。
2. 默认参数和 @arguments
.box-shadow(@x: 0; @y: 0; @blur: 1px; @color: #000) {
box-shadow: @x @y @blur @color;
}
#main {
.box-shadow(0; 5px; 30px; red);
}
Less 支持默认参数。@arguments 可以引用传入混入的参数列表,但复杂属性建议明确写出各参数,避免逗号和空格列表产生歧义:
.text-shadow(@x: 0; @y: 0; @blur: 1px; @color: #000) {
text-shadow: @x @y @blur @color;
}
参数是否需要单位取决于最终 CSS 属性和运算方式,不是所有混入参数都必须带单位。
3. 模式匹配
可以根据第一个参数匹配不同混入:
.triangle(@direction; @width: 20px; @color: #000) when (@direction = top) {
border-color: transparent transparent @color transparent;
}
.triangle(@direction; @width: 20px; @color: #000) when (@direction = right) {
border-color: transparent @color transparent transparent;
}
.triangle(@direction; @width: 20px; @color: #000) when (@direction = bottom) {
border-color: @color transparent transparent transparent;
}
.triangle(@direction; @width: 20px; @color: #000) when (@direction = left) {
border-color: transparent transparent transparent @color;
}
.triangle(@direction; @width: 20px; @color: #000) {
border-style: solid;
border-width: @width;
}
#main {
.triangle(left; 50px; #999);
}
@_ 可以作为通配匹配参数,但应注意多个匹配混入可能同时执行,后面的声明可能覆盖前面的声明。
4. 命名空间
Less 可以使用带括号的规则集作为命名空间:
#card() {
.detail(@width: 300px) {
width: @width;
.size(@height: 300px) {
height: @height;
}
}
}
#wrap {
#card > .detail > .size(100px);
}
#main {
#card > .detail();
}
这里的 > 是 Less 混入命名空间的调用语法,不是最终 CSS 中的子元素选择器。
5. 条件混入和 when
Less 没有 Sass 风格的通用 @if 语句,但可以通过 when 守卫实现条件混入:
#card() {
.border(@width; @color; @style)
when (@width > 100px) and (@color = #999) {
border: @style @color @width;
}
.background(@color) when not (@color >= #222) {
background: @color;
}
.font(@size: 20px) when (@size > 50px), (@size < 100px) {
font-size: @size;
}
}
#main {
#card > .border(200px; #999; solid);
#card > .background(#111);
#card > .font(40px);
}
and表示条件同时满足;not表示条件取反;- 逗号可以表达多个守卫条件;
- Less 使用
<=,不要写成=<。
6. 不定数量参数
.box-shadow(...) {
box-shadow: @arguments;
}
.text-shadow(@first; ...) {
text-shadow: @arguments;
}
#main {
.box-shadow(1px 4px 30px red);
.text-shadow(1px; 4px; 30px; red);
}
7. !important 混入
.border() {
border: solid 1px red;
margin: 50px;
}
#main {
.border() !important;
}
调用混入时添加 !important,通常会让混入生成的各项声明带上 !important。但应谨慎使用,避免提高后续样式覆盖难度。
8. 递归混入模拟循环
Less 没有 Sass 的 @for,可以使用递归混入生成栅格类:
.generate-columns(@n; @i: 1) when (@i <= @n) {
.column-@{i} {
width: (@i * 100% / @n);
}
.generate-columns(@n; @i + 1);
}
.generate-columns(4);
这会生成 .column-1 到 .column-4。递归必须有明确的终止条件,否则会造成编译错误或无限递归。
9. 属性拼接
Less 使用 + 和 +_ 进行属性值合并:
.box-shadow() {
box-shadow+: inset 0 0 10px #555;
}
.main {
.box-shadow();
box-shadow+: 0 0 20px black;
}
输出为逗号分隔的多层阴影:
.main {
box-shadow: inset 0 0 10px #555, 0 0 20px black;
}
空格拼接示例:
.animation() {
transform+_: scale(2);
}
.main {
.animation();
transform+_: rotate(15deg);
}
10. 在混入中计算值
.average(@x; @y) {
@average: ((@x + @y) / 2);
}
div {
.average(16px; 50px);
padding: @average;
}
上面的结果为 padding: 33px。复杂计算应注意单位和 CSS 属性的合法取值。
十、Less 继承 :extend
Less 的 :extend 会扩展选择器关系,不是复制属性:
.animation {
transition: all 0.3s ease-out;
.hide {
transform: scale(0);
}
}
#main {
&:extend(.animation);
}
#con {
&:extend(.animation .hide);
}
大致会让 #main 共享 .animation 的声明,让 #con 参与 .animation .hide 的选择器组合。
all 扩展
#main {
width: 200px;
}
#main::after {
content: 'Less is good!';
}
#wrap:extend(#main all) {}
all 会尝试匹配包含目标选择器的更多选择器关系,功能强大但可能生成复杂 CSS,应谨慎使用。
与混入相比:
- 混入通常把声明复制到调用者;
extend主要合并选择器;extend可能减少重复声明,但容易产生难以预料的选择器组合;- 组件边界中通常优先考虑明确的混入或 CSS 自定义属性。
十一、Less 导入
1. 普通导入
@import 'main';
// 等价于导入 main.less
Less 文件通常可以省略扩展名。@import 在 Less 文件中的位置可以较灵活,嵌套导入的规则会根据 Less 规则输出。
2. reference
@import (reference) 'bootstrap.less';
#wrap:extend(.navbar all) {}
reference 会导入文件中的定义供当前文件使用,但不会把被引用文件的全部 CSS 直接输出。
3. once 和 multiple
@import (once) 'foo.less';
@import (once) 'foo.less';
once 是默认行为,同一路径通常只导入一次:
@import (multiple) 'foo.less';
@import (multiple) 'foo.less';
multiple 允许同一个文件重复导入。实际项目中应合理组织入口文件,避免重复输出。
十二、Less 函数
Less 内置函数可以按能力分为字符串、数字、列表、类型判断和颜色函数等。
1. 类型判断
常见函数包括:
isnumber():判断是否为数字;iscolor():判断是否为颜色;isstring():判断是否为字符串;iskeyword():判断是否为关键字;isurl():判断是否为 URL;ispixel():判断是否为像素值;ispercentage():判断是否为百分比;isunit():判断是否为指定单位。
2. 颜色函数
常见函数包括:
saturate():增加饱和度;lighten():增加亮度;darken():降低亮度;fade():设置或调整透明度;mix():按比例混合颜色;grayscale():生成灰度颜色。
3. 数学函数
常见函数包括:
ceil():向上取整;floor():向下取整;percentage():把无单位数字转换为百分比;round():四舍五入;sqrt():平方根;abs():绝对值;pow():幂运算。
使用函数时应参考当前 Less 版本文档,不要只依赖旧版本输出结果。
十三、其他 Less 特性
1. 注释
/* CSS 注释,通常会出现在生成的 CSS 中 */
// Less 单行注释,通常不会输出到生成的 CSS
2. 转义输出
Less 可以使用 ~ 将值作为字符串输出,但应确保输出结果仍然是合法 CSS:
.main {
width: ~'calc(300px - 30px)';
}
现代 Less 中,能直接写成普通 CSS 的表达式不必强行转义。calc() 中的加减运算符通常应保留空格。
3. 变量插值与动画名称
@duration: 1s;
@ease: ease;
.judge(@i) when (@i = 1) {
@size: 15px;
}
.judge(@i) when (@i > 1) {
@size: 16px;
}
.loop-animation(@i) when (@i < 4) {
.circle:nth-child(@{i}) {
.judge(@i);
border-radius: @size @size 0 0;
animation: ~'circle-@{i}' @duration infinite @ease;
transition-delay: ~'@{i}ms';
}
@keyframes ~'circle-@{i}' {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.loop-animation(@i + 1);
}
.loop-animation(1);
插值变量名时要使用正确的 @{name} 语法,并确保混入名称和变量已经定义。
4. JavaScript 求值的历史能力
旧版 Less.js 支持通过反引号执行 JavaScript,例如:
@upper: ~`'aaa'.toUpperCase()`;
但这项能力存在以下风险:
- 依赖运行时 Less.js 或特定构建配置;
- 现代工具通常默认关闭 JavaScript 求值;
Math.random()会让构建结果不可复现;window、alert等浏览器对象在 Node 构建环境中不存在;- 执行任意代码会带来安全问题;
- 计算结果发生在编译期,不是浏览器运行时的动态 CSS。
因此不建议在生产项目中使用 Less 的 JavaScript 求值。需要根据视口或业务状态设置样式时,应由 JavaScript 运行时通过 CSS 自定义属性传值:
document.documentElement.style.setProperty(
'--viewport-height',
`${window.innerHeight}px`,
)
.panel {
height: var(--viewport-height);
}
十四、预处理器与现代 CSS 的边界
预处理器并不能替代浏览器运行时能力。应区分:
- Sass/Less 变量:构建时替换;
- CSS 自定义属性:浏览器运行时仍然存在,可以被 JavaScript 修改;
- Sass/Less 循环:构建时生成 CSS;
- CSS
@for:原生 CSS 不提供同等的通用编译期循环; - Sass/Less 混入:构建时复用声明;
- CSS 原生嵌套、媒体查询嵌套和级联层:由浏览器解析;
- Sass/Less 函数:构建时计算;
- CSS 函数:浏览器运行时计算。
如果项目只需要变量、主题切换和简单嵌套,原生 CSS 可能已经足够;如果需要生成大量规则、复杂编译期计算、混入和模块化组织,Sass 或 Less 仍然有价值。
十五、选择和使用建议
- 新项目优先确认原生 CSS 是否已经满足需求;
- Sass 项目优先使用 Dart Sass、
@use和@forward; - Less 项目应在构建阶段编译,不建议生产环境运行 Less.js;
- 依赖应安装在项目本地,避免全局工具造成版本不一致;
- 控制嵌套深度,避免生成过高特异性的选择器;
- 优先使用参数化混入,谨慎使用
@extend和:extend; - 避免依赖 Less JavaScript 求值和随机编译结果;
- 变量、混入和函数应按照模块职责组织;
- 使用
math.div()等现代 Sass API,关注弃用提示; - 编译后检查生成的 CSS,特别是
calc()、颜色、阴影和媒体查询; - 在团队中统一预处理器、版本、格式化工具和构建命令。
总结
CSS 预处理器的价值不只是嵌套。Sass 和 Less 还提供了变量、混入、函数、条件、循环、导入、继承和编译期计算等能力。
但现代 CSS 已经逐步吸收了变量、嵌套、容器查询和级联层等能力,是否使用预处理器应根据项目需求决定。预处理器源代码最终仍然要编译为 CSS,不能把构建期语法和浏览器运行时能力混为一谈。