☰
CSS预处理器核心实战:Sass与Less嵌套和混入详解
2026/10/7 11:07:42 网站建设 项目流程

原本手写 CSS 的时候,大家最讨厌的两件事是什么?第一是选择器重复写,第二是一套样式换个值就得整体复制一遍。所以后来我接触到 Sass 和 Less 这类预处理器的时候,最直观的感受就是:原来 CSS 可以这样写。而在这套体系里,最值得花时间啃透的两个核心武器,就是嵌套(Nesting)和混入(Mixin)。这篇内容不是官方文档的翻译,是我把两种预处理器放在一起用、踩坑、返工之后整理出来的实操笔记,从语法差异到编译陷阱到模块化方案一次讲清楚,适合刚准备从原生 CSS 切换过来的同学,也适合已经用了一段时间但总觉得自己没玩明白的人。

1. 为什么要碰预处理器:嵌套与混入到底解决了什么问题

1.1 CSS 的三大痛点,嵌套和混入正好对症下药

先说原生 CSS 最烦人的场景。写一个简单的导航栏,你需要给 ul、li、a、a:hover 分别定义样式,每个选择器前都得带上同样的父级前缀。写完没多久要改结构,可能就要全局搜索替换。更别提多个组件共用同一套按钮样式时,你只能复制粘贴那一大段 border-radius、padding、transition。这种问题概括起来就是三个:选择器重复劳动、层级关系靠缩进全靠自觉、复用代码只能靠拷贝。

嵌套解决的是前两个问题。在 Sass 或 Less 里,你可以在一个选择器内部再去写它的后代、伪类甚至伪元素,层级关系直接跟着视觉结构走。比如:

.nav { background: #2c3e50; > li { display: inline-block; &:hover a { color: #3498db; } } }

这段代码编译出来,就是.nav内部的li和对应的 hover 状态。结构上一眼能看出.nav下面有什么,改层级的时候也不用一个文件里来回跳。混入解决的则是第三个问题。把需要复用的样式片段抽出来,起个名字,需要的地方像调用函数一样引入,参数不一样还可以传值进去。这样一套按钮样式,可以从大按钮到小按钮全部覆盖,而不用复制三个版本。

我实际用下来的感受是,这两个特性之所以被称为“核心”,是因为它们直接改变了写 CSS 时的思维模式。原生 CSS 是在写“扁平规则”,每一个选择器都要为它最终生成的规则负责;而预处理器让你先写“结构”,再填充“内容”,可维护性提升的幅度非常明显。

1.2 嵌套与混入的前置认知:先想清楚要不要入手

在决定引入 Sass 或 Less 之前,有个很实际的问题:你的项目值不值得上预处理器?如果只是一个一两个页面的静态页面,恭喜你,原生 CSS 配合自定义属性已经非常能打。但只要是持续迭代的项目,哪怕只有两三个工程师在维护,预处理器的收益都会随着版本迭代迅速放大。

从团队协作的角度说,嵌套规范了代码的书写结构,混入统一了组件的样式来源。比如一个公共的清除浮动混入,全团队都在用同一个定义,而不是每人写一套 clearfix。这种效果是原生 CSS 很难做到的,因为你无法保证每个人都会遵守某个约定。但混入是技术层面的复用,它直接把“标准答案”锁死在代码里,想写错都不容易。

另外要注意,选 Sass 还是 Less,不是简单的“听说哪个流行用哪个”。我做过的项目里,两种都深度用过,后面我会具体对比它们的嵌套和混入语法差异。先给个大致结论:如果你看重思路严谨、需要复杂的逻辑控制,Sass 更顺手;如果追求配置简单、让写 CSS 的人没有额外学习压力,Less 也很不错。两者对嵌套和混入的核心支持都很完整,差异主要体现在细节。

2. 嵌套规则拆解:从选择器到属性的完整玩法

2.1 基础嵌套写法,以及关键符号 & 的使用

嵌套最直观的形态,是把后代选择器写进父选择器的大括号里。Sass 和 Less 在这点上几乎长得一样:

.card { .header { font-size: 16px; } .body { padding: 16px; } }

编译出来就是.card .header和.card .body。这是基础中的基础,新手很容易上手。但嵌套里真正的关键角色是&符号,它代表当前嵌套层级的选择器本身。最常见的两个场景,一个是伪类,一个是修饰类。

.btn { background: #3498db; &:hover { background: #2980b9; } &.active { background: #27ae60; } }

这里如果不加&,怎么写?只能用.btn:hover回到原来的写法。一旦你习惯了&,会发现它在修改命名空间时特别省事。比如某个组件在深色背景容器里要换套颜色,可以这样:

.nav { color: #fff; .dark-theme & { color: #ccc; } }

编译结果是.dark-theme .nav { color: #ccc; }。注意我故意把父级放到了前面,利用&可以改变最终选择的组成顺序。这个技巧用来处理主题切换很实用,因为你在.nav的代码块里就能看到它的“暗色主题分支”,不用跳回 HTML 结构里思考归属。

还有一个我经常用的点:嵌套配合:not或者:last-child之类的结构伪类时,&可以直接把当前元素写成前缀,例如:

.menu-item { & + & { border-left: 1px solid #e0e0e0; } }

这段生成的 CSS 是.menu-item + .menu-item,意思是相邻的菜单项之间加分隔线。这类逻辑用原生 CSS 也能写,但嵌套让这些规则紧密聚合在一起,代码的归属感强了很多。

2.2 属性嵌套、@at-root 这类进阶操作

除了选择器嵌套,Sass 和 Less 还支持属性嵌套。听起来很玄乎,其实原理就是把带相同前缀的属性拆开放到小括号或大括号里。拿字体属性举例,传统的写法是:

font-family: Arial, sans-serif; font-size: 16px; font-weight: bold;

用 Sass 可以写成:

.text { font: { family: Arial, sans-serif; size: 16px; weight: bold; } }

编译出来的 CSS 和手写版完全一致。这个能力的价值在写 border、background、margin 这类有子属性的地方体现得特别明显。Less 的写法略有不同,它是把属性名和后面的值用冒号分开,用嵌套块表示子属性:

.text { font: { family: Arial, sans-serif; size: 16px; } }

实际项目里,属性嵌套我用的频率不算最高,但有个好处值得一提:它能把同一逻辑实体的属性集中在一起维护。尤其写background时,background-image、background-position、background-size一串跟着层级关系缩进排列,视觉上比平铺要清晰。

再说@at-root,这是 Sass 的一个特性,Less 原生没有完全对等的语法。它的作用是把嵌套内部的选择器提升到最外层,跳出当前祖先:

.component { color: #333; @at-root .wrapper .component-cur { color: #f00; } }

生成结果是.wrapper .component-cur,和.component没有任何叠加关系。这个特性在处理“组件内部工具类”时很顺手,比如想让某个工具类不受组件上下文影响,直接提升到根级别。

2.3 优先级和 CSS 体积:嵌套要避开的几个坑

嵌套虽好,但滥用起来会带来两个很头疼的问题:选择器优先级失控和 CSS 体积膨胀。

先讲优先级。嵌套会让后代选择器一层层累积。例如我们写了一个三层结构:

.page { .content { .article { .title { color: #000; } } } }

对应的选择器就是.page .content .article .title,特异性高达 4 个类。后期任何想要覆盖这个标题颜色的规则,至少也得有相同或更高的特异性才能生效。这导致最经典的翻车场景:你想在某个页面微调标题颜色,却发现样式不生效,最后只能在后面再加选择器或者干脆上!important。一个!important出来,后面的人想改就得继续叠,形成恶性循环。

嵌套过深还会直接放大编译后的 CSS 体积。如果你嵌套了 6 层,那么中间层级的每一个组合都会在最终样式中生成一次选择器,里面哪怕只定义了一条属性,整个选择器也必须完整输出。所以我给自己定了一个实操规则:嵌套尽量控制在 3 层以内,并且只需要嵌套“必须体现从属关系”的结构,不要给所有元素都套上父子关系。

还有一个容易忽略的盲区:嵌套会改变媒体查询的书写方式。Sass 允许在嵌套块内部直接写@media:

.card { width: 960px; @media (max-width: 768px) { width: 100%; } }

编译后浏览器会理解为一个媒体查询块里的.card宽度覆盖。这种写法的好处是响应式规则跟着组件走,缺点是如果你在多个组件里都写了媒体查询,最终 CSS 里会出现很多个重复的@media块,文件体积稍微变大。对于现在的网络环境不算致命,但能省则省,更好的实践是把媒体查询按断点抽成混入,再由各组件统一调用。

3. 混入(Mixin)核心机制:Sass 与 Less 的一次对比

3.1 Sass 混入的完整语法与典型场景

Sass 的混入用@mixin定义,用@include引入。最小的混入不需要任何参数,比如:

@mixin clearfix { &::after { content: ''; display: table; clear: both; } }

引入时直接@include clearfix;就可以了。带参数的混入是更常规的玩法,和函数传参的感觉很像:

@mixin btn-variant($bg, $color: #fff) { background: $bg; color: $color; border: 1px solid darken($bg, 10%); }

这里$bg是必填参数,$color带了默认值。调用时可以按位置传,也可以用命名参数,可读性更好:

.btn-danger { @include btn-variant($bg: #e74c3c, $color: #fff); }

Sass 还支持可变参数...,适合内置一组灵活的参数不确定的场景。我常用它做按钮尺寸混入:

@mixin size($args...) { width: map-get($args, width); height: map-get($args, height); }

不过说实话,参数超过四五个以后,这种写法会开始难读。我更推荐把参数打包成一个 Sass map 传入,后续扩展参数不容易打乱调用顺序。

除了简单的属性集合,混入还能包含整个选择器块,配合@content可以把使用方传入的内容嵌入混入内部。比如一个响应式工具混入:

@mixin respond-to($breakpoint) { @if $breakpoint == mobile { @media (max-width: 767px) { @content; } } @else if $breakpoint == tablet { @media (min-width: 768px) and (max-width: 1023px) { @content; } } }

使用方就在混入的括号后面再写一层内容:

.card { font-size: 16px; @include respond-to(mobile) { font-size: 14px; } }

这种写法把断点判断逻辑从组件代码里拿掉了,组件里只表达意图,复杂度收敛在混入内部,团队协作时尤其舒服。

3.2 Less“类名即混入”的设计:守卫、模式匹配与 @arguments

Less 的混入思路和 Sass 不太一样,它直接用类选择器当作混入名。定义一个类:

.rounded-corners(@radius: 4px) { border-radius: @radius; -webkit-border-radius: @radius; }

调用的时候,既可以像普通类那样直接写.rounded-corners;,也可以带参数.rounded-corners(6px);。这个设计让 Less 的上手成本非常低,因为你不是在学习一套新的@mixin/@include语法,而是“多写一个类名”而已。

但 Less 真正的差异化特性是守卫(Guard)。它用when关键字给混入加上条件判断,比如:

.text-color(@color) when (lightness(@color) >= 50%) { color: #000; } .text-color(@color) when (lightness(@color) < 50%) { color: #fff; }

调用.text-color(#333);时,因为#333的明度很低,会匹配第二段规则,文字变白色。这种模式相当于给同一个混入拆出了多个分支。配合@arguments变量可以拿到传进来的所有参数:

.box-shadow(@x: 0, @y: 0, @blur: 10px, @color: rgba(0,0,0,0.2)) { box-shadow: @arguments; }

@arguments会把四个参数原样拼成一个空格分隔的列表,省去了写一堆重复变量名的过程。

还有一个我很常用的模式匹配玩法:用混入的第一个参数区分形态,再配合when做内部判断。比如做一个简易的图标混入,参数传字体、双色、线性渐变等不同形态,内部自动切换属性输出。这类逻辑你在原生 CSS 里几乎不可能优雅地实现,但 Less 混入写起来很流畅。

3.3 Sass/Less 混入能力对比速查

为了帮大家快速做技术选型,我整理了一份自己常用的对比表。

能力点SassLess
混入定义语法@mixin name{}类选择器(可带参数括号)
混入引入语法@include name;name;或.name();
默认参数支持支持
命名参数支持不支持(按位置传递)
可变参数支持...支持...,不过细节略不同
内容块注入支持@content通过@arguments间接处理
条件逻辑@if/@elsewhen守卫
循环生成@each/@for/@while递归混入模拟
作用域局部变量默认值覆盖方便变量延迟加载要注意调用顺序

从团队维护的角度来说,如果项目里的样式逻辑比较重、需要大量按条件生成不同效果,Sass 的@if/@each/@content这套组合拳更完整。如果团队以前是写 CSS 为主,不想引入太多编程概念,Less 的学习曲线平滑很多,毕竟“把类当函数用”已经有天然直觉的支撑。

4. 混入进阶玩法与组件化实战

4.1 用混入做参数化样式工厂:从按钮到多主题颜色

讲了这么多语法和差异,来看点能直接抄作业的例子。我用混入构建一个小小的样式工厂,目标是实现一套按钮体系,支持普通按钮、带角度变体、不同尺寸、禁用状态。

先定义一个基础混入:

@mixin button-base($radius: 4px) { display: inline-block; padding: 8px 16px; border: none; border-radius: $radius; font-size: 14px; cursor: pointer; transition: background 0.2s ease; &:disabled { cursor: not-allowed; opacity: 0.6; } }

再定义颜色变体混入:

@mixin button-color($bg, $fg: #fff) { background-color: $bg; color: $fg; &:hover:not(:disabled) { background-color: darken($bg, 8%); } &:active:not(:disabled) { background-color: darken($bg, 12%); } }

最后定义尺寸变体:

@mixin button-size($padding-y, $padding-x, $font-size) { padding: $padding-y $padding-x; font-size: $font-size; }

组件调用时只写意图,不写原始样式:

.btn-primary { @include button-base(6px); @include button-color(#3498db); @include button-size(10px, 20px, 14px); }

这样三个混入可以自由组合,新增一个按钮类型时,调用几条混入就结束了。同样的思路换成多主题场景,把颜色值换成 Sass map 或 CSS 变量,主题扩展就变成改数据,不用到处改选择器。

这套方法在 Less 里也成立,只是把@include换成类名调用。我实测下来的体会是:混入的粒度一定要控制在“一个混入做一件事”。一开始我偷懒把颜色、尺寸、圆角都塞进一个混入里,后期加需求时动一次混入,所有调用它的组件全跟着变,风险非常大。拆开以后反而灵活得多。

4.2 混入与 @extend 的分工:该复制还是该合并

写预处理器时间久了会遇到一个问题:同样是复用样式,用混入还是用@extend(Less 里类似&:extend)。先看混入的产出逻辑。它每次被调用,都会把混入里的完整样式复制一份到调用位置。假设有三处按钮调用同一个混入,最终生成的 CSS 里就有三份相同的声明块。这样做的优势是支持带参数、支持逻辑分支,缺点是体积略大。

@extend的产出逻辑完全不同。它不会复制样式,而是把当前选择器合并到被继承的选择器后面。举个例子:

.error { border: 1px solid #e74c3c; color: #e74c3c; } .serious-error { @extend .error; font-weight: bold; }

编译出来会让.serious-error和.error共享同一个规则块:

.error, .serious-error { border: 1px solid #e74c3c; color: #e74c3c; } .serious-error { font-weight: bold; }

这在 CSS 体积上更省,但它有两个限制。一是被继承的选择器必须是静态的,你没法给@extend传参数。二是@extend会把选择器合并进同一规则,如果被继承的选择器本身在项目里被改动,所有继承它的选择器都会跟着变,这是个隐性耦合。

所以我自己的取舍标准很简单:如果是“完全相同的静态样式片段”,用@extend合并输出,省体积;如果需要参数化、带条件分支或者可变的模块,用混入。很多风格指南把这个说成“只让 extend 用于修饰关系”,我觉得更准确的说法是:extend 适合给同族组件补修饰符,混入适合做跨场景的样式套件。平时尽量优先用混入,只有当你确认某组样式永不变化且不需要传参时,才考虑用 extend。

4.3 混入结合循环与逻辑控制,批量生成工具类

Sass 有个组合能力非常强大:混入配合@each循环批量生成工具类。我在做一套间距工具类的时候,几乎是一行一行的重复代码在写,后来改成循环,整个生成逻辑干净了很多。

$spacing-map: (xs: 4px, sm: 8px, md: 16px, lg: 24px); @each $name, $val in $spacing-map { .m-#{$name} { margin: $val; } .mt-#{$name} { margin-top: $val; } .mb-#{$name} { margin-bottom: $val; } }

这段循环一跑,12 个工具类直接生成,后面想管理或扩展,只要改 map 就好。由于引进了循环,混入内部的逻辑判断也变得更加自由。一个典型的场景是同时输出默认样式和 hover 样式,可以用@if控制是否有后缀:

@mixin gen-color($class-name, $color, $hover: true) { .#{$class-name} { color: $color; @if $hover { &:hover { color: darken($color, 5%); } } } }

Less 没有原生的@each,但可以用递归混入实现。下面是一个生成列宽工具类的示例:

.gen-cols(@i, @n) when (@i <= @n) { .col-@{i} { width: percentage((@i / @n)); } .gen-cols((@i + 1), @n); } .gen-cols(1, 4);

这种写法需要一点“把循环改成递归”的思维,一开始不太习惯,但掌握后也能写出高效的批量生成代码。如果你确实需要大量的循环逻辑,Sass 的体验还是要好不少。

这里附带一个性能提示:循环生成大量工具类虽然方便,但编译时间和产物体积都会涨。生产环境建议只生成实际用到的子集,或者配合 PurgeCSS 这类工具把没有用到的类删掉。我见过有人为了图方便生成 300 个间距类,结果编译产物好几大百 KB,页面加载压力全浪费在无用样式上。

5. 编译配置与常见问题排查实录

5.1 编译环境选型:dart-sass 与 node-sass 怎么选

聊到实际落地,必须先说清楚一个容易混淆的点:Sass 的编译实现不止一种。老项目里常碰到node-sass,它用的是 LibSass(C++ 实现),性能好但已经宣布弃用。新项目我更推荐dart-sass,命令上就是sass包,由官方维护,持续支持新语法。如果你是前端工程里通过 webpack 或 Vite 使用,对应的加载器会自动选择可用的实现,但需要在依赖里明确。

下面是我的最小化配置步骤,以 Vite 项目为例:

  1. 安装依赖:
npm install sass -D
  1. 在 Vite 配置文件里设置 CSS 预处理选项:
export default defineConfig({ css: { preprocessorOptions: { scss: { additionalData: `@use "@/styles/variables.scss" as *;` } } } });

这里的additionalData相当于每个 SCSS 文件自动注入公共变量,不用每个文件都手动@use一次。Less 的配置逻辑是一样的,只是 key 换成less,并在 preprocessorOptions 里设置javascriptEnabled等选项。

注意一个坑:Sass 的@import已经在官方文档里被标记为逐步淘汰,新项目推荐@use。但@use引入后,变量和混入默认带命名空间,开头我是踩过坑的。比如@use "variables",调用变量要写成variables.$primary。想要直接引用$primary,需要加as *,我在上面的例子里写的就是这个思路。这个细节直接关系到你在 mixin 里能不能直接使用全局变量,值得多留意。

5.2 嵌套和混入的典型翻车现场

先把我在实际开发里遇到过的经典问题整理成一张排查速查表,都是前人踩烂了但还会再踩的坑。

症状原因解决思路
编译后选择器比预期长很多嵌套层级过深,每一层都生成后代选择器拆分组件,用&精确表达当前选择器
某个混入引入后整块样式丢失混入名与全局环境冲突,或变量拼写不一致检查变量作用域,用@debug输出变量值
@extend报无法扩展复杂选择器被扩展的选择器带组合符或嵌套占位符改用混入复制样式,或把基础样式定义为占位符选择器%
浏览器缓存旧代码,新样式不生效编译产物未强缓存更新确认 CI 流程的 cache 目录清理策略
Less 引入变量后样式不一致Less 变量具备延迟加载,变量赋值顺序影响最终值重新理解 Less 作用域,把变量统一到入口文件

单独展开说两个最值得注意的案例。第一个是嵌套里的选择器拼接把&用错了位置。有位同事想给.card下的图片写一个修饰类,结果写成了:

.card { .image & { width: 50%; } }

编译过去变成.image .card,完全反了。正确写法是.card .image {}或.card { .image {} }。这类错误编译不报错,只会让你发现样式莫名其妙不生效,排查起来很费劲。

第二个是 Sass@use和变量继承的冲突。全局 variables 模块没有通过as *导出时,混入里用到$primary会直接编译失败。改用命名空间写法后,混入内部每次都得写variables.$primary,代码很啰嗦,最后统一在入口文件里用@use "variables" as *;才彻底解决。

5.3 排查思路:从 sourcemap 到生成结果比对

遇到编译不过或者样式不对的情况,最有效的第一步是开 sourcemap。Vite 或 webpack 里配置css.sourcemap: true或者devtool相关选项,就能在浏览器开发者工具的样式面板里直接看到当前规则对应的是源文件第几行的 SCSS。这么一来,嵌套层级过深、混入重复展开这类问题,能一眼定位到源头。

如果开了 sourcemap 还是没问题但页面样式就是不对,我建议做一次“生成结果比对”。直接执行一次完整编译命令,把输出 CSS 单独打开,看具体生成了什么。很多时候 SCSS 写的时候感觉很整洁,但编译出来的选择器嵌套关系和你想象的不一样。比如 Attributes 选择器与伪类的组合、群组选择器合并等,看编译产物是最直观的验证方式。

Less 的排查与 Sass 类似,但它报错信息里有时候不提示具体行号。遇到这种情况,我习惯用最小化复现法:新建一个临时文件,把出问题的 SCSS/Less 片段单独放进去编译,如果能复现就把代码块继续缩小,直到找到触发点。这个方法听起来笨,但排查效率和经验值增长非常快。我自己很多对预处理器的深入理解都是这么一点点逼出来的。

最后再分享一个个人操作习惯。我现在写大型项目的样式,会强制自己遵守一条规则:嵌套不超过四层,混入只管单一职责,公共样式能抽成混入或变量就坚决不复制。每次拿到一个新需求,先想用什么混入组合,而不是直接写选择器。这套习惯坚持一年之后,最大的收益不是代码写得多快,而是改样式的时候几乎没有翻过车。预处理器本身并不是什么高深的技术,真正考验人的是你能不能把它当作一套规范来约束自己。如果你正准备从原生 CSS 迁移,不妨就从今天开始给自己写第一个嵌套块、定义第一个混入试起,用不了多久就能感受到这两样东西带来的变化。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询