技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTCSS

CSS 预处理器,你还是只会嵌套么?

所属馆藏
CSS
文件格式
Markdown
原始路径
CSS/13-CSS预处理器,你还是只会嵌套么 ?
本文目录17 个章节
  1. 一、Sass 基础
  2. 二、Sass 运算
  3. 三、Sass 控制命令
  4. 四、Sass 函数
  5. 五、Sass、Less 与 Stylus
  6. 六、Less 的使用方式
  7. 七、Less 变量
  8. 八、Less 嵌套和媒体查询
  9. 九、Less 混入方法
  10. 十、Less 继承 :extend
  11. 十一、Less 导入
  12. 十二、Less 函数
  13. 十三、其他 Less 特性
  14. 十四、预处理器与现代 CSS 的边界
  15. 十五、选择和使用建议
  16. 总结
  17. 参考资料

CSS 预处理器,你还是只会嵌套么?

Category(分类): CSS Status: 已整理

CSS 预处理器可以在浏览器理解 CSS 之前,先通过构建工具把带有额外语法的源文件编译成标准 CSS。常见的预处理器包括 Sass/SCSS、Less 和 Stylus。

它们通常提供:

  • 变量;
  • 嵌套;
  • 混入(mixin);
  • 选择器扩展;
  • 函数;
  • 条件和循环;
  • 文件组织和模块化能力。

现代 CSS 已经提供了自定义属性、calc()、原生嵌套、媒体查询嵌套、容器查询和级联层等能力,因此预处理器不再是所有项目的必需品。但 Sass/Less 的编译期变量、混入、循环和函数仍然适合设计系统、主题生成和兼容旧项目。

CSS 预处理器示意图

一、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. oncemultiple

@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() 会让构建结果不可复现;
  • windowalert 等浏览器对象在 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 仍然有价值。

十五、选择和使用建议

  1. 新项目优先确认原生 CSS 是否已经满足需求;
  2. Sass 项目优先使用 Dart Sass、@use@forward
  3. Less 项目应在构建阶段编译,不建议生产环境运行 Less.js;
  4. 依赖应安装在项目本地,避免全局工具造成版本不一致;
  5. 控制嵌套深度,避免生成过高特异性的选择器;
  6. 优先使用参数化混入,谨慎使用 @extend:extend
  7. 避免依赖 Less JavaScript 求值和随机编译结果;
  8. 变量、混入和函数应按照模块职责组织;
  9. 使用 math.div() 等现代 Sass API,关注弃用提示;
  10. 编译后检查生成的 CSS,特别是 calc()、颜色、阴影和媒体查询;
  11. 在团队中统一预处理器、版本、格式化工具和构建命令。

总结

CSS 预处理器的价值不只是嵌套。Sass 和 Less 还提供了变量、混入、函数、条件、循环、导入、继承和编译期计算等能力。

但现代 CSS 已经逐步吸收了变量、嵌套、容器查询和级联层等能力,是否使用预处理器应根据项目需求决定。预处理器源代码最终仍然要编译为 CSS,不能把构建期语法和浏览器运行时能力混为一谈。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS