ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

CSS预处理器核心实战:Sass与Less嵌套和混入详解

CSS预处理器核心实战:Sass与Less嵌套和混入详解 原本手写 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“类名即混入”的设计守卫、模式匹配与 argumentsLess 的混入思路和 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 的分工该复制还是该合并写预处理器时间久了会遇到一个问题同样是复用样式用混入还是用extendLess 里类似: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它用的是 LibSassC 实现性能好但已经宣布弃用。新项目我更推荐dart-sass命令上就是sass包由官方维护持续支持新语法。如果你是前端工程里通过 webpack 或 Vite 使用对应的加载器会自动选择可用的实现但需要在依赖里明确。下面是我的最小化配置步骤以 Vite 项目为例安装依赖npm install sass -D在 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 {} }。这类错误编译不报错只会让你发现样式莫名其妙不生效排查起来很费劲。第二个是 Sassuse和变量继承的冲突。全局 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 迁移不妨就从今天开始给自己写第一个嵌套块、定义第一个混入试起用不了多久就能感受到这两样东西带来的变化。
返回列表