Day19_LESS 完全指南——从入门到工程实践

LESS 完全指南------从入门到工程实践

一篇覆盖 LESS 全核心特性、配以完整可运行示例与真实场景解析的深度技术博客。


目录

  1. [什么是 CSS 预处理器](#什么是 CSS 预处理器)
  2. [LESS 简介与核心概念](#LESS 简介与核心概念)
  3. [LESS 的编译方式](#LESS 的编译方式)
    • 3.1 [使用 less.js 在浏览器端编译](#使用 less.js 在浏览器端编译)
    • 3.2 [使用 VSCode 插件自动编译](#使用 VSCode 插件自动编译)
    • 3.3 [使用 Node.js CLI 编译](#使用 Node.js CLI 编译)
    • 3.4 [LESS 编译原理深度解析](#LESS 编译原理深度解析)
  4. [LESS 核心语法详解](#LESS 核心语法详解)
  5. [LESS 知识脉络总结](#LESS 知识脉络总结)
    • 5.5 [特性 × 真实网站应用矩阵](#特性 × 真实网站应用矩阵)
  6. [PC 项目实战:用 LESS 构建完整页面](#PC 项目实战:用 LESS 构建完整页面)
    • 6.0 [工程准备(container 与公共混合)](#工程准备(container 与公共混合))
  7. 经典应用场景与真实网站案例
  8. [LESS vs SASS/SCSS 对比](#LESS vs SASS/SCSS 对比)
  9. 工程最佳实践与文件组织
    • 9.5 [Design Token 命名体系](#Design Token 命名体系)
    • 9.6 [LESS 与 CSS 自定义属性混合策略](#LESS 与 CSS 自定义属性混合策略)
    • 9.7 [Vite 项目集成 LESS](#Vite 项目集成 LESS)
  10. 常见问题与避坑指南
  11. 总结

1 什么是 CSS 预处理器

名词解释

术语 说明
CSS 预处理器 一种在 CSS 之上的脚本语言扩展,通过编译器将其转换为标准 CSS。提供变量、函数、混合、嵌套等编程能力。
编译(Compile) 将预处理器语言(.less / .scss)翻译为浏览器可直接解析的 .css 文件的过程。
Source Map 编译产物与源文件之间的映射文件,供浏览器 DevTools 调试时定位原始 .less 位置。
AST(抽象语法树) 编译器将源代码解析后形成的树形数据结构,是代码转换的中间表示。

为什么需要 CSS 预处理器

原生 CSS 存在以下局限:

  • 无变量:修改主题色需要全文件搜索替换
  • 无嵌套:父子关系选择器需要重复书写父选择器
  • 无复用:相同的一组属性无法抽取为"函数"
  • 无运算:尺寸计算必须手工完成,难以响应式自适应

CSS 预处理器通过引入编程范式,极大提升了样式代码的可维护性和开发效率。

主流 CSS 预处理器

#mermaid-svg-WROw4812nreltf8I{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WROw4812nreltf8I .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WROw4812nreltf8I .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WROw4812nreltf8I .error-icon{fill:#552222;}#mermaid-svg-WROw4812nreltf8I .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WROw4812nreltf8I .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WROw4812nreltf8I .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WROw4812nreltf8I .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WROw4812nreltf8I .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WROw4812nreltf8I .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WROw4812nreltf8I .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WROw4812nreltf8I .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WROw4812nreltf8I .marker.cross{stroke:#333333;}#mermaid-svg-WROw4812nreltf8I svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WROw4812nreltf8I p{margin:0;}#mermaid-svg-WROw4812nreltf8I .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WROw4812nreltf8I .cluster-label text{fill:#333;}#mermaid-svg-WROw4812nreltf8I .cluster-label span{color:#333;}#mermaid-svg-WROw4812nreltf8I .cluster-label span p{background-color:transparent;}#mermaid-svg-WROw4812nreltf8I .label text,#mermaid-svg-WROw4812nreltf8I span{fill:#333;color:#333;}#mermaid-svg-WROw4812nreltf8I .node rect,#mermaid-svg-WROw4812nreltf8I .node circle,#mermaid-svg-WROw4812nreltf8I .node ellipse,#mermaid-svg-WROw4812nreltf8I .node polygon,#mermaid-svg-WROw4812nreltf8I .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WROw4812nreltf8I .rough-node .label text,#mermaid-svg-WROw4812nreltf8I .node .label text,#mermaid-svg-WROw4812nreltf8I .image-shape .label,#mermaid-svg-WROw4812nreltf8I .icon-shape .label{text-anchor:middle;}#mermaid-svg-WROw4812nreltf8I .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WROw4812nreltf8I .rough-node .label,#mermaid-svg-WROw4812nreltf8I .node .label,#mermaid-svg-WROw4812nreltf8I .image-shape .label,#mermaid-svg-WROw4812nreltf8I .icon-shape .label{text-align:center;}#mermaid-svg-WROw4812nreltf8I .node.clickable{cursor:pointer;}#mermaid-svg-WROw4812nreltf8I .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WROw4812nreltf8I .arrowheadPath{fill:#333333;}#mermaid-svg-WROw4812nreltf8I .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WROw4812nreltf8I .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WROw4812nreltf8I .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WROw4812nreltf8I .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WROw4812nreltf8I .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WROw4812nreltf8I .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WROw4812nreltf8I .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WROw4812nreltf8I .cluster text{fill:#333;}#mermaid-svg-WROw4812nreltf8I .cluster span{color:#333;}#mermaid-svg-WROw4812nreltf8I div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WROw4812nreltf8I .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WROw4812nreltf8I rect.text{fill:none;stroke-width:0;}#mermaid-svg-WROw4812nreltf8I .icon-shape,#mermaid-svg-WROw4812nreltf8I .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WROw4812nreltf8I .icon-shape p,#mermaid-svg-WROw4812nreltf8I .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WROw4812nreltf8I .icon-shape .label rect,#mermaid-svg-WROw4812nreltf8I .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WROw4812nreltf8I .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WROw4812nreltf8I .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WROw4812nreltf8I :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} CSS 预处理器
LESS
SASS / SCSS
Stylus
Bootstrap v3 采用
Bootstrap v4+ 采用
Vue CLI 默认支持
Vue.js 早期版本


2 LESS 简介与核心概念

LESS (Leaner Style Sheets)是一种向后兼容 CSS 的样式语言扩展,由 Alexis Sellier 于 2009 年创建,后由 Less.js 核心团队维护。官方网站:https://lesscss.org,中文文档:https://less.bootcss.com

LESS 的核心特性概览

#mermaid-svg-VuzSpb5SLzwwmO0A{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VuzSpb5SLzwwmO0A .error-icon{fill:#552222;}#mermaid-svg-VuzSpb5SLzwwmO0A .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VuzSpb5SLzwwmO0A .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VuzSpb5SLzwwmO0A .marker.cross{stroke:#333333;}#mermaid-svg-VuzSpb5SLzwwmO0A svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VuzSpb5SLzwwmO0A p{margin:0;}#mermaid-svg-VuzSpb5SLzwwmO0A .edge{stroke-width:3;}#mermaid-svg-VuzSpb5SLzwwmO0A .section--1 rect,#mermaid-svg-VuzSpb5SLzwwmO0A .section--1 path,#mermaid-svg-VuzSpb5SLzwwmO0A .section--1 circle,#mermaid-svg-VuzSpb5SLzwwmO0A .section--1 polygon,#mermaid-svg-VuzSpb5SLzwwmO0A .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .section--1 text{fill:#ffffff;}#mermaid-svg-VuzSpb5SLzwwmO0A .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-depth--1{stroke-width:17;}#mermaid-svg-VuzSpb5SLzwwmO0A .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled circle,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:lightgray;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:#efefef;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-0 rect,#mermaid-svg-VuzSpb5SLzwwmO0A .section-0 path,#mermaid-svg-VuzSpb5SLzwwmO0A .section-0 circle,#mermaid-svg-VuzSpb5SLzwwmO0A .section-0 polygon,#mermaid-svg-VuzSpb5SLzwwmO0A .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-VuzSpb5SLzwwmO0A .section-0 text{fill:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .node-icon-0{font-size:40px;color:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-depth-0{stroke-width:14;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled circle,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:lightgray;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:#efefef;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-1 rect,#mermaid-svg-VuzSpb5SLzwwmO0A .section-1 path,#mermaid-svg-VuzSpb5SLzwwmO0A .section-1 circle,#mermaid-svg-VuzSpb5SLzwwmO0A .section-1 polygon,#mermaid-svg-VuzSpb5SLzwwmO0A .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .section-1 text{fill:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .node-icon-1{font-size:40px;color:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-depth-1{stroke-width:11;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled circle,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:lightgray;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:#efefef;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-2 rect,#mermaid-svg-VuzSpb5SLzwwmO0A .section-2 path,#mermaid-svg-VuzSpb5SLzwwmO0A .section-2 circle,#mermaid-svg-VuzSpb5SLzwwmO0A .section-2 polygon,#mermaid-svg-VuzSpb5SLzwwmO0A .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .section-2 text{fill:#ffffff;}#mermaid-svg-VuzSpb5SLzwwmO0A .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-depth-2{stroke-width:8;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled circle,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:lightgray;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:#efefef;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-3 rect,#mermaid-svg-VuzSpb5SLzwwmO0A .section-3 path,#mermaid-svg-VuzSpb5SLzwwmO0A .section-3 circle,#mermaid-svg-VuzSpb5SLzwwmO0A .section-3 polygon,#mermaid-svg-VuzSpb5SLzwwmO0A .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .section-3 text{fill:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .node-icon-3{font-size:40px;color:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-depth-3{stroke-width:5;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled circle,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:lightgray;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:#efefef;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-4 rect,#mermaid-svg-VuzSpb5SLzwwmO0A .section-4 path,#mermaid-svg-VuzSpb5SLzwwmO0A .section-4 circle,#mermaid-svg-VuzSpb5SLzwwmO0A .section-4 polygon,#mermaid-svg-VuzSpb5SLzwwmO0A .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .section-4 text{fill:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .node-icon-4{font-size:40px;color:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-depth-4{stroke-width:2;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled circle,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:lightgray;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:#efefef;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-5 rect,#mermaid-svg-VuzSpb5SLzwwmO0A .section-5 path,#mermaid-svg-VuzSpb5SLzwwmO0A .section-5 circle,#mermaid-svg-VuzSpb5SLzwwmO0A .section-5 polygon,#mermaid-svg-VuzSpb5SLzwwmO0A .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .section-5 text{fill:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .node-icon-5{font-size:40px;color:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-depth-5{stroke-width:-1;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled circle,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:lightgray;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:#efefef;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-6 rect,#mermaid-svg-VuzSpb5SLzwwmO0A .section-6 path,#mermaid-svg-VuzSpb5SLzwwmO0A .section-6 circle,#mermaid-svg-VuzSpb5SLzwwmO0A .section-6 polygon,#mermaid-svg-VuzSpb5SLzwwmO0A .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .section-6 text{fill:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .node-icon-6{font-size:40px;color:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-depth-6{stroke-width:-4;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled circle,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:lightgray;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:#efefef;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-7 rect,#mermaid-svg-VuzSpb5SLzwwmO0A .section-7 path,#mermaid-svg-VuzSpb5SLzwwmO0A .section-7 circle,#mermaid-svg-VuzSpb5SLzwwmO0A .section-7 polygon,#mermaid-svg-VuzSpb5SLzwwmO0A .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .section-7 text{fill:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .node-icon-7{font-size:40px;color:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-depth-7{stroke-width:-7;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled circle,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:lightgray;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:#efefef;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-8 rect,#mermaid-svg-VuzSpb5SLzwwmO0A .section-8 path,#mermaid-svg-VuzSpb5SLzwwmO0A .section-8 circle,#mermaid-svg-VuzSpb5SLzwwmO0A .section-8 polygon,#mermaid-svg-VuzSpb5SLzwwmO0A .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .section-8 text{fill:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .node-icon-8{font-size:40px;color:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-depth-8{stroke-width:-10;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled circle,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:lightgray;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:#efefef;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-9 rect,#mermaid-svg-VuzSpb5SLzwwmO0A .section-9 path,#mermaid-svg-VuzSpb5SLzwwmO0A .section-9 circle,#mermaid-svg-VuzSpb5SLzwwmO0A .section-9 polygon,#mermaid-svg-VuzSpb5SLzwwmO0A .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .section-9 text{fill:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .node-icon-9{font-size:40px;color:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-depth-9{stroke-width:-13;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled circle,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:lightgray;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:#efefef;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-10 rect,#mermaid-svg-VuzSpb5SLzwwmO0A .section-10 path,#mermaid-svg-VuzSpb5SLzwwmO0A .section-10 circle,#mermaid-svg-VuzSpb5SLzwwmO0A .section-10 polygon,#mermaid-svg-VuzSpb5SLzwwmO0A .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .section-10 text{fill:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .node-icon-10{font-size:40px;color:black;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .edge-depth-10{stroke-width:-16;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled circle,#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:lightgray;}#mermaid-svg-VuzSpb5SLzwwmO0A .disabled text{fill:#efefef;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-root rect,#mermaid-svg-VuzSpb5SLzwwmO0A .section-root path,#mermaid-svg-VuzSpb5SLzwwmO0A .section-root circle,#mermaid-svg-VuzSpb5SLzwwmO0A .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-VuzSpb5SLzwwmO0A .section-root text{fill:#ffffff;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-root span{color:#ffffff;}#mermaid-svg-VuzSpb5SLzwwmO0A .section-2 span{color:#ffffff;}#mermaid-svg-VuzSpb5SLzwwmO0A .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-VuzSpb5SLzwwmO0A .edge{fill:none;}#mermaid-svg-VuzSpb5SLzwwmO0A .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-VuzSpb5SLzwwmO0A :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} LESS
变量 Variables
属性值变量
选择器变量
属性名变量
媒体查询变量
混合 Mixins
无参混合
有参混合
默认值混合
arguments 关键字
嵌套 Nesting
层级选择器
& 父引用
媒体查询嵌套
运算 Operations
加减乘除
单位处理
函数 Functions
颜色函数
数学函数
字符串函数
条件 Guards
when 条件
多重条件
导入 Import
LESS 文件导入
CSS 文件导入

2.1 学习脉络:从语法到页面工程

掌握 LESS 的推荐路径是:先能编译 → 再掌握变量与混合 → 用嵌套与导入组织工程 → 用 Guards 与函数处理复杂 UI → 最后落到完整 PC 页面模块

【代码注释】

核心逻辑

  • 学习路径分三层:编译 (LESS→CSS)→ 语法 (变量/混合/嵌套)→ 工程@import 拆分 + PC 页面模块)。
  • Mermaid 流程图从 less.js / Easy Less / lessc 入口,经语法模块,落到 variables.lessmain.less 工程链。

关键概念

解决的问题
编译层 源文件如何变成浏览器可读的 .css
语法层 如何消除重复选择器、硬编码颜色
工程层 多人协作时文件如何拆分与入口汇总

注意点

  • 生产环境禁止 依赖浏览器 less.js 实时编译(阻塞渲染、体积大)。
  • 先掌握 VSCode Easy Less 或 Vite/Webpack 预编译,再写复杂页面。

实战场景

  • 个人练手:Easy Less 保存即出 CSS;团队项目:Vite preprocessorOptions.less + additionalData 全局注入变量。

2.2 语法模块与实战模块对照

序号 学习主题 核心语法点 典型产出
01 编译 LESS type="text/less"less.js、Easy Less 得到可引用的 .css
02 注释 /* */ vs // 可维护的模块说明
03 变量 @var@{prop}@{sel}、转义 ~"..." 主题色、断点、动态选择器
04 混合 无参/有参/默认值、@arguments 居中、清除浮动、按钮皮肤
05 条件判断 when、多方向三角形 下拉箭头、自适应间距
06 导入 @import、导入顺序 多文件工程入口
07 嵌套 后代/子代/兄弟、& 导航、列表、状态类
08 混合+嵌套 命名空间、&::after 组件库内部组织
09 运算符 + - * /percentage() 栅格宽度、响应式尺寸
10 函数 darkenfademix 色阶、半透明遮罩
00 工程准备 .container@import 统一版心与重置
01 Topbar Flex、&:not(:last-child) 顶部链接栏
02 Logo+搜索 浮动/布局、darken() 品牌区与搜索框
03 页面导航 嵌套 li&.active 主导航高亮

2.3 官方文档与延伸阅读

来源 地址 适合查阅的内容
LESS 官方 lesscss.org 语法、语言特性、变更说明
LESS 中文站 less.bootcss.com 中文语法说明、函数列表
函数参考 less.bootcss.com/functions 颜色/数学/字符串函数
MDN @import developer.mozilla.org CSS @import 编译后 CSS 中 @import 的行为
Bootstrap 3 LESS 源码 开源仓库中的 less/ 目录 大型项目如何用变量+混合+栅格组织样式

【代码注释】

核心逻辑

  • 对照表把「课堂序号」映射到「语法点」与「页面产出」(Topbar、Logo、导航等),便于按模块复习。
  • 官方文档负责 API 准确性;Bootstrap 3 less/ 源码负责工程组织范例。

阅读 Bootstrap 源码时重点看

关注点 原因
变量命名 @brand-primary Design Token 雏形
Mixin 是否带 () 区分「仅复用」与「输出类」
嵌套深度 超过 3~4 层难维护
bootstrap.less 入口 @import 顺序即依赖关系

实战场景

  • 面试「如何组织大型 LESS 项目」→ 答 variables + mixins + components + 单一入口编译。

3 LESS 的编译方式

3.1 使用 less.js 在浏览器端编译

这是最简单的上手方式,适合快速原型验证,不推荐用于生产环境(每次页面加载都需要实时编译,性能较差)。

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 浏览器端编译示例</title>

  <!--
    关键:type 必须写 "text/less",而非 "text/css"
    浏览器不会直接解析,由 less.js 接管编译
  -->
  <style type="text/less">
    /* 定义变量 */
    @primary-color: #1a73e8;
    @font-size-base: 16px;
    @border-radius: 4px;

    /* 使用变量 */
    .card {
      width: 320px;
      padding: 20px;
      border: 1px solid @primary-color;
      border-radius: @border-radius;
      font-size: @font-size-base;
      color: @primary-color;

      .card-title {
        font-size: (@font-size-base * 1.25);
        font-weight: bold;
        margin-bottom: 12px;
      }

      .card-body {
        line-height: 1.6;
        color: #333;
      }
    }
  </style>

  <!-- less.js 必须在所有 type="text/less" 的 style 标签之后引入 -->
  <script src="./less.js"></script>
</head>
<body>
  <div class="card">
    <div class="card-title">卡片标题</div>
    <div class="card-body">这是卡片正文内容,演示 LESS 浏览器端实时编译。</div>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  1. <style type="text/less"> 内写 LESS 语法(变量、嵌套)。
  2. 页面底部引入 less.js,DOM Ready 后扫描并编译,动态插入 <style> 标准 CSS。
  3. 浏览器最终解析的是编译后的 CSS,而非 .less 原文。

关键 API

  • type="text/less":必须写对,写成 text/css 则变量语法无效。
  • @变量名:编译期替换为字面量,如 @primary-color#1a73e8

注意点

  • less.js 必须放在所有 text/less 的 style 之后
  • 嵌套 .card-title 会展开为 .card .card-title,与原生 CSS 等价。

实战场景

  • 课堂演示、CodePen 快速原型;勿用于生产(首屏需等 JS 编译,SEO/性能差)。

3.2 使用 VSCode 插件自动编译

这是开发阶段最推荐的工作流,零配置,保存即编译。

步骤:

  1. 在 VSCode 扩展市场搜索并安装 Easy Less
  2. 新建 style/index.less 文件,编写 LESS 代码
  3. 每次保存,Easy Less 自动在同目录生成 index.css
  4. HTML 中引用编译后的 CSS 文件
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>VSCode 插件编译示例</title>
  <!-- 引用编译后的 CSS,而非 .less 源文件 -->
  <link rel="stylesheet" href="./style/index.css">
</head>
<body>
  <div id="box">
    <div class="item">子元素</div>
    <p>段落文字</p>
  </div>
</body>
</html>

对应的 style/index.less

less 复制代码
@fcolor: #900;   /* 前景色(文字颜色) */
@bcolor: #099;   /* 背景色 */
@len:  1000px;   /* 容器主尺寸 */

#box {
    margin-top: 40px;
    padding: 20px;
    width: @len;
    height: (@len / 4);   /* 运算:高度为宽度的四分之一 */
    color: @fcolor;
    background: @bcolor;

    /* 嵌套:子元素选择器 */
    .item {
        width: 600px;
    }

    p {
        padding: 20px;
    }
}

编译后的 CSS:

css 复制代码
#box {
  margin-top: 40px;
  padding: 20px;
  width: 1000px;
  height: 250px;
  color: #900;
  background: #099;
}
#box .item {
  width: 600px;
}
#box p {
  padding: 20px;
}

【代码注释】

核心逻辑

  • 开发时写 index.less,保存后 Easy Less 调用 lessc 生成同目录 index.css
  • HTML 只链接 .css,浏览器不直接接触 LESS 源文件。

关键语法(本例)

LESS 编译后 CSS 说明
@len: 1000px width: 1000px 变量替换
height: (@len / 4) height: 250px 括号内才做除法
.item { ... } 嵌套在 #box #box .item { ... } 后代选择器展开

注意点

  • 除法 (@len / 4) 不加括号时,LESS 可能把 / 当 CSS 分隔符(见 §10.1)。
  • 修改 LESS 后需保存才会更新 CSS;部署时只上传 CSS。

实战场景

  • 尚硅谷 PC 项目、静态页教学:本地 Easy Less + 引用 style/index.css

3.3 使用 Node.js CLI 编译

适合构建流水线(CI/CD)或与 Webpack/Vite 集成。

bash 复制代码
# 全局安装 lessc
npm install -g less

# 编译单文件
lessc input.less output.css

# 编译并压缩输出
lessc --clean-css input.less output.min.css

# 生成 Source Map(方便调试)
lessc --source-map input.less output.css

Webpack 集成(项目中常见配置):

javascript 复制代码
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/,
        use: [
          'style-loader',    // 将 CSS 注入 DOM
          'css-loader',      // 处理 @import 和 url()
          'less-loader',     // 将 LESS 编译为 CSS
        ],
      },
    ],
  },
};

【代码注释】

核心逻辑

  • lessc input.less output.css:命令行单次编译,适合 CI/CD。
  • Webpack/Vite 在构建阶段调用 LESS 编译器,产物打入 dist/

Webpack Loader 链(从右到左执行)

复制代码
.less 文件 → less-loader(编译)→ css-loader(处理 @import/url)→ style-loader(注入 DOM)

关键命令

命令 作用
lessc a.less b.css 基础编译
lessc --clean-css 压缩输出
lessc --source-map 生成 .map 便于 DevTools 定位 .less 行号

注意点

  • 生产用 MiniCssExtractPlugin 提取独立 CSS,不用 style-loader 内联到 JS。
  • Vite 只需 npm i -D less,无需 less-loader(见 §9.7)。

实战场景

  • Jenkins/GitHub Actions 构建前 lessc;React/Vue 项目用构建工具内置管道。

3.4 LESS 编译原理深度解析

了解编译器内部原理,有助于排查奇怪的编译行为、理解变量作用域与懒加载的本质。

编译管线:从 .less.css

#mermaid-svg-XfwDPYx892JkWDnN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XfwDPYx892JkWDnN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XfwDPYx892JkWDnN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XfwDPYx892JkWDnN .error-icon{fill:#552222;}#mermaid-svg-XfwDPYx892JkWDnN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XfwDPYx892JkWDnN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XfwDPYx892JkWDnN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XfwDPYx892JkWDnN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XfwDPYx892JkWDnN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XfwDPYx892JkWDnN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XfwDPYx892JkWDnN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XfwDPYx892JkWDnN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XfwDPYx892JkWDnN .marker.cross{stroke:#333333;}#mermaid-svg-XfwDPYx892JkWDnN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XfwDPYx892JkWDnN p{margin:0;}#mermaid-svg-XfwDPYx892JkWDnN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XfwDPYx892JkWDnN .cluster-label text{fill:#333;}#mermaid-svg-XfwDPYx892JkWDnN .cluster-label span{color:#333;}#mermaid-svg-XfwDPYx892JkWDnN .cluster-label span p{background-color:transparent;}#mermaid-svg-XfwDPYx892JkWDnN .label text,#mermaid-svg-XfwDPYx892JkWDnN span{fill:#333;color:#333;}#mermaid-svg-XfwDPYx892JkWDnN .node rect,#mermaid-svg-XfwDPYx892JkWDnN .node circle,#mermaid-svg-XfwDPYx892JkWDnN .node ellipse,#mermaid-svg-XfwDPYx892JkWDnN .node polygon,#mermaid-svg-XfwDPYx892JkWDnN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XfwDPYx892JkWDnN .rough-node .label text,#mermaid-svg-XfwDPYx892JkWDnN .node .label text,#mermaid-svg-XfwDPYx892JkWDnN .image-shape .label,#mermaid-svg-XfwDPYx892JkWDnN .icon-shape .label{text-anchor:middle;}#mermaid-svg-XfwDPYx892JkWDnN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XfwDPYx892JkWDnN .rough-node .label,#mermaid-svg-XfwDPYx892JkWDnN .node .label,#mermaid-svg-XfwDPYx892JkWDnN .image-shape .label,#mermaid-svg-XfwDPYx892JkWDnN .icon-shape .label{text-align:center;}#mermaid-svg-XfwDPYx892JkWDnN .node.clickable{cursor:pointer;}#mermaid-svg-XfwDPYx892JkWDnN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XfwDPYx892JkWDnN .arrowheadPath{fill:#333333;}#mermaid-svg-XfwDPYx892JkWDnN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XfwDPYx892JkWDnN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XfwDPYx892JkWDnN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XfwDPYx892JkWDnN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XfwDPYx892JkWDnN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XfwDPYx892JkWDnN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XfwDPYx892JkWDnN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XfwDPYx892JkWDnN .cluster text{fill:#333;}#mermaid-svg-XfwDPYx892JkWDnN .cluster span{color:#333;}#mermaid-svg-XfwDPYx892JkWDnN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XfwDPYx892JkWDnN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XfwDPYx892JkWDnN rect.text{fill:none;stroke-width:0;}#mermaid-svg-XfwDPYx892JkWDnN .icon-shape,#mermaid-svg-XfwDPYx892JkWDnN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XfwDPYx892JkWDnN .icon-shape p,#mermaid-svg-XfwDPYx892JkWDnN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XfwDPYx892JkWDnN .icon-shape .label rect,#mermaid-svg-XfwDPYx892JkWDnN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XfwDPYx892JkWDnN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XfwDPYx892JkWDnN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XfwDPYx892JkWDnN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} .less 源文件
词法分析

Lexer

拆解 Token
语法分析

Parser

构建 AST
变量求值

Eval

懒加载 + 作用域
规则展开

Expand

Mixin/嵌套/递归
代码生成

CodeGen

输出 CSS 字符串
.css 输出文件
Source Map

调试映射文件

各阶段详解:

阶段 核心工作 典型产物
词法分析(Lexer) 将源代码字符流拆解为 Token 序列(选择器、属性、值、@符号等) [@, primary, :, #1a73e8, ;]
语法分析(Parser) 依据 LESS 文法将 Token 序列组装成抽象语法树(AST) 嵌套的规则树节点
变量求值(Eval) 在作用域链中查找变量值(懒加载、就近原则)、执行运算与函数调用 所有 @var 替换为字面量值
规则展开(Expand) 将嵌套规则展开为平铺 CSS 选择器;递归调用 Mixin;处理 Guards 条件 平铺 CSS 规则树
代码生成(CodeGen) 将规则树序列化为 CSS 字符串;可选压缩(去空白、注释)与 Source Map 生成 .css 字符串
变量作用域与懒加载机制

LESS 采用词法作用域(Lexical Scope) ,变量查找遵循"当前块 → 上级块 → 全局"的链式规则,并在整个作用域内取最后一次赋值(懒加载):

less 复制代码
@size: 10px;        /* 全局:10px */

.outer {
  @size: 20px;      /* .outer 作用域:20px(最终值) */
  width: @size;     /* 输出:20px(取当前作用域最终值) */

  .inner {
    @size: 30px;    /* .inner 作用域:30px */
    height: @size;  /* 输出:30px */
    @size: 35px;    /* 懒加载:覆盖上一行,.inner 最终值 = 35px */
  }
}

.standalone {
  width: @size;     /* 输出:10px(全局作用域) */
}

编译结果:

css 复制代码
.outer        { width: 20px; }
.outer .inner { height: 35px; }
.standalone   { width: 10px; }

关键规则: 懒加载使 LESS 变量行为与 CSS 的层叠规则一致------"最后定义有效",而非"先声明先生效"。

Mixin 调用时的变量隔离

Mixin 拥有独立的执行作用域,内部变量不会泄漏到调用方:

less 复制代码
.mixin() {
  @private: 99px;   /* 私有变量,不会污染调用方 */
  width: @private;
}

.box {
  .mixin();
  /* height: @private; */  /* ❌ 报错:@private 不在此作用域内 */
}
Source Map 与浏览器调试

Source Map 是一个 JSON 文件(.css.map),记录了编译后 CSS 每一行与原始 LESS 文件的位置映射关系,使浏览器 DevTools 能直接显示 .less 文件和行号:

bash 复制代码
# 生成带 Source Map 的 CSS
lessc --source-map styles.less styles.css

# 输出 styles.css + styles.css.map
# styles.css 末尾自动追加:
# /*# sourceMappingURL=styles.css.map */

生产环境建议: 不要将 Source Map 部署到线上(暴露源码),只在开发环境生成,通过 .gitignore 排除或 CI 分离处理。

【代码注释】

核心逻辑(编译五阶段)

  1. Lexer :字符流 → Token(@:#1a73e8 等)。
  2. Parser:Token → AST(规则树、嵌套结构)。
  3. Eval :变量懒加载求值、运算、darken() 等函数。
  4. Expand:嵌套展平、Mixin 展开、Guards 分支匹配。
  5. CodeGen:输出 CSS 字符串 + 可选 Source Map。

变量懒加载(与示例对应)

  • 同一作用域内最后一次 @size 赋值生效 → .inner 最终 height: 35px
  • 与 CSS 层叠「后声明覆盖」直觉一致,但发生在编译期

Mixin 作用域隔离

  • Mixin 内 @private 不泄漏到 .box,调用方无法引用------类似函数局部变量。

注意点

  • 排查「变量为何是 Unexpected」:多为作用域外引用或循环 @import
  • 线上勿部署 .map 文件,避免暴露源码目录结构。

实战场景

  • DevTools 显示 .less 行号依赖 Source Map;Ant Design 主题编译失败多在 Eval/Expand 阶段变量未定义。

4 LESS 核心语法详解

4.1 注释

LESS 支持两种注释语法,行为存在本质区别:

less 复制代码
/* 这是 CSS 注释,会原样编译到 CSS 中 */
// 这是 LESS 注释,不会编译到 CSS 中

完整可运行示例:

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 注释演示</title>
  <style type="text/less">
    /* ======================
       CSS 注释:会保留在编译结果中
       适合写版权声明、模块说明
       ====================== */

    // LESS 行注释:编译后消失,适合开发调试说明

    .demo {
      color: red;   // 这行注释在 CSS 中不会出现
      /* background: blue; */ /* 这行注释会保留 */
    }
  </style>
  <script src="./less.js"></script>
</head>
<body>
  <p class="demo">注释演示文字</p>
</body>
</html>

编译后的 CSS:

css 复制代码
/* ======================
   CSS 注释:会保留在编译结果中
   适合写版权声明、模块说明
   ====================== */

.demo {
  color: red;
  /* background: blue; */
}

【代码注释】

核心逻辑

  • /* */:走 CSS 规范,编译后保留(压缩工具可再删)。
  • //:LESS 专有,编译后整行消失,不会进入生产 CSS。

对比表

语法 编译后 适用
/* 模块说明 */ 保留 版权、分区标题
// 临时禁用 消失 调试、TODO

注意点

  • 行内 // 注释后的代码仍有效;块注释可包住多行规则。
  • cssnano 等压缩默认会删 /* */,与 LESS 无关。

实战场景

  • Bootstrap LESS 源码用 // 写说明;发布版 CSS 只留简短版权块。

真实场景: Bootstrap 4 的 LESS 源码大量使用 // 行注释做代码说明,编译产物中只保留必要的版权块注释。


4.2 变量

LESS 变量是整个预处理器体系的基石,让"一处修改,全局生效"成为可能。

名词解释
术语 说明
变量(Variable) @ 开头的标识符,存储可复用的值(颜色、尺寸、字体等)。
懒加载(Lazy Evaluation) LESS 变量在整个作用域范围内都可以使用,甚至可以在定义之前引用。
作用域(Scope) 变量的可见范围,LESS 遵循就近原则(先在当前块查找,再向上冒泡)。
转义字符串(Escape) ~"..." 语法,用于包裹含有特殊字符或无法直接写入变量值的字符串。
定义与使用变量
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 变量完整演示</title>
  <style type="text/less">

    /* ── 1. 定义变量 ── */
    @primary-color: #1a73e8;     /* 主色调 */
    @danger-color:  #ea4a36;     /* 危险/错误色 */
    @success-color: #34a853;     /* 成功色 */
    @base-font-size: 14px;       /* 基础字号 */
    @container-width: 1200px;    /* 容器宽度 */

    /* 含有特殊字符的变量:使用转义语法 ~"..." */
    @media-md: ~"min-width: 768px";
    @media-lg: ~"min-width: 992px";

    /* 变量用作属性名 */
    @prop: background-position;

    /* 变量用作选择器 */
    @alert-selector: .alert-box;

    /* ── 2. 变量作为属性值(最常见用法) ── */
    .button {
      background-color: @primary-color;
      color: #fff;
      font-size: @base-font-size;
      padding: 8px 16px;
      border: none;
      border-radius: 4px;

      &:hover {
        background-color: darken(@primary-color, 10%);
      }
    }

    .alert-danger {
      color: @danger-color;
      border: 1px solid @danger-color;
      padding: 12px;
    }

    /* ── 3. 变量作为属性名 ── */
    .sprite {
      width: 32px;
      height: 32px;
      @{prop}: -32px 0;   /* 等同于 background-position: -32px 0 */
    }

    /* ── 4. 变量作为选择器 ── */
    @{alert-selector} {
      display: flex;
      align-items: center;
      padding: 12px 16px;
      border-radius: 4px;
    }

    /* ── 5. 变量在媒体查询中使用 ── */
    .container {
      width: 100%;
      margin: 0 auto;

      @media (@media-md) {
        width: 750px;
      }

      @media (@media-lg) {
        width: @container-width;
      }
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="container">
    <button class="button">主按钮</button>
    <div class="alert-danger">危险提示文字</div>
    <div class="alert-box">通用提示框</div>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  • @name: value编译期全文替换;改一处变量,所有引用同步更新。
  • 四种插值:@color 值、@{prop} 属性名、@{sel} 选择器、~"..." 转义字符串(媒体查询)。

关键 API

写法 编译示例 用途
@primary-color background: #1a73e8 主题色
@{prop}: -32px 0 background-position: ... 雪碧图
@{alert-selector} { } .alert-box { } 动态类名
@media (@media-md) @media (min-width: 768px) 断点变量

注意点

  • 媒体查询变量含 : 必须用 ~"min-width: 768px",否则解析错误(§10.2)。
  • darken(@primary-color, 10%) 参数是百分比不是 0~1(§10.4)。

实战场景

  • Ant Design / Element UI 主题定制:覆盖 @primary-color 等变量重新编译即可换肤。
变量作用域演示
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 变量作用域</title>
  <style type="text/less">

    @color: #333;         /* 全局变量 */

    .parent {
      @color: #900;       /* 局部变量,覆盖全局 */
      color: @color;      /* → #900(就近原则) */

      .child {
        color: @color;    /* → #900(继承父级作用域) */
      }
    }

    .sibling {
      color: @color;      /* → #333(使用全局变量) */
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="parent">
    父元素(#900)
    <div class="child">子元素(#900)</div>
  </div>
  <div class="sibling">兄弟元素(#333)</div>
</body>
</html>

【代码注释】

核心逻辑

  • 查找顺序:当前规则块 → 父块 → 全局;就近 + 懒加载(最后赋值有效)
  • .parent@color: #900 遮蔽 全局 #333.sibling 仍用 #333

与 JS 对比

LESS 变量 JS let
遮蔽 ✅ 内层覆盖外层 ✅ 块级遮蔽
编译期求值 运行期

注意点

  • 主题变量写在 .theme-blue { @primary: ... } 内,子选择器自动继承该作用域(§7.1)。

实战场景

  • Ant Design、Bootstrap 主题:构建时 modifyVars 覆盖全局 @primary-color

真实网站场景: Ant Design、Element UI、Bootstrap 均通过 LESS/SCSS 变量系统实现主题定制。用户只需覆盖预定义的变量(如 @primary-color),即可实现整站换肤,无需修改组件样式。


4.3 混合(Mixins)

名词解释
术语 说明
混合(Mixin) 可复用的样式代码块,类似编程语言中的函数。定义一次,多处调用。
带参数混合 调用时可以传入参数值,实现"模板化"的样式生成。
默认参数 混合定义时给参数指定默认值,调用时可省略该参数。
@arguments LESS 内置变量,代表混合被调用时传入的所有参数列表,按顺序排列。
命名空间(Namespace) 将相关混合组织在一个类选择器内,用 #namespace > .mixin() 调用,避免命名污染。
① 无参混合
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>无参混合演示</title>
  <style type="text/less">

    /* 定义无参混合:绝对定位居中 */
    .center-box01() {
      position: absolute;
      width: 400px;
      height: 300px;
      left: 50%;
      top: 50%;
      margin-left: -200px;    /* 负值为宽度的一半 */
      margin-top:  -150px;    /* 负值为高度的一半 */
    }

    /* 定义清除浮动混合 */
    .clearfix() {
      &::after {
        content: "";
        display: block;
        clear: both;
      }
    }

    /* 调用混合 */
    .dialog {
      .center-box01();         /* 复用居中逻辑 */
      background: #fff;
      border-radius: 8px;
      box-shadow: 0 4px 20px rgba(0,0,0,.15);
    }

    .news-list {
      .center-box01();         /* 同样的居中逻辑,无需重复书写 */
      background: #f5f5f5;
      border: 1px solid #ddd;
    }

    .layout {
      .clearfix();             /* 清除浮动 */
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div style="position:relative;height:400px;background:#eee;">
    <div class="dialog">居中弹窗</div>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  • .center-box01() 定义可复用样式块;.dialog { .center-box01(); } 在调用处展开 属性,不单独输出 .center-box01 规则。
  • .clearfix()&::after → 调用者 .layout::after 清除浮动。

Mixin 有无括号(§10.3)

定义 是否输出为 CSS 类 用途
.mixin() ❌ 仅当被调用时展开 纯复用
.mixin ✅ 会生成 .mixin 既可当类又可当 mixin

注意点

  • 无参 mixin 必须(),否则污染 CSS 选择器列表。
  • 居中方案也可用 Flex/Grid,本例为经典负 margin 教学写法。

实战场景

  • 全局 mixins/clearfix.less;弹窗居中、列表浮动布局在 PC 项目中反复调用。
② 带参数的混合
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>带参数混合演示</title>
  <style type="text/less">

    /* 定义带参数混合:绝对定位居中(动态尺寸) */
    .center-box02(@width, @height) {
      position: absolute;
      width: @width;
      height: @height;
      left: 50%;
      top: 50%;
      margin-left: -(@width / 2);     /* 动态计算负边距 */
      margin-top:  -(@height / 2);
    }

    /* 调用方式一:按参数顺序传值 */
    .modal-sm {
      .center-box02(400px, 300px);
      background: #fff;
      border-radius: 8px;
    }

    /* 调用方式二:按参数名传值(顺序可以不同) */
    .modal-lg {
      .center-box02(@height: 600px, @width: 900px);
      background: #f0f8ff;
      border: 1px solid #1a73e8;
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div style="position:relative;height:700px;background:#eee;">
    <div class="modal-sm">小弹窗 400×300</div>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  • .center-box02(@width, @height) 形参在调用时替换;负 margin 用 -(@width / 2) 动态居中。
  • 支持位置传参 400px, 300px按名传参 @height: 600px, @width: 900px

关键 API

  • 按名传参:参数多、有默认值时可读性更好。
  • 除法:-(@width / 2) 括号强制数学运算。

注意点

  • 无括号 margin-left: -@width / 2 可能编译异常(§10.1)。

实战场景

  • 多尺寸 Modal、Banner 用同一套居中 mixin,只改宽高参数。
③ 带默认值的混合
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>带默认值混合演示</title>
  <style type="text/less">

    /* 参数带默认值:@width 默认 1200px */
    .center-box03(@width: 1200px, @height) {
      position: absolute;
      width: @width;
      height: @height;
      left: 50%;
      top: 50%;
      margin-left: -(@width / 2);
      margin-top:  -(@height / 2);
    }

    /* 当有默认值的参数在前时,必须使用按名传参 */
    .panel-default {
      .center-box03(@height: 300px);   /* @width 使用默认值 1200px */
      background: #fff;
    }

    /* 也可以覆盖默认值 */
    .panel-narrow {
      .center-box03(800px, 500px);     /* 覆盖 @width 为 800px */
      background: #f5f5f5;
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div style="position:relative;height:600px;background:#ccc;">
    <div class="panel-default">默认宽度面板</div>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  • @width: 1200px 为默认参数;调用 .center-box03(@height: 300px) 时宽度用默认 1200,高度用 300。
  • 默认值在前时,后续参数必须用按名传参,否则解析歧义。

注意点

  • 与 Python 默认参数规则类似:省略前面的默认参数必须用关键字形式。
  • 也可 .center-box03(800px, 500px) 按顺序覆盖两个参数。

实战场景

  • 栅格容器默认全宽,个别面板传 @height 定制高度。
④ @arguments 关键字
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>@arguments 演示</title>
  <style type="text/less">

    /* @arguments 自动收集所有参数,按传入顺序拼接 */
    .box-shadow(@x, @y, @blur, @spread, @color) {
      -webkit-box-shadow: @arguments;   /* 兼容旧版 Safari/Chrome */
      -moz-box-shadow:    @arguments;   /* 兼容旧版 Firefox */
      -o-box-shadow:      @arguments;   /* 兼容旧版 Opera */
      box-shadow:         @arguments;   /* 标准属性 */
    }

    /* 定义过渡动画混合 */
    .transition(@property, @duration, @timing: ease, @delay: 0s) {
      -webkit-transition: @arguments;
      transition: @arguments;
    }

    .card {
      width: 300px;
      height: 200px;
      background: #fff;
      /* 调用 box-shadow 混合 */
      .box-shadow(3px, 10px, 15px, 0px, rgba(0,0,0,.2));
      /* 调用 transition 混合 */
      .transition(all, .3s, ease-in-out);
    }

    .card:hover {
      .box-shadow(6px, 15px, 25px, 0px, rgba(0,0,0,.3));
      transform: translateY(-4px);
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="card">悬停查看阴影变化</div>
</body>
</html>

【代码注释】

核心逻辑

  • 调用 .box-shadow(3px, 10px, 15px, 0px, rgba(...)) 时,@arguments 在 mixin 内展开为完整实参列表
  • 各前缀属性行写 -webkit-box-shadow: @arguments 等,改参数只改一处调用。

关键 API

less 复制代码
.box-shadow(@x, @y, @blur, @spread, @color) {
  -webkit-box-shadow: @arguments;
  box-shadow: @arguments;
}

注意点

  • 参数顺序必须与目标 CSS 属性值顺序一致。
  • 现代项目前缀需求减少,但 Ant Design/Bootstrap 3 仍保留此模式。

实战场景

  • mixins/vendor-prefixes.lesstransition / box-shadow 工厂函数。

真实网站场景:

  • Ant Design 的 LESS 混合库(mixins.less)使用 @arguments 封装了 box-shadowtransitiontransform 等跨浏览器兼容属性。
  • Bootstrap 3mixins/vendor-prefixes.less 同样大量使用此模式处理 CSS3 前缀问题。

4.4 条件判断(Guards)

LESS 通过 when 关键字为混合添加条件守卫(Guards),实现类型级别的条件分支,这是 LESS 中实现"CSS 算法"的核心机制。

名词解释
术语 说明
Guards(守卫) 附加在混合定义上的条件表达式。只有条件为真时,该混合的定义才会被匹配和执行。
模式匹配(Pattern Matching) 同名混合可以定义多个,LESS 根据参数值匹配对应的定义,类似 Haskell/Erlang 的模式匹配。
比较运算符 可用 >>===<< 进行数值比较;用 = 进行值相等比较。
经典用例:CSS 三角形工厂
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 条件判断------三角形生成器</title>
  <style type="text/less">

    /* 用 when 条件守卫实现方向分支 */
    /* 向上三角:底部边有颜色,其余透明 */
    .triangle(@border-width, @color, @direction) when (@direction = up) {
      width: 0;
      height: 0;
      border-style: solid;
      border-width: @border-width;
      border-color: transparent transparent @color transparent;
    }

    /* 向下三角:顶部边有颜色 */
    .triangle(@border-width, @color, @direction) when (@direction = down) {
      width: 0;
      height: 0;
      border-style: solid;
      border-width: @border-width;
      border-color: @color transparent transparent transparent;
    }

    /* 向左三角:右侧边有颜色 */
    .triangle(@border-width, @color, @direction) when (@direction = left) {
      width: 0;
      height: 0;
      border-style: solid;
      border-width: @border-width;
      border-color: transparent @color transparent transparent;
    }

    /* 向右三角:左侧边有颜色 */
    .triangle(@border-width, @color, @direction) when (@direction = right) {
      width: 0;
      height: 0;
      border-style: solid;
      border-width: @border-width;
      border-color: transparent transparent transparent @color;
    }

    /* 使用三角形混合 */
    .arrow-up    { .triangle(20px, #1a73e8, up);    margin: 20px; display: inline-block; }
    .arrow-down  { .triangle(20px, #ea4a36, down);  margin: 20px; display: inline-block; }
    .arrow-left  { .triangle(20px, #34a853, left);  margin: 20px; display: inline-block; }
    .arrow-right { .triangle(20px, #fbbc04, right); margin: 20px; display: inline-block; }

    /* 下拉菜单箭头示例 */
    .dropdown-arrow {
      .triangle(8px, #666, down);
      display: inline-block;
      vertical-align: middle;
      margin-left: 4px;
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="arrow-up"></div>
  <div class="arrow-down"></div>
  <div class="arrow-left"></div>
  <div class="arrow-right"></div>

  <p>下拉菜单 <span class="dropdown-arrow"></span></p>
</body>
</html>

【代码注释】

核心逻辑

  • 同名 mixin .triangle(...) 定义多份,用 when (@direction = up)Guards 做模式匹配,仅匹配分支展开。
  • CSS 三角:width/height: 0 + 四边 border,显示边设色,其余 transparent

关键 API

  • when (条件):守卫表达式;=相等比较非赋值。
  • 下拉箭头:.triangle(8px, #666, down) 即可。

注意点

  • Guards 不能替代 if/else 做复杂逻辑,仅适合分支样式。
  • 数值比较用 when (@cols > 3) 等(见下节)。

实战场景

  • 气泡尖角、Select 下拉箭头、Tooltip 三角;Bootstrap caret 同源思路。
数值条件守卫
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 数值条件守卫</title>
  <style type="text/less">

    /* 根据列数动态调整间距 */
    .grid-col(@cols) when (@cols <= 3) {
      width: percentage(1 / @cols);
      padding: 0 20px;
    }

    .grid-col(@cols) when (@cols > 3) {
      width: percentage(1 / @cols);
      padding: 0 10px;    /* 列多时减小内边距 */
    }

    /* 根据字号大小动态调整行高 */
    .font-size(@size) when (@size < 14px) {
      font-size: @size;
      line-height: 1.8;   /* 小字号用较大行高提升可读性 */
    }

    .font-size(@size) when (@size >= 14px) {
      font-size: @size;
      line-height: 1.5;
    }

    .text-sm { .font-size(12px); }
    .text-md { .font-size(16px); }
    .text-lg { .font-size(24px); }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <p class="text-sm">小号文字(12px,行高1.8)</p>
  <p class="text-md">中号文字(16px,行高1.5)</p>
  <p class="text-lg">大号文字(24px,行高1.5)</p>
</body>
</html>

【代码注释】

核心逻辑

  • .text-size(@size)when (@cols <= 3) / when (@cols > 3) 分支输出不同字号,等价于 if-else。
  • 守卫支持 and(与)、逗号 ,(或),可组合数值、字符串比较。

关键 API

写法 含义
when (@a > 0) and (@b > 0) 同时满足
when (@a > 0), (@b > 0) 满足其一

注意点

  • 分支应互斥且覆盖常用场景,否则无匹配时 mixin 不展开。
  • 与 §4.4 方向守卫配合,可驱动 .text-sm / .text-lg 等修饰类。

实战场景

  • 栅格列数、字号档位、按钮尺寸等多档样式生成。

真实网站场景:

  • 天猫、京东 商品页的下拉选择器箭头、面包屑导航分隔符均使用 CSS 三角形技术,在 LESS 项目中由三角形混合统一生成。
  • Bootstrap 3_dropdowns.less 中大量使用 CSS 三角形构造下拉箭头。

4.5 导入(Import)

LESS 的 @import 允许将样式代码分割为多个文件进行模块化管理,这是大型项目样式工程化的基础。

名词解释
术语 说明
模块化(Modularization) 将代码按功能拆分为独立文件(变量文件、重置文件、组件文件等),通过 @import 组合。
@import 关键字 导入另一个 LESS 文件的全部内容到当前文件中,类似 C/C++ 的 #include
导入关键词 LESS 扩展了 CSS 的 @import,提供 referenceonceinline 等导入选项。
基础导入

variables.less(变量文件):

less 复制代码
/* 全局颜色变量 */
@master-red:   #ea4a36;    /* 主品牌红 */
@btn-red:      #e1251b;    /* 按钮红 */
@sep-color:    #b3aeae;    /* 分隔线颜色 */
@text-primary: #333;       /* 主文字色 */
@text-muted:   #999;       /* 次要文字色 */

/* 全局尺寸变量 */
@container-width: 1190px;
@gutter: 15px;

reset.less(重置样式):

less 复制代码
* {
  margin:  0;
  padding: 0;
  box-sizing: border-box;
}

a {
  text-decoration: none;
  color: inherit;
}

ul, ol {
  list-style: none;
}

img {
  display: block;
  max-width: 100%;
}

mixins.less(混合库):

less 复制代码
/* 清除浮动 */
.clearfix() {
  &::after {
    content: "";
    display: block;
    clear: both;
  }
}

/* Flex 居中 */
.flex-center() {
  display: flex;
  justify-content: center;
  align-items: center;
}

/* 文字截断(单行) */
.text-ellipsis() {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

index.less(入口文件):

less 复制代码
/* 导入变量(必须最先导入,其他文件依赖变量) */
@import "variables";

/* 导入重置样式 */
@import "reset";

/* 导入混合库 */
@import "mixins";

/* 导入 CSS 文件(原样保留 @import 语句到编译结果) */
@import "normalize.css";

/* 页面级样式 */
.box {
  width: 500px;
  color: @master-red;    /* 使用从 variables.less 导入的变量 */
  .clearfix();           /* 使用从 mixins.less 导入的混合 */
}

完整可运行示例(单文件演示导入逻辑):

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 导入演示</title>
  <style type="text/less">
    /* 在单文件中模拟多文件导入的效果 */

    /* ── 等同于 @import "variables" 的内容 ── */
    @brand-color: #1a73e8;
    @font-size:   14px;

    /* ── 等同于 @import "mixins" 的内容 ── */
    .clearfix() {
      &::after {
        content: "";
        display: block;
        clear: both;
      }
    }

    .text-ellipsis() {
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }

    /* ── 业务样式(使用导入的变量和混合) ── */
    .news-list {
      width: 400px;
      .clearfix();

      li {
        float: left;
        width: 180px;
        height: 40px;
        line-height: 40px;
        color: @brand-color;
        font-size: @font-size;
        .text-ellipsis();
        margin: 0 10px 10px 0;
        border-bottom: 1px solid #eee;
      }
    }
  </style>
  <script src="./less.js"></script>
</head>
<body>
  <ul class="news-list">
    <li>这是一条很长的新闻标题文字会被截断</li>
    <li>另一条新闻标题文字</li>
    <li>第三条新闻超长标题内容超过宽度</li>
    <li>第四条普通标题</li>
  </ul>
</body>
</html>

【代码注释】

核心逻辑

  • @import "variables"variables.less 内联展开到当前文件(可省略扩展名)。
  • @import "normalize.css" 保留为 CSS @import,由浏览器另行请求,不内联

导入顺序建议

复制代码
variables → mixins → reset → components → page

LESS 导入关键词对照表:

关键词 示例 行为
(无) @import "file" 标准导入,文件内容内联展开
reference @import (reference) "file" 导入但只引用(不输出),引用混合/变量时才输出
once @import (once) "file" 只导入一次(默认行为)
multiple @import (multiple) "file" 允许多次导入同一文件
inline @import (inline) "file" 内联文件内容但不处理(保留原始 CSS)

真实网站场景:

  • Bootstrap 3bootstrap.less 入口文件通过 @import 组织了 40+ 个模块文件(变量、重置、网格、按钮、表单等),所有模块独立维护,通过入口文件统一编译输出。

4.6 嵌套(Nesting)

嵌套是 LESS 最直观、最常用的特性,让样式结构与 HTML 结构保持一致,大幅减少选择器重复书写。

① 基本嵌套(层级选择器)
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 基本嵌套演示</title>
  <style type="text/less">

    .container {
      margin: 0 auto;
      width: 1200px;

      /* 后代选择器:.container .wrapper */
      .wrapper {
        height: 200px;
        background: #f5f5f5;

        /* 后代:.container .wrapper .item */
        .item {
          background: #ccc;
          padding: 10px;
        }

        /* 子代选择器:.container .wrapper > .active */
        > .active {
          background: #1a73e8;
          color: #fff;
        }

        /* 紧邻兄弟:.container .wrapper + .content */
        + .content {
          background: #aaa;
        }

        /* 通用兄弟:.container .wrapper ~ div */
        ~ div {
          background: #ddd;
        }
      }

      /* 链接列表 */
      .news {
        padding: 0;
        margin: 10px;

        li {
          width: 500px;
          line-height: 40px;
          border-bottom: 1px solid #eee;

          /* & 父引用:.news li.active */
          &.active {
            background: #900;
            color: #fff;
          }

          /* 属性选择器:.news li[name='xiaole'] */
          &[data-type='featured'] {
            font-weight: bold;
            color: #ea4a36;
          }

          /* 伪类:.news li:hover */
          &:hover {
            background: #f0f8ff;
            cursor: pointer;
          }

          /* 伪元素:.news li::after */
          &::after {
            content: "›";
            float: right;
            color: #999;
          }
        }
      }
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="container">
    <div class="wrapper">
      <div class="item">普通子元素</div>
      <div class="active">激活子元素</div>
    </div>
    <ul class="news">
      <li class="active">激活新闻</li>
      <li data-type="featured">精选新闻</li>
      <li>普通新闻</li>
    </ul>
  </div>
</body>
</html>

编译后等效 CSS 片段:

css 复制代码
.container { margin: 0 auto; width: 1200px; }
.container .wrapper { height: 200px; background: #f5f5f5; }
.container .wrapper .item { background: #ccc; padding: 10px; }
.container .wrapper > .active { background: #1a73e8; color: #fff; }
.container .wrapper + .content { background: #aaa; }
.container .wrapper ~ div { background: #ddd; }
.container .news li { width: 500px; line-height: 40px; }
.container .news li.active { background: #900; color: #fff; }
.container .news li:hover { background: #f0f8ff; cursor: pointer; }
.container .news li::after { content: "›"; float: right; color: #999; }

【代码注释】

核心逻辑

  • 嵌套把子选择器写在父规则内,编译时展开为完整选择器链,减少重复前缀。
  • & 引用当前父选择器,拼接方式决定是交集还是后代。

& 拼接规则

写法 编译结果 含义
&.active .news li.active 同一元素多类
&:hover .news li:hover 伪类
& .child .news li .child 后代(有空格)
> a .container > a 直接子代

注意点

  • & 与父选择器之间无空格 才拼接;&::after 常用于清除浮动、装饰线。
  • 嵌套过深(>3 层)可读性下降,组件库常限制 2~3 层。

实战场景

  • 列表项 hover/active、新闻模块 .container .news li 一类 PC 页结构。
② 媒体查询嵌套

LESS 允许将媒体查询写在选择器内部,使响应式代码与对应选择器在视觉上保持邻近关系,提升可读性。

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>媒体查询嵌套演示</title>
  <style type="text/less">

    /* 变量化断点(可集中管理) */
    @bp-sm: 576px;
    @bp-md: 768px;
    @bp-lg: 992px;
    @bp-xl: 1200px;

    .container {
      width: 100%;
      margin-left: auto;
      margin-right: auto;
      padding: 0 15px;

      /* 媒体查询直接嵌套在选择器内 */
      @media (min-width: @bp-md) {
        width: 750px;
        padding: 0;
      }

      @media (min-width: @bp-lg) {
        width: 970px;
      }

      @media (min-width: @bp-xl) {
        width: 1170px;
      }
    }

    /* 卡片响应式布局 */
    .card-grid {
      display: flex;
      flex-wrap: wrap;
      gap: 16px;

      .card {
        width: 100%;             /* 移动端全宽 */
        background: #fff;
        border: 1px solid #ddd;
        border-radius: 8px;
        padding: 16px;

        @media (min-width: @bp-md) {
          width: calc(50% - 8px);   /* 平板两列 */
        }

        @media (min-width: @bp-lg) {
          width: calc(33.333% - 11px);  /* 桌面三列 */
        }
      }
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="container">
    <div class="card-grid">
      <div class="card">卡片 1</div>
      <div class="card">卡片 2</div>
      <div class="card">卡片 3</div>
      <div class="card">卡片 4</div>
    </div>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  • @media (min-width: @bp-md) 写在 .container / .card 内部,编译为 @media (...) { .card-grid .card { ... } },响应式规则与组件同屏维护
  • @bp-sm@bp-xl 集中管理断点,改一处变量全站媒体查询同步。

关键 API

  • 断点变量 + 嵌套媒体查询 = Bootstrap 式容器宽度阶梯(750 / 970 / 1170)。
  • calc(50% - 8px) 可与 gap 布局配合做列宽。

注意点

  • 媒体查询变量含 :~"min-width: 768px" 或单独 @bp-md: 768px 再拼进 @media
  • 移动端优先:先写默认样式,再在大屏 @media 中覆盖。

实战场景

  • 卡片栅格 1→2→3 列、版心 .container 随屏宽变化;与 §7.2 栅格变量思路一致。

真实网站场景: Bootstrap 4 将响应式断点定义为 SASS 变量($grid-breakpoints),并通过 mixin 将媒体查询注入各组件,与 LESS 的媒体查询嵌套原理相同。

③ 混合与嵌套结合
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>混合与嵌套结合演示</title>
  <style type="text/less">

    /* 包含嵌套的混合 */
    .center-box() {
      width: 400px;
      height: 300px;
      background: #f0f8ff;

      /* 混合内部可以定义嵌套混合 */
      .center-box01() {
        width: 200px;
        height: 150px;
        background: #1a73e8;
        color: #fff;
      }
    }

    /* 清除浮动(& 引用在混合中的经典用法) */
    .clearfix() {
      &::after {
        content: "";
        display: block;
        clear: both;
      }
    }

    /* 卡片悬停效果混合 */
    .card-hover-effect() {
      transition: all .3s ease;

      &:hover {
        transform: translateY(-4px);
        box-shadow: 0 8px 24px rgba(0,0,0,.15);
      }
    }

    /* 应用混合 */
    .box {
      .center-box();
      .card-hover-effect();
      position: relative;
      cursor: pointer;
    }

    .layout-row {
      .clearfix();
      width: 100%;

      .col {
        float: left;
        width: 33.333%;
        padding: 0 15px;
      }
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="box">悬停查看效果</div>
  <div class="layout-row">
    <div class="col">列1</div>
    <div class="col">列2</div>
    <div class="col">列3</div>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  • Mixin 内 &调用点 替换为调用者选择器:.layout-row { .clearfix(); }.layout-row::after
  • .card-hover-effect()&:hover,展开后悬停样式挂在 .box:hover,而非孤立伪类。
  • 混合内可再定义子混合(.center-box01()),形成局部命名空间。

注意点

  • 子混合若未在同次调用链中引用,可能无法按预期展开;大项目更推荐 #utils > .mixin()(§4.9)。
  • 浮动三列布局必须配合 .clearfix(),否则父高度塌陷。

实战场景

  • 卡片悬停抬升、三列 .layout-row 浮动 + 清除浮动,PC 列表页常见组合。

4.7 运算符(Operations)

LESS 支持对数值进行四则运算,让尺寸计算从"硬编码"变为"公式推导"。

名词解释
术语 说明
单位推断 运算结果的单位取决于操作数,规则:两个操作数单位不同,取第一个操作数的单位;只有一个有单位,取该单位。
括号强制除法 LESS 中除法 / 必须用括号包裹,如 (100px / 2),否则被当作 CSS 字体属性分隔符。
calc() CSS 原生计算函数,LESS 不会对 calc() 内部表达式进行预处理。
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 运算符完整演示</title>
  <style type="text/less">

    @base: 100px;
    @gutter: 15px;
    @columns: 12;

    .ops-demo {
      /* 加法:同单位 */
      width: @base + 50px;              /* → 150px */

      /* 加法:不同单位,取第一个操作数单位 */
      margin: 10em + 20px;             /* → 30em(取 em) */

      /* 减法 */
      height: @base - 30px;            /* → 70px */

      /* 乘法 */
      padding: @gutter * 2;            /* → 30px */

      /* 除法:必须用括号 */
      font-size: (@base / 10);         /* → 10px */

      /* 综合运算 */
      border-radius: (@base / 4 + 5px);  /* → 30px */
    }

    /* 实际应用:等分列宽计算 */
    .grid(@col, @total: @columns) {
      width: percentage(@col / @total);
    }

    .col-4 { .grid(4); }    /* 33.3333% */
    .col-6 { .grid(6); }    /* 50% */
    .col-3 { .grid(3); }    /* 25% */

    /* 响应式容器宽度:减去两侧 gutter */
    .inner {
      width: (1200px - @gutter * 2);   /* → 1170px */
      margin: 0 auto;
      padding: 0 @gutter;
    }

    /* 颜色运算 */
    @primary: #4a90e2;
    .color-ops {
      background: @primary + #111;         /* 颜色相加(各通道相加,最大255) */
      border-color: @primary - #222;       /* 颜色相减 */
      color: @primary * 2;                 /* 颜色各通道乘以2 */
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="ops-demo">运算符演示</div>
  <div class="inner">
    <div class="col-6" style="float:left;background:#f0f8ff;">50% 宽度列</div>
    <div class="col-6" style="float:left;background:#fff3cd;">50% 宽度列</div>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  • LESS 在编译期对数字、长度、颜色做运算;结果写入 CSS 字面量。
  • 除法 必须用括号 (100px / 4),否则 / 可能被当作 CSS 分隔符(§10.1)。
  • percentage(@col / @total) 将比例转为 %,栅格核心算法。

易错:混合单位

表达式 结果 说明
10px + 20px 30px ✅ 同单位
10em + 20px 30em ⚠️ 取第一个单位,px 被丢
(100px / 4) 25px ✅ 括号除法

注意点

  • 颜色运算可用但少见;hover 色优先 darken() / lighten()(§4.8)。
  • 100px % 6 取余请用 mod() 函数。

实战场景

  • 12 列宽 percentage(4/12)、间距 @gutter / 2、负 margin 居中 -(@width/2)

运算规则总结表:

场景 示例 结果
同单位加法 10px + 20px 30px
异单位加法 10em + 20px 30em(取第一个)
只有一个单位 10 + 20px 30px
乘法 15px * 2 30px
除法(必须括号) (100px / 4) 25px
取余(不支持) 100px % 6 不支持

4.8 内置函数(Functions)

LESS 提供了丰富的内置函数,涵盖颜色处理、数学运算、字符串操作等。完整列表参见官方文档:https://less.bootcss.com/functions/

常用函数速查表
分类 函数 说明 示例
数学 percentage(n) 小数转百分比 percentage(0.5)50%
数学 mod(a, b) 取余 mod(100px, 6)4px
数学 ceil(n) 向上取整 ceil(2.3)3
数学 floor(n) 向下取整 floor(2.9)2
数学 round(n) 四舍五入 round(2.5)3
颜色 lighten(c, n%) 提亮颜色 lighten(#900, 10%)#cc0000
颜色 darken(c, n%) 加深颜色 darken(#900, 20%)#300000
颜色 saturate(c, n%) 增加饱和度 saturate(#808080, 50%)
颜色 desaturate(c, n%) 降低饱和度 desaturate(#f00, 30%)
颜色 mix(c1, c2, w%) 混合两色 mix(#f00, #00f, 50%)
颜色 fade(c, n%) 设置透明度 fade(#900, 50%)rgba(153,0,0,0.5)
颜色 rgba(r,g,b,a) 创建 rgba 颜色 rgba(255, 0, 0, 0.5)
字符串 e(s) 转义字符串(同 ~"..." e("min-width:768px")
完整函数示例
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 内置函数完整演示</title>
  <style type="text/less">

    @primary: #1a73e8;
    @danger:  #ea4a36;

    /* ── 颜色函数 ── */
    .btn-primary {
      background: @primary;
      color: #fff;
      padding: 8px 20px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      display: inline-block;
      margin: 8px;
      transition: background .2s;

      &:hover {
        background: darken(@primary, 10%);   /* 悬停加深 10% */
      }

      &:active {
        background: darken(@primary, 20%);   /* 点击加深 20% */
      }

      &:disabled {
        background: lighten(@primary, 30%);  /* 禁用提亮 30% */
        cursor: not-allowed;
      }
    }

    .btn-danger {
      background: @danger;
      color: #fff;
      padding: 8px 20px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      display: inline-block;
      margin: 8px;

      &:hover {
        background: darken(@danger, 10%);
      }
    }

    /* 颜色透明度 */
    .overlay {
      background: fade(#000, 60%);    /* rgba(0,0,0,0.6) */
      position: fixed;
      top: 0; left: 0; right: 0; bottom: 0;
    }

    /* 颜色混合:生成渐变系列 */
    @base-color: #1a73e8;
    .color-50  { background: mix(@base-color, #fff, 50%); }   /* 50% 浅化 */
    .color-100 { background: mix(@base-color, #fff, 30%); }   /* 更浅 */
    .color-200 { background: @base-color; }                   /* 原色 */
    .color-300 { background: mix(@base-color, #000, 80%); }   /* 稍深 */

    /* ── 数学函数 ── */
    .grid-item {
      width: percentage(1 / 6);    /* 等分六列 → 16.6667% */
      height: percentage(0.7);     /* → 70% */
      float: left;
      padding: mod(100px, 6) 0;    /* 取余 → 4px */
    }

    /* ── 实际主题生成示例 ── */
    @theme-hue: #4a90e2;

    .theme-light {
      background: lighten(@theme-hue, 40%);
      color: darken(@theme-hue, 30%);
      border: 1px solid lighten(@theme-hue, 20%);
    }

    .theme-dark {
      background: darken(@theme-hue, 20%);
      color: lighten(@theme-hue, 40%);
      border: 1px solid darken(@theme-hue, 30%);
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <button class="btn-primary">主要按钮</button>
  <button class="btn-primary" disabled>禁用按钮</button>
  <button class="btn-danger">危险按钮</button>

  <div style="display:flex;gap:8px;margin:16px 0;">
    <div class="color-50"  style="width:80px;height:40px;"></div>
    <div class="color-100" style="width:80px;height:40px;"></div>
    <div class="color-200" style="width:80px;height:40px;"></div>
    <div class="color-300" style="width:80px;height:40px;"></div>
  </div>

  <div class="theme-light" style="padding:16px;margin:8px 0;">Light 主题</div>
  <div class="theme-dark"  style="padding:16px;margin:8px 0;">Dark 主题</div>
</body>
</html>

【代码注释】

核心逻辑

  • 颜色函数在 HSL 空间 运算:darken / lighten 调亮度,saturate / desaturate 调饱和度,fade 调透明度。
  • mix(c1, c2, w%) 按权重混合两色,生成 50/100/200 色阶。
  • 数学函数 percentagemodceil 等与 §4.7 运算互补。

常用函数速记

函数 典型用途
darken(@primary, 10%) 按钮 hover
fade(#000, 60%) 遮罩层
mix(@base, #fff, 50%) 浅色背景系列
percentage(1/6) 六等分列宽

注意点

  • darken 第二参数是 百分比(10%),不是 0.1(§10.4)。
  • 禁用态用 lighten 比手写灰色更易随主题色联动。

实战场景

  • 主色一套变量 + 函数推导 hover/active/disabled;主题 light/dark 块(.theme-light)。

4.9 命名空间混合与混合嵌套

当混合数量增多时,可用 命名空间 把工具类混合收拢到同一选择器下,避免全局类名污染;混合内部也可以再定义子混合,并在调用处组合使用。

名词解释
术语 说明
命名空间(Namespace) 形如 #utils { .clearfix() {} },通过 #utils > .clearfix() 调用,编译后不输出 #utils 空规则(仅展开调用处)。
混合嵌套 在一个混合定义块内再定义子混合,适合按组件维度组织工具方法。
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>命名空间混合与混合嵌套</title>
  <style type="text/less">
    @master-red: #c81623;

    /* 外层混合内可定义子混合 */
    .center-box() {
      width: 400px;
      height: 300px;
      background: fade(@master-red, 15%);

      .center-box-lg() {
        width: 500px;
        height: 400px;
        background: fade(@master-red, 25%);
      }
    }

    .clearfix() {
      &::after {
        content: "";
        display: block;
        clear: both;
      }
    }

    #layout-utils {
      .clearfix() {
        &::after { content: ""; display: block; clear: both; }
      }
      .ellipsis() {
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
      }
    }

    .box {
      .center-box();
      color: @master-red;
      .center-box-lg();
    }

    .item {
      .center-box();
      margin-top: 12px;
    }

    .wrapper {
      #layout-utils > .clearfix();
      width: 900px;
      border: 1px dashed #ddd;
      padding: 8px;

      .col {
        float: left;
        width: 200px;
        #layout-utils > .ellipsis();
      }
    }
  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="box">主容器(嵌套混合尺寸)</div>
  <div class="wrapper">
    <div class="col">很长很长很长很长很长很长很长很长的栏目名称</div>
    <div class="col">第二列</div>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  • #layout-utils { .clearfix() {} } 定义命名空间 ,调用 #layout-utils > .clearfix() 只展开 mixin,不输出空 #layout-utils 规则块。
  • 混合内嵌套子混合(.center-box-lg())形成局部作用域,适合按组件打包工具方法。

对比

方式 检索性 冲突风险
全局 .clearfix() 易重名
#utils > .clearfix() 低,需记命名空间

注意点

  • () 的 mixin 默认不进 CSS;无 () 的类会同时生成选择器(§10.3)。
  • 生产项目更推荐 mixins/clearfix.less + @import,命名空间适合大型组件库内部。

实战场景

  • Ant Design 早期 LESS 版:typographyellipsis 等工具 mixin 集中管理。

真实网站场景: 组件库(如 Ant Design 早期 LESS 版)将 typographyclearfixtext-ellipsis 等工具混合集中在内部模块,业务侧通过命名空间或统一入口 @import 引用,减少全局类名冲突。


4.10 图片路径与资源引用

样式中的 url() 在 LESS 里可按普通 CSS 书写;路径相对于 当前 LESS 文件所在目录 (编译后则相对于输出 CSS 位置)。建议将位图、图标放在项目下的 images/ 目录,便于统一维护。

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 图片路径演示</title>
  <style type="text/less">
    @logo-width: 190px;
    @logo-height: 56px;
    @banner-height: 120px;

    .header-logo {
      width: @logo-width;
      height: @logo-height;
      /* 相对当前页面:images 与 html 同级 */
      background: url("images/logo.png") no-repeat left center;
      background-size: contain;
      text-indent: -9999px;
      overflow: hidden;
    }

    .promo-banner {
      height: @banner-height;
      background: url("images/banner.jpg") no-repeat center;
      background-size: cover;
      border-radius: 4px;
    }

    .sprite-icon-cart {
      width: 24px;
      height: 24px;
      background: url("images/icons.png") no-repeat -48px 0;
    }
  </style>
  <script src="./less.js"></script>
</head>
<body>
  <header>
    <h1 class="header-logo">站点 Logo</h1>
    <span class="sprite-icon-cart" title="购物车"></span>
  </header>
  <section class="promo-banner" aria-label="促销横幅区域"></section>
  <p style="font-size:12px;color:#666;margin-top:8px;">
    将 logo.png、banner.jpg、icons.png 置于与页面同级的 images/ 目录即可看到背景图效果。
  </p>
</body>
</html>

【代码注释】

核心逻辑

  • url() 在 LESS 中多数原样输出 到 CSS;路径解析以当前 .less 文件目录为基准,非 HTML 位置。
  • Logo/Banner 用 background-size: contain/cover;雪碧图用 background-position 负偏移切图标。

关键 API

场景 写法
同目录 images url("images/logo.png")
less 在 css/ 子目录 url("../images/logo.png")
雪碧图 no-repeat -48px 0 + 变量偏移

注意点

  • LESS 不会自动 base64 内联,需 webpack url-loader 等插件。
  • text-indent: -9999px 常用于 Logo 文字隐藏(可访问性上更推荐真实 alt/aria)。

实战场景

  • 电商 PC 头图、购物车图标;@logo-width / @sprite-path 换肤只改变量。

真实网站场景: 电商 PC 站头部 Logo、轮播 Banner、雪碧图图标(购物车、消息)普遍用背景图 + 精确偏移;LESS 变量统一管理尺寸与偏移,换肤时只改 @sprite-path 或主题目录。


4.11 高级特性:$prop、:extend()、属性合并与递归循环

本节聚合官方文档中四个常被忽视却极为实用的高级特性,深度掌握它们才能写出"工业级"LESS 代码。


4.11.1 $prop ------ 将属性值当变量引用(v3.0.0)

名词解释

术语 说明
$prop 语法 LESS v3.0.0 新增的属性引用语法,允许在同一规则块内将某个属性的值当作变量直接复用,无需额外定义变量。
懒加载(Lazy Evaluation) LESS 的变量/属性值求值策略:取当前作用域内最后一次赋值的结果,而非声明顺序。

$color$background 等形式直接引用同一规则块内的 CSS 属性值,避免重复书写:

less 复制代码
/* 变量定义 → 属性引用双重减少 */
.widget {
  color: #1a73e8;
  background-color: $color;  /* 直接复用 color 属性的值 */
  border-color: $color;
}

编译结果:

css 复制代码
.widget {
  color: #1a73e8;
  background-color: #1a73e8;
  border-color: #1a73e8;
}

注意:懒加载规则同样适用。 同一作用域中若 color 被多次赋值,$color最后一次赋值:

less 复制代码
.block {
  color: red;
  .inner {
    background-color: $color; /* 取外层 color 的最终值:blue */
  }
  color: blue;  /* 此行是最终值 */
}

编译为:

css 复制代码
.block { color: red; color: blue; }
.block .inner { background-color: blue; }

【代码注释】

核心逻辑

  • $color$background 引用同一规则块内已声明属性的值,编译期替换,零运行时开销。
  • 遵循 LESS 懒加载 :取作用域内该属性最后一次赋值,非书写顺序快照。

注意点

  • 子块 .inner { background: $color } 取外层 color 的最终值(见懒加载示例)。
  • @变量 不同:$prop 无需额外 @border-color: @accent,减少 Token 重复。

实战场景

  • Badge 边框/背景与文字同色;Icon 描边与填充联动。

经典场景: Ant Design 5.x 通过这种方式在组件内减少 CSS Token 变量声明数量。

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>$prop 属性引用演示</title>
  <style type="text/less">
    @accent: #6200ea;

    .badge {
      color: @accent;
      border: 1px solid $color;       /* 复用 color 值 */
      background: fade($color, 10%);  /* 复用 color 值并降低透明度 */
      padding: 2px 8px;
      border-radius: 999px;
      font-size: 12px;
      display: inline-block;
    }

    .tag {
      background: @accent;
      color: #fff;
      border-radius: 4px;
      padding: 4px 10px;

      &:hover {
        background: darken($background, 10%);  /* 复用 background 值 */
      }
    }
  </style>
  <script src="./less.js"></script>
</head>
<body style="padding:20px;font-family:sans-serif">
  <span class="badge">NEW</span>
  <span class="badge" style="--color:#e91e63">HOT</span>
  <br><br>
  <span class="tag">前端</span>
  <span class="tag">LESS</span>
</body>
</html>

【代码注释】

核心逻辑

  • .badgeborder: 1px solid $color → 边框与 color: @accent 一致;fade($color, 10%) 生成浅色背景。
  • .tag:hoverdarken($background, 10%)$background 指向当前 background 属性值。

关键 API

  • $color + fade():一套主色衍生边框、背景、悬停。
  • 编译后均为普通 CSS 颜色字面量。

注意点

  • $background 仅在已声明 background 的规则内有效。
  • 动态主题(运行时换肤)应配合 CSS Variables(§9),$prop 仅编译期。

实战场景

  • 标签、徽章、角标组件;减少 @accent-border@accent-bg 等重复变量。

4.11.2 @@variable ------ 变量的变量

LESS 支持用另一个变量的值作为变量名,实现动态变量选择:

less 复制代码
@primary:   #1a73e8;
@secondary: #ea4a36;
@success:   #34a853;

.section {
  @theme: primary;        /* 用字符串指定主题名 */
  color: @@theme;         /* 等价于 @primary → #1a73e8 */
}

编译结果:

css 复制代码
.section { color: #1a73e8; }

经典场景: 通过 @@ 实现"主题名 → 颜色值"的映射,在守卫(Guards)中组合使用可产生类似"主题切换"的效果:

less 复制代码
@themes: primary, secondary, success;
@primary:   #1a73e8;
@secondary: #ea4a36;
@success:   #34a853;

.generate-btn(@name) {
  .btn-@{name} {
    background: @@name;
    color: #fff;
    padding: 6px 14px;
    border-radius: 4px;
  }
}

.generate-btn(primary);
.generate-btn(secondary);
.generate-btn(success);

编译结果:

css 复制代码
.btn-primary   { background: #1a73e8; color: #fff; padding: 6px 14px; border-radius: 4px; }
.btn-secondary { background: #ea4a36; color: #fff; padding: 6px 14px; border-radius: 4px; }
.btn-success   { background: #34a853; color: #fff; padding: 6px 14px; border-radius: 4px; }

【代码注释】

核心逻辑

  • @@theme:先求 @theme"primary",再求 @primary#1a73e8变量的变量)。
  • .generate-btn(@name)@@name + .btn-@{name} 批量生成多色按钮类。

两步求值

复制代码
@name = primary  →  @@name  →  @primary  →  #1a73e8

注意点

  • @theme 存的是变量名 (无 @ 前缀的标识符),不是颜色字面量。
  • 可与递归 .gen-*((@i - 1)) 组合批量生成主题/尺寸类(§4.11.5)。

实战场景

  • 多主题色表、状态色映射;减少 .btn-primary / .btn-danger 手写重复。

4.11.3 :extend() ------ 选择器继承(v1.4.0)

名词解释

术语 说明
:extend() LESS 的选择器合并机制,将当前选择器"追加"到被扩展选择器的定义处,共享同一段 CSS 规则,从而减少编译后 CSS 的重复属性。
Mixin 与 Extend 的区别 Mixin 是复制属性 (每处调用都产生一份 CSS),Extend 是合并选择器(只有一份 CSS,多个选择器共享)。

基础语法:

less 复制代码
/* 公共基础类 */
.base-text {
  font-family: "Inter", sans-serif;
  line-height: 1.6;
  color: #333;
}

/* 标题扩展基础类 */
.heading {
  &:extend(.base-text);
  font-size: 24px;
  font-weight: 700;
}

/* 正文扩展基础类 */
.body-text {
  &:extend(.base-text);
  font-size: 16px;
}

编译结果:

css 复制代码
/* 三个选择器共享同一条规则------CSS 体积最小 */
.base-text,
.heading,
.body-text {
  font-family: "Inter", sans-serif;
  line-height: 1.6;
  color: #333;
}
.heading    { font-size: 24px; font-weight: 700; }
.body-text  { font-size: 16px; }

对比使用 Mixin:

less 复制代码
/* Mixin 方式------属性被复制 3 次,CSS 体积更大 */
.base-text() { font-family: "Inter", sans-serif; line-height: 1.6; color: #333; }
.heading    { .base-text(); font-size: 24px; }
.body-text  { .base-text(); font-size: 16px; }

Extend 的 all 关键字:扩展目标选择器的所有变体(包括伪类、组合选择器):

less 复制代码
.error {
  color: red;
  &:hover { color: darkred; }
  &.active { font-weight: bold; }
}

/* 不带 all:只扩展 .error 本身 */
.critical        { &:extend(.error); }

/* 带 all:扩展 .error 的所有变体(含 :hover、.active 等) */
.critical-all    { &:extend(.error all); }

完整可运行示例:

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>:extend() 选择器继承演示</title>
  <style type="text/less">
    /* ---- 公共基础样式 ---- */
    .card-base {
      border-radius: 8px;
      padding: 16px;
      box-shadow: 0 2px 8px rgba(0,0,0,.12);
      background: #fff;
    }

    /* ---- 通过 extend 继承,避免重复属性 ---- */
    .card-info    { &:extend(.card-base); border-left: 4px solid #1a73e8; }
    .card-success { &:extend(.card-base); border-left: 4px solid #34a853; }
    .card-danger  { &:extend(.card-base); border-left: 4px solid #ea4a36; }
    .card-warning { &:extend(.card-base); border-left: 4px solid #fbbc04; }

    body { padding: 20px; background: #f5f5f5; font-family: sans-serif; }
    .card-base + .card-base,
    .card-info + .card-info,
    [class^="card-"] + [class^="card-"] { margin-top: 12px; }
  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="card-info">    信息卡片:extend 让四种卡片共享 .card-base 的圆角/阴影/内边距</div>
  <div class="card-success"> 成功卡片:每种卡片只需额外声明自身的左边框颜色</div>
  <div class="card-danger">  危险卡片:编译后 CSS 中 .card-base 的属性只出现一次</div>
  <div class="card-warning"> 警告卡片:对比 Mixin,Extend 可显著减小样式文件体积</div>
</body>
</html>

【代码注释】

核心逻辑

  • &:extend(.card-base) 把当前选择器并入 .card-base 规则组,共享一份圆角/阴影/内边距。
  • Mixin 是复制属性 到每个调用处;Extend 是合并选择器,体积更小。

Extend vs Mixin

Extend Mixin
输出 选择器列表合并 每处一份属性
参数
hover 变体 all 关键字 更灵活

注意点

  • &:extend(.error all) 可连带扩展 :hover.active 等变体。
  • 静态基础类(卡片、排版)用 Extend;动态颜色/尺寸用 Mixin。

实战场景

  • 信息/成功/危险/警告四类卡片共享 .card-base;Bootstrap 按钮基础类思路类似。

何时用 Extend,何时用 Mixin:

场景 推荐
纯样式继承、减少 CSS 体积 :extend()
需要传入参数、动态生成样式 Mixin
状态/变体样式(hover、active) Mixin(更灵活)
框架级共享基础类(按钮基础等) :extend() + Mixin 混合

4.11.4 属性合并(Merge)------ ++_(v1.5.0 / v1.7.0)

名词解释

符号 说明 适用场景
property+: 将多个值以逗号拼接为一个属性 backgroundbox-shadowtransition
property+_: 将多个值以空格拼接为一个属性 transformbackground-position

逗号合并(+):

less 复制代码
/* 为元素叠加多个 box-shadow */
.shadow-base() {
  box-shadow+: inset 0 1px 3px rgba(0,0,0,.2);
}
.shadow-outer() {
  box-shadow+: 0 4px 12px rgba(0,0,0,.15);
}

.card {
  .shadow-base();
  .shadow-outer();
}

编译结果:

css 复制代码
.card {
  box-shadow: inset 0 1px 3px rgba(0,0,0,.2), 0 4px 12px rgba(0,0,0,.15);
}

空格合并(+_):

less 复制代码
/* 组合多个 transform 变换 */
.scale-mixin()  { transform+_: scale(1.05); }
.rotate-mixin() { transform+_: rotate(5deg); }

.animated {
  .scale-mixin();
  .rotate-mixin();
}

编译结果:

css 复制代码
.animated {
  transform: scale(1.05) rotate(5deg);
}

完整可运行示例:

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>属性合并 Merge 演示</title>
  <style type="text/less">
    /* 阴影混合库 */
    .shadow-inset()  { box-shadow+: inset 0 2px 4px rgba(0,0,0,.15); }
    .shadow-drop()   { box-shadow+: 0 6px 18px rgba(0,0,0,.12); }
    .shadow-glow(@c) { box-shadow+: 0 0 0 3px fade(@c, 30%); }

    /* 变换混合库 */
    .scale(@s: 1.05)  { transform+_: scale(@s); }
    .rotate(@deg: 3deg) { transform+_: rotate(@deg); }

    .btn {
      padding: 10px 22px;
      border-radius: 6px;
      background: #1a73e8;
      color: #fff;
      border: none;
      cursor: pointer;
      font-size: 15px;
      transition: all .2s;

      /* 叠加三层阴影 */
      .shadow-inset();
      .shadow-drop();
      .shadow-glow(#1a73e8);

      &:hover {
        .scale(1.03);
        .shadow-drop();
        .shadow-glow(#1a73e8);
      }

      &:active {
        .scale(0.97);
        .rotate(1deg);
      }
    }

    body { padding: 40px; background: #f0f4f8; font-family: sans-serif; }
  </style>
  <script src="./less.js"></script>
</head>
<body>
  <button class="btn">点击我</button>
</body>
</html>

【代码注释】

核心逻辑

  • box-shadow+::多个 mixin 的阴影值用逗号拼接,实现多层阴影叠加。
  • transform+_::多个变换用空格 拼接为 transform: scale(...) rotate(...),避免后者覆盖前者。

合并符号

写法 拼接符 典型属性
property+: , box-shadow, background, transition
property+_: 空格 transform, background-position

注意点

  • 参与合并的每条声明都必须带 ++_,否则后写的 mixin 覆盖而非合并。
  • v1.5.0+ 特性;与 @arguments 前缀工厂是不同维度的复用。

实战场景

  • 按钮同时内阴影 + 外阴影 + 发光;hover 叠加 scale + rotate

4.11.5 递归循环(Recursive Loop)------ 批量生成工具类

名词解释

术语 说明
递归 Mixin LESS 中没有原生 for 循环,通过"Mixin 调用自身 + Guards 控制终止条件"实现循环逻辑。
each() 函数 v3.7.0 引入的列表迭代函数,语法更简洁,推荐在支持 v3.7+ 的项目中使用。

方式一:Guards 递归(兼容所有版本):

less 复制代码
/* 生成 .mt-1 ~ .mt-10 的 margin-top 工具类 */
@base-spacing: 4px;
@max-steps: 10;

.generate-spacing(@n) when (@n > 0) {
  .generate-spacing((@n - 1));        /* 先递归到底 */
  .mt-@{n} { margin-top:  (@n * @base-spacing); }
  .mb-@{n} { margin-bottom:(@n * @base-spacing); }
  .pt-@{n} { padding-top:  (@n * @base-spacing); }
  .pb-@{n} { padding-bottom:(@n * @base-spacing); }
}

.generate-spacing(@max-steps);

编译结果(节选):

css 复制代码
.mt-1 { margin-top: 4px; }  .mb-1 { margin-bottom: 4px; }
.mt-2 { margin-top: 8px; }  .mb-2 { margin-bottom: 8px; }
/* ...  */
.mt-10 { margin-top: 40px; } .mb-10 { margin-bottom: 40px; }

方式二:each() 函数(v3.7.0+,推荐):

less 复制代码
/* 使用 each 遍历颜色主题列表 */
@themes: {
  primary:   #1a73e8;
  success:   #34a853;
  danger:    #ea4a36;
  warning:   #fbbc04;
};

each(@themes, {
  .text-@{key}    { color: @value; }
  .bg-@{key}      { background-color: @value; }
  .border-@{key}  { border-color: @value; }
});

完整可运行示例(栅格 + 间距批量生成):

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>递归循环 ------ 批量生成工具类</title>
  <style type="text/less">
    /* ---- 递归生成 12 列栅格 ---- */
    @columns: 12;
    .gen-col(@i) when (@i > 0) {
      .gen-col((@i - 1));
      .col-@{i} {
        width: percentage(@i / @columns);
        float: left;
        box-sizing: border-box;
        padding: 0 6px;
      }
    }
    .gen-col(@columns);

    /* ---- 递归生成间距工具类(0-8,步长 4px) ---- */
    .gen-spacing(@n) when (@n >= 0) {
      .gen-spacing((@n - 1));
      .m-@{n}  { margin:         (@n * 4px); }
      .mt-@{n} { margin-top:     (@n * 4px); }
      .mb-@{n} { margin-bottom:  (@n * 4px); }
      .p-@{n}  { padding:        (@n * 4px); }
    }
    .gen-spacing(8);

    /* ---- 布局辅助 ---- */
    .row::after { content:""; display:block; clear:both; }
    .demo { padding: 20px; font-family: sans-serif; }
    [class^="col-"] {
      background: #e8f0fe; border: 1px solid #1a73e8;
      text-align: center; padding: 8px 0; font-size: 13px;
      margin-bottom: 8px;
    }
  </style>
  <script src="./less.js"></script>
</head>
<body class="demo">
  <h3>递归生成 12 列栅格 + 间距工具类</h3>
  <div class="row">
    <div class="col-3">col-3</div>
    <div class="col-3">col-3</div>
    <div class="col-6">col-6</div>
  </div>
  <div class="row mt-4">
    <div class="col-4">col-4</div>
    <div class="col-8">col-8</div>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  • .gen-col(@i) when (@i > 0):Guard 为终止条件,@i 到 0 时不再匹配,递归结束。
  • 体内先 .gen-col((@i - 1)) 再输出 .col-@{i},保证 .col-1....col-12 顺序稳定。
  • each(@themes, { ... })(v3.7+)可替代手写递归,遍历 map 生成 .text-primary 等。

关键 API

方式 适用
Guards 递归 全版本;栅格、间距工具类
each() v3.7+;主题色表批量生成

注意点

  • 递归深度过大增加编译时间;生产常用预生成工具类或 PostCSS。
  • percentage(@i / @columns) 括号除法,避免 / 解析歧义。

实战场景

  • Tailwind 式 mt-1mt-10、Bootstrap 12 列 .col-* 源码生成。

5 LESS 知识脉络总结

5.1 核心知识体系对照表

特性 LESS 语法 类比概念 核心价值
变量 @var: value 常量 / 配置项 主题统一修改
混合 .mixin() 函数 样式代码复用
嵌套 { child {} } 树形结构 减少选择器重复
运算 + - * (/) 算术表达式 动态尺寸计算
条件 when (@x = y) if/switch CSS 算法逻辑
导入 @import "file" 模块化 代码组织拆分
函数 darken() 标准库 颜色/数学工具

5.2 选择器嵌套规则总结

#mermaid-svg-5HRGEICoOGJ3SBHd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5HRGEICoOGJ3SBHd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5HRGEICoOGJ3SBHd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5HRGEICoOGJ3SBHd .error-icon{fill:#552222;}#mermaid-svg-5HRGEICoOGJ3SBHd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5HRGEICoOGJ3SBHd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5HRGEICoOGJ3SBHd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5HRGEICoOGJ3SBHd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5HRGEICoOGJ3SBHd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5HRGEICoOGJ3SBHd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5HRGEICoOGJ3SBHd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5HRGEICoOGJ3SBHd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5HRGEICoOGJ3SBHd .marker.cross{stroke:#333333;}#mermaid-svg-5HRGEICoOGJ3SBHd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5HRGEICoOGJ3SBHd p{margin:0;}#mermaid-svg-5HRGEICoOGJ3SBHd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5HRGEICoOGJ3SBHd .cluster-label text{fill:#333;}#mermaid-svg-5HRGEICoOGJ3SBHd .cluster-label span{color:#333;}#mermaid-svg-5HRGEICoOGJ3SBHd .cluster-label span p{background-color:transparent;}#mermaid-svg-5HRGEICoOGJ3SBHd .label text,#mermaid-svg-5HRGEICoOGJ3SBHd span{fill:#333;color:#333;}#mermaid-svg-5HRGEICoOGJ3SBHd .node rect,#mermaid-svg-5HRGEICoOGJ3SBHd .node circle,#mermaid-svg-5HRGEICoOGJ3SBHd .node ellipse,#mermaid-svg-5HRGEICoOGJ3SBHd .node polygon,#mermaid-svg-5HRGEICoOGJ3SBHd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5HRGEICoOGJ3SBHd .rough-node .label text,#mermaid-svg-5HRGEICoOGJ3SBHd .node .label text,#mermaid-svg-5HRGEICoOGJ3SBHd .image-shape .label,#mermaid-svg-5HRGEICoOGJ3SBHd .icon-shape .label{text-anchor:middle;}#mermaid-svg-5HRGEICoOGJ3SBHd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5HRGEICoOGJ3SBHd .rough-node .label,#mermaid-svg-5HRGEICoOGJ3SBHd .node .label,#mermaid-svg-5HRGEICoOGJ3SBHd .image-shape .label,#mermaid-svg-5HRGEICoOGJ3SBHd .icon-shape .label{text-align:center;}#mermaid-svg-5HRGEICoOGJ3SBHd .node.clickable{cursor:pointer;}#mermaid-svg-5HRGEICoOGJ3SBHd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5HRGEICoOGJ3SBHd .arrowheadPath{fill:#333333;}#mermaid-svg-5HRGEICoOGJ3SBHd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5HRGEICoOGJ3SBHd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5HRGEICoOGJ3SBHd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5HRGEICoOGJ3SBHd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5HRGEICoOGJ3SBHd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5HRGEICoOGJ3SBHd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5HRGEICoOGJ3SBHd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5HRGEICoOGJ3SBHd .cluster text{fill:#333;}#mermaid-svg-5HRGEICoOGJ3SBHd .cluster span{color:#333;}#mermaid-svg-5HRGEICoOGJ3SBHd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5HRGEICoOGJ3SBHd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5HRGEICoOGJ3SBHd rect.text{fill:none;stroke-width:0;}#mermaid-svg-5HRGEICoOGJ3SBHd .icon-shape,#mermaid-svg-5HRGEICoOGJ3SBHd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5HRGEICoOGJ3SBHd .icon-shape p,#mermaid-svg-5HRGEICoOGJ3SBHd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5HRGEICoOGJ3SBHd .icon-shape .label rect,#mermaid-svg-5HRGEICoOGJ3SBHd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5HRGEICoOGJ3SBHd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5HRGEICoOGJ3SBHd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5HRGEICoOGJ3SBHd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 父选择器 .parent
后代 .child
直接子代 > .child
紧邻兄弟 + .sibling
通用兄弟 ~ .sibling
& 父引用
交集 &.active
伪类 &:hover
伪元素 &::after
属性选择器 &attr

5.3 混合参数传递规则

#mermaid-svg-bCknfS98Fb3lzDfE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bCknfS98Fb3lzDfE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bCknfS98Fb3lzDfE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bCknfS98Fb3lzDfE .error-icon{fill:#552222;}#mermaid-svg-bCknfS98Fb3lzDfE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bCknfS98Fb3lzDfE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bCknfS98Fb3lzDfE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bCknfS98Fb3lzDfE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bCknfS98Fb3lzDfE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bCknfS98Fb3lzDfE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bCknfS98Fb3lzDfE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bCknfS98Fb3lzDfE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bCknfS98Fb3lzDfE .marker.cross{stroke:#333333;}#mermaid-svg-bCknfS98Fb3lzDfE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bCknfS98Fb3lzDfE p{margin:0;}#mermaid-svg-bCknfS98Fb3lzDfE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bCknfS98Fb3lzDfE .cluster-label text{fill:#333;}#mermaid-svg-bCknfS98Fb3lzDfE .cluster-label span{color:#333;}#mermaid-svg-bCknfS98Fb3lzDfE .cluster-label span p{background-color:transparent;}#mermaid-svg-bCknfS98Fb3lzDfE .label text,#mermaid-svg-bCknfS98Fb3lzDfE span{fill:#333;color:#333;}#mermaid-svg-bCknfS98Fb3lzDfE .node rect,#mermaid-svg-bCknfS98Fb3lzDfE .node circle,#mermaid-svg-bCknfS98Fb3lzDfE .node ellipse,#mermaid-svg-bCknfS98Fb3lzDfE .node polygon,#mermaid-svg-bCknfS98Fb3lzDfE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bCknfS98Fb3lzDfE .rough-node .label text,#mermaid-svg-bCknfS98Fb3lzDfE .node .label text,#mermaid-svg-bCknfS98Fb3lzDfE .image-shape .label,#mermaid-svg-bCknfS98Fb3lzDfE .icon-shape .label{text-anchor:middle;}#mermaid-svg-bCknfS98Fb3lzDfE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bCknfS98Fb3lzDfE .rough-node .label,#mermaid-svg-bCknfS98Fb3lzDfE .node .label,#mermaid-svg-bCknfS98Fb3lzDfE .image-shape .label,#mermaid-svg-bCknfS98Fb3lzDfE .icon-shape .label{text-align:center;}#mermaid-svg-bCknfS98Fb3lzDfE .node.clickable{cursor:pointer;}#mermaid-svg-bCknfS98Fb3lzDfE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bCknfS98Fb3lzDfE .arrowheadPath{fill:#333333;}#mermaid-svg-bCknfS98Fb3lzDfE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bCknfS98Fb3lzDfE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bCknfS98Fb3lzDfE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bCknfS98Fb3lzDfE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bCknfS98Fb3lzDfE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bCknfS98Fb3lzDfE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bCknfS98Fb3lzDfE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bCknfS98Fb3lzDfE .cluster text{fill:#333;}#mermaid-svg-bCknfS98Fb3lzDfE .cluster span{color:#333;}#mermaid-svg-bCknfS98Fb3lzDfE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bCknfS98Fb3lzDfE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bCknfS98Fb3lzDfE rect.text{fill:none;stroke-width:0;}#mermaid-svg-bCknfS98Fb3lzDfE .icon-shape,#mermaid-svg-bCknfS98Fb3lzDfE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bCknfS98Fb3lzDfE .icon-shape p,#mermaid-svg-bCknfS98Fb3lzDfE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bCknfS98Fb3lzDfE .icon-shape .label rect,#mermaid-svg-bCknfS98Fb3lzDfE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bCknfS98Fb3lzDfE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bCknfS98Fb3lzDfE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bCknfS98Fb3lzDfE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 调用混合
参数模式
顺序传参

.mixin 600px 400px
按名传参

.mixin @h:600px @w:400px
使用默认值

省略有默认值的参数
LESS 按位置匹配形参
顺序可以不同
默认值生效

5.4 LESS 变量 vs CSS 自定义属性(CSS Variables)

维度 LESS 变量 (@var) CSS 自定义属性 (--var)
处理阶段 编译时(静态) 运行时(动态)
浏览器支持 100%(编译为纯 CSS) IE 不支持
JavaScript 访问 无法直接访问 getPropertyValue('--var')
动态修改 不可以 可以(适合暗色模式切换)
作用域 LESS 词法作用域 CSS 级联作用域
适用场景 构建时主题、工程化 运行时换肤、JS 交互

5.5 特性 × 真实网站应用矩阵

LESS 特性 典型 CSS 能力 业界使用举例 归纳要点
变量 @ 主题色、间距、字号 Ant Design 3、Bootstrap 3 主题定制 编译期替换,改一处全局生效
混合 可复用声明块 Bootstrap .button-variant()、清除浮动 () 不输出多余类
Guards when 条件化规则 多方向箭头、响应式间距分支 @if,靠模式匹配
嵌套 & 缩短选择器 导航 li.active:hover 控制深度 ≤3 层
运算 calc 类计算 12 栅格 percentage(1/12) 除法必须加括号
函数 颜色变换 按钮 hover 自动 darken() 参数是百分比不是 0--1
@import 模块化 Bootstrap 入口聚合数十模块 变量文件最先导入
命名空间 工具库隔离 内部 UI 库 utils 包 #ns > .mixin() 调用

#mermaid-svg-qANZfozJx4s0Xo6k{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qANZfozJx4s0Xo6k .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qANZfozJx4s0Xo6k .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qANZfozJx4s0Xo6k .error-icon{fill:#552222;}#mermaid-svg-qANZfozJx4s0Xo6k .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qANZfozJx4s0Xo6k .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qANZfozJx4s0Xo6k .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qANZfozJx4s0Xo6k .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qANZfozJx4s0Xo6k .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qANZfozJx4s0Xo6k .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qANZfozJx4s0Xo6k .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qANZfozJx4s0Xo6k .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qANZfozJx4s0Xo6k .marker.cross{stroke:#333333;}#mermaid-svg-qANZfozJx4s0Xo6k svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qANZfozJx4s0Xo6k p{margin:0;}#mermaid-svg-qANZfozJx4s0Xo6k .pieCircle{stroke:#000000;stroke-width:2px;opacity:0.7;}#mermaid-svg-qANZfozJx4s0Xo6k .pieOuterCircle{stroke:#000000;stroke-width:1px;fill:none;}#mermaid-svg-qANZfozJx4s0Xo6k .pieTitleText{text-anchor:middle;font-size:25px;fill:#000000;font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-qANZfozJx4s0Xo6k .slice{font-family:"trebuchet ms",verdana,arial,sans-serif;fill:#000000;font-size:17px;}#mermaid-svg-qANZfozJx4s0Xo6k .legend text{fill:#000000;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:17px;}#mermaid-svg-qANZfozJx4s0Xo6k :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 45% 20% 20% 15% 大型站点 LESS 工程文件占比(示意) variables mixins components layout/pages

【代码注释】

核心逻辑

  • 大型 LESS 工程 ≈ variables + mixins + components + pages 四层;组件样式占主导(饼图 ~45%)。
  • §5.1~5.5 对照表把语法点映射到 Ant Design、Bootstrap 等真实用法,便于复习闭环。

工程占比启示

目录 职责
variables 主题 Token
mixins 清除浮动、按钮变体
components 按钮/表单/导航 skin
layout/pages Topbar、版心、频道页

注意点

  • Bootstrap 4+ 默认 SCSS,但 Guards、嵌套、@import 组织方式与 LESS 互通。
  • 运行时换肤见 §5.4:LESS 编译期 + CSS Variables 运行期可组合(§9)。

6 PC 项目实战:用 LESS 构建完整页面

以电商网站为背景,展示 LESS 在完整项目中的工程化实践。以下模块按 准备 → Topbar → Header → Nav 递进,与目录中的语法练习一一对应,最终合并为可运行的单页结构。

6.0 工程准备(container 与公共混合)

在写页面模块前,先约定 版心宽度清除浮动 等公共能力,避免各区块重复写布局代码。

less 复制代码
/* 版心容器:水平居中 */
.container {
  margin-left:  auto;
  margin-right: auto;
  width:        1200px;
}

.clearfix() {
  &::after {
    content: "";
    display: block;
    clear: both;
  }
}
html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>工程准备 - 版心演示</title>
  <style type="text/less">
    .clearfix() {
      &::after { content: ""; display: block; clear: both; }
    }
    .container {
      margin: 0 auto;
      width: 1200px;
      background: #f5f5f5;
      .clearfix();
    }
    .container .block {
      float: left;
      width: 380px;
      height: 60px;
      margin: 10px;
      background: #fff;
      border: 1px solid #e0e0e0;
      line-height: 60px;
      text-align: center;
    }
  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="container">
    <div class="block">版心内模块 A</div>
    <div class="block">版心内模块 B</div>
    <div class="block">版心内模块 C</div>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  • .containermargin: 0 auto + width: 1200px 实现 PC 版心居中,与后续 Topbar/Header 共用。
  • .clearfix() mixin 在浮动子元素父级调用,用 &::after 撑开高度。

注意点

  • 版心宽度建议抽为 @container-width,响应式时改媒体查询而非散落 magic number。
  • 现代布局可用 Flex/Grid;本课保留 float 以对应经典电商 PC 结构。

实战场景

  • §6.1 文件树中 variables.lessmain.less 聚合;本节为页面模块写代码前的公共底座。

6.1 项目文件结构

复制代码
css/
├── variables.less    ← 全局变量(颜色、字号、间距)
├── reset.less        ← 重置样式
├── mixins.less       ← 混合库
└── index.less        ← 入口文件(@import 其他文件 + 页面样式)

6.2 variables.less(全局变量)

less 复制代码
/* ── 品牌色系 ── */
@form-red:    #ea4a36;   /* 表单强调色(搜索框描边) */
@btn-red:     #e1251b;   /* 主按钮色 */
@sep-color:   #b3aeae;   /* 分隔线 */
@text-dark:   #333;
@text-gray:   #666;
@text-light:  #999;
@bg-gray:     #f5f5f5;

/* ── 布局变量 ── */
@container-width: 1190px;
@gutter:           15px;
@header-height:    100px;
@topbar-height:    30px;

/* ── 字体变量 ── */
@font-size-base:   14px;
@font-size-sm:     12px;
@font-size-lg:     16px;

/* ── 断点变量 ── */
@bp-sm:   576px;
@bp-md:   768px;
@bp-lg:   992px;
@bp-xl:   1200px;

6.3 reset.less(重置样式)

less 复制代码
* {
  margin:  0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
  font-size: @font-size-base;
  color: @text-dark;
  background: #fff;
}

a {
  text-decoration: none;
  color: inherit;
  &:hover { color: @btn-red; }
}

ul, ol { list-style: none; }
img     { display: block; max-width: 100%; }

6.4 mixins.less(混合库)

less 复制代码
/* 清除浮动 */
.clearfix() {
  &::after {
    content: "";
    display: block;
    clear: both;
  }
}

/* 文字截断(单行省略号) */
.ellipsis() {
  overflow:      hidden;
  white-space:   nowrap;
  text-overflow: ellipsis;
}

/* 绝对定位居中 */
.abs-center(@w, @h) {
  position:    absolute;
  width:       @w;
  height:      @h;
  left:        50%;
  top:         50%;
  margin-left: -(@w / 2);
  margin-top:  -(@h / 2);
}

/* CSS 三角形(用于下拉箭头等) */
.triangle(@size, @color, @dir) when (@dir = up) {
  width: 0; height: 0;
  border-style: solid;
  border-width: @size;
  border-color: transparent transparent @color transparent;
}
.triangle(@size, @color, @dir) when (@dir = down) {
  width: 0; height: 0;
  border-style: solid;
  border-width: @size;
  border-color: @color transparent transparent transparent;
}

/* 多行文字截断 */
.clamp(@lines) {
  display:            -webkit-box;
  -webkit-line-clamp: @lines;
  -webkit-box-orient: vertical;
  overflow:           hidden;
}

6.5 index.less(页面样式)

less 复制代码
@import "variables";
@import "reset";
@import "mixins";

/* ────────────────────────────────────
   顶部工具栏(Topbar)
──────────────────────────────────── */
.topbar {
  height:     @topbar-height;
  background: @bg-gray;
  border-bottom: 1px solid #e0e0e0;
  line-height: @topbar-height;

  .topbar-inner {
    width:  @container-width;
    margin: 0 auto;
    .clearfix();

    .topbar-left {
      float: left;
      font-size: @font-size-sm;
      color: @text-gray;
    }

    .topbar-right {
      float: right;

      a {
        font-size: @font-size-sm;
        color: @text-gray;
        padding: 0 8px;
        border-right: 1px solid @sep-color;

        &:last-child { border-right: none; }
        &:hover      { color: @btn-red; }
      }
    }
  }
}

/* ────────────────────────────────────
   Logo 与搜索框
──────────────────────────────────── */
.header {
  height:     @header-height;
  background: #fff;

  .header-inner {
    width:  @container-width;
    margin: 0 auto;
    .clearfix();
  }

  .logo {
    float:       left;
    width:       200px;
    height:      @header-height;
    line-height: @header-height;
  }

  .search-bar {
    float:      left;
    margin-top: 25px;
    margin-left: 80px;

    .search-input {
      width:       500px;
      height:      36px;
      padding:     0 12px;
      border:      2px solid @form-red;
      border-right: none;
      outline:     none;
      font-size:   @font-size-base;
    }

    .search-btn {
      width:      80px;
      height:     36px;
      background: @btn-red;
      color:      #fff;
      border:     none;
      cursor:     pointer;
      font-size:  @font-size-base;
      vertical-align: top;

      &:hover {
        background: darken(@btn-red, 10%);
      }
    }
  }
}

/* ────────────────────────────────────
   页面导航(Nav)
──────────────────────────────────── */
.nav {
  height:     40px;
  background: @btn-red;

  .nav-inner {
    width:  @container-width;
    margin: 0 auto;
    .clearfix();
  }

  .nav-list {
    li {
      float:       left;
      height:      40px;
      line-height: 40px;
      padding:     0 20px;
      font-size:   @font-size-base;

      a {
        color: #fff;

        &:hover {
          background: darken(@btn-red, 15%);
        }
      }

      &.active > a {
        background: darken(@btn-red, 20%);
        font-weight: bold;
      }
    }
  }
}

完整可运行 HTML 示例(单文件演示页面结构):

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>PC 项目实战演示</title>
  <style type="text/less">

    /* ── 变量 ── */
    @btn-red:     #e1251b;
    @form-red:    #ea4a36;
    @sep-color:   #b3aeae;
    @text-gray:   #666;
    @bg-gray:     #f5f5f5;
    @font-sm:     12px;
    @font-base:   14px;
    @container:   1190px;

    /* ── 混合 ── */
    .clearfix() {
      &::after { content: ""; display: block; clear: both; }
    }

    /* ── 全局重置 ── */
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: "Microsoft YaHei", sans-serif; font-size: @font-base; }
    a { text-decoration: none; color: inherit; }
    ul { list-style: none; }

    /* ── Topbar ── */
    .topbar {
      height: 30px;
      background: @bg-gray;
      border-bottom: 1px solid #e0e0e0;
      line-height: 30px;

      .inner {
        width: @container;
        margin: 0 auto;
        .clearfix();
      }

      .left {
        float: left;
        font-size: @font-sm;
        color: @text-gray;
      }

      .right {
        float: right;
        a {
          font-size: @font-sm;
          color: @text-gray;
          padding: 0 8px;
          border-right: 1px solid @sep-color;
          &:last-child { border-right: none; }
          &:hover { color: @btn-red; }
        }
      }
    }

    /* ── Header ── */
    .header {
      height: 80px;
      background: #fff;
      border-bottom: 2px solid @btn-red;

      .inner {
        width: @container;
        margin: 0 auto;
        .clearfix();
      }

      .logo {
        float: left;
        height: 80px;
        line-height: 80px;
        font-size: 28px;
        font-weight: bold;
        color: @btn-red;
        letter-spacing: 2px;
      }

      .search {
        float: left;
        margin: 20px 0 0 60px;

        input {
          width: 400px;
          height: 36px;
          padding: 0 12px;
          border: 2px solid @form-red;
          border-right: none;
          outline: none;
          vertical-align: top;
        }

        button {
          width: 70px;
          height: 36px;
          background: @btn-red;
          color: #fff;
          border: none;
          cursor: pointer;
          vertical-align: top;
          &:hover { background: darken(@btn-red, 10%); }
        }
      }
    }

    /* ── Nav ── */
    .nav {
      height: 40px;
      background: @btn-red;

      .inner {
        width: @container;
        margin: 0 auto;
      }

      li {
        display: inline-block;
        height: 40px;
        line-height: 40px;

        a {
          display: block;
          padding: 0 18px;
          color: #fff;
          font-size: @font-base;

          &:hover {
            background: darken(@btn-red, 15%);
          }
        }
      }
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>

  <!-- Topbar -->
  <div class="topbar">
    <div class="inner">
      <div class="left">欢迎来到前端学习商城!</div>
      <div class="right">
        <a href="#">请登录</a>
        <a href="#">免费注册</a>
        <a href="#">我的订单</a>
        <a href="#">客户服务</a>
        <a href="#">网站导航</a>
      </div>
    </div>
  </div>

  <!-- Header -->
  <div class="header">
    <div class="inner">
      <div class="logo">SHOP</div>
      <div class="search">
        <input type="text" placeholder="搜索商品、品牌...">
        <button>搜 索</button>
      </div>
    </div>
  </div>

  <!-- Nav -->
  <div class="nav">
    <div class="inner">
      <ul>
        <li><a href="#">全部商品分类</a></li>
        <li><a href="#">服装城</a></li>
        <li><a href="#">美妆馆</a></li>
        <li><a href="#">电器城</a></li>
        <li><a href="#">手机</a></li>
        <li><a href="#">家居</a></li>
        <li><a href="#">超市</a></li>
      </ul>
    </div>
  </div>

</body>
</html>

【代码注释】

核心逻辑

  • 变量层@btn-red@container@font-base 集中品牌色与排版;改一处全站联动。
  • 结构层 :Topbar(浮动左右)→ Header(Logo + 搜索)→ Nav(li 内链 hover),均 .inner 限宽 + .clearfix()
  • 交互层 :搜索按钮、导航 a:hoverdarken(@btn-red, 10%/15%) 自动派生深色。

模块对照

区块 LESS 要点
Topbar 浮动 + clearfix + 链接色变量
Header 嵌套 .search input/button
Nav display:inline-block + hover darken

实战场景

  • 京东/天猫式 PC 顶栏三段式;与 §4 变量、嵌套、混合、函数综合演练。

7 经典应用场景与真实网站案例

7.1 主题换肤系统

场景: 同一套 UI 组件需要支持多套配色主题(默认/深色/品牌色)。

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>主题换肤示例</title>
  <style type="text/less">

    /* ── 主题变量定义 ── */
    /* 默认主题(蓝色)*/
    .theme-blue {
      @primary: #1a73e8;
      @primary-hover: darken(@primary, 10%);
      @primary-light: lighten(@primary, 45%);

      .btn {
        background: @primary;
        color: #fff;
        &:hover { background: @primary-hover; }
      }

      .badge {
        background: @primary-light;
        color: @primary;
      }
    }

    /* 红色主题 */
    .theme-red {
      @primary: #ea4a36;
      @primary-hover: darken(@primary, 10%);
      @primary-light: lighten(@primary, 40%);

      .btn {
        background: @primary;
        color: #fff;
        &:hover { background: @primary-hover; }
      }

      .badge {
        background: @primary-light;
        color: @primary;
      }
    }

    /* 公共样式 */
    .btn {
      padding: 8px 20px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
      display: inline-block;
      margin: 8px;
    }

    .badge {
      padding: 4px 10px;
      border-radius: 12px;
      font-size: 12px;
      display: inline-block;
      margin: 8px;
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="theme-blue">
    <button class="btn">蓝色主题按钮</button>
    <span class="badge">蓝色徽章</span>
  </div>

  <div class="theme-red">
    <button class="btn">红色主题按钮</button>
    <span class="badge">红色徽章</span>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  • .theme-blue / .theme-red 父块内定义 @primary 等变量,子 .btn.badge 词法作用域自动取对应主题色。
  • darken / lighten 在同一作用域内推导 hover、浅色背景,无需为每主题手写色值表。

构建时 vs 运行时

方式 机制
本例(父类作用域) 编译出多套 CSS 规则
Ant Design modifyVars 构建覆盖全局 @primary-color
CSS Variables(§9) 运行时 data-theme 切换

实战场景

  • 同一 DOM 挂不同主题 class;B 端后台多品牌皮肤。

真实网站: Ant Design、Element Plus(早期版本)、WeUI 均采用此方案。


7.2 响应式栅格系统

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 栅格系统</title>
  <style type="text/less">

    @columns:     12;
    @gutter:      30px;
    @container-sm: 750px;
    @container-md: 970px;
    @container-lg: 1170px;

    /* 生成栅格列的混合 */
    .make-col(@n) {
      float: left;
      width: percentage(@n / @columns);
      padding: 0 (@gutter / 2);
      min-height: 1px;
    }

    .row {
      margin: 0 -(@gutter / 2);
      &::after { content: ""; display: block; clear: both; }
    }

    /* 手动展开常用列宽(演示)*/
    .col-1  { .make-col(1);  }
    .col-2  { .make-col(2);  }
    .col-3  { .make-col(3);  }
    .col-4  { .make-col(4);  }
    .col-6  { .make-col(6);  }
    .col-8  { .make-col(8);  }
    .col-9  { .make-col(9);  }
    .col-12 { .make-col(12); }

    /* 容器 */
    .container {
      width: 100%;
      margin: 0 auto;
      padding: 0 15px;

      @media (min-width: 768px)  { width: @container-sm; }
      @media (min-width: 992px)  { width: @container-md; }
      @media (min-width: 1200px) { width: @container-lg; }
    }

    /* 辅助样式 */
    [class*="col-"] {
      background: #e8f4fd;
      border: 1px solid #1a73e8;
      height: 50px;
      line-height: 50px;
      text-align: center;
      font-size: 12px;
      color: #1a73e8;
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="container">
    <div class="row">
      <div class="col-3">col-3 (25%)</div>
      <div class="col-3">col-3 (25%)</div>
      <div class="col-3">col-3 (25%)</div>
      <div class="col-3">col-3 (25%)</div>
    </div>
    <div class="row" style="margin-top:10px">
      <div class="col-8">col-8 (66.67%)</div>
      <div class="col-4">col-4 (33.33%)</div>
    </div>
    <div class="row" style="margin-top:10px">
      <div class="col-6">col-6 (50%)</div>
      <div class="col-6">col-6 (50%)</div>
    </div>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  • .make-col(@n)width: percentage(@n / @columns) 将 1~12 列转为百分比宽。
  • .row { margin: 0 -(@gutter/2) } + 列 padding: 0 (@gutter/2):gutter 由负 margin 抵消,列间留白一致。

关键公式

复制代码
列宽 = percentage(n / 12)
半 gutter = @gutter / 2

注意点

  • 浮动栅格需 .row::after { clear: both };容器 @media 切换 @container-sm/md/lg
  • 与 §4.11.5 递归 .gen-col 等价,本例手写 .col-1.col-12 便于阅读。

实战场景

  • Bootstrap 3 LESS 源码、后台管理系统多栏布局。

真实网站: Bootstrap 3/4 的栅格系统正是基于这套逻辑,Bootstrap 3 使用 LESS 编写,代码结构与此示例高度相似。


7.3 按钮组件库(Mixin 模式)

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 按钮组件库</title>
  <style type="text/less">

    /* 按钮基础 mixin */
    .btn-base() {
      display:         inline-block;
      padding:         8px 16px;
      border:          1px solid transparent;
      border-radius:   4px;
      font-size:       14px;
      font-weight:     400;
      cursor:          pointer;
      text-align:      center;
      white-space:     nowrap;
      vertical-align:  middle;
      user-select:     none;
      transition:      all .15s ease-in-out;
      text-decoration: none;

      &:disabled {
        opacity: .65;
        cursor:  not-allowed;
      }
    }

    /* 颜色变体 mixin */
    .btn-variant(@bg, @border, @color: #fff) {
      background:   @bg;
      border-color: @border;
      color:        @color;

      &:hover, &:focus {
        background:   darken(@bg, 8%);
        border-color: darken(@border, 12%);
      }

      &:active {
        background:   darken(@bg, 15%);
        border-color: darken(@border, 20%);
      }
    }

    /* 尺寸变体 */
    .btn-size(@padding-y, @padding-x, @font-size, @border-radius) {
      padding:       @padding-y @padding-x;
      font-size:     @font-size;
      border-radius: @border-radius;
    }

    /* 生成按钮类 */
    .btn           { .btn-base(); }
    .btn-primary   { .btn-base(); .btn-variant(#1a73e8, #1a73e8); }
    .btn-success   { .btn-base(); .btn-variant(#34a853, #2e9147); }
    .btn-danger    { .btn-base(); .btn-variant(#ea4a36, #d43b27); }
    .btn-warning   { .btn-base(); .btn-variant(#fbbc04, #e8ab00, #333); }
    .btn-secondary { .btn-base(); .btn-variant(#6c757d, #6c757d); }
    .btn-outline   { .btn-base(); .btn-variant(transparent, #1a73e8, #1a73e8); }

    /* 尺寸修饰 */
    .btn-lg { .btn-size(10px, 20px, 16px, 6px); }
    .btn-sm { .btn-size(5px,  10px, 12px, 3px); }

    /* 布局 */
    body { padding: 20px; }
    .btn-row { margin-bottom: 12px; }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="btn-row">
    <button class="btn-primary">Primary</button>
    <button class="btn-success">Success</button>
    <button class="btn-danger">Danger</button>
    <button class="btn-warning">Warning</button>
    <button class="btn-secondary">Secondary</button>
    <button class="btn-outline">Outline</button>
  </div>

  <div class="btn-row">
    <button class="btn-primary btn-lg">大号按钮</button>
    <button class="btn-primary">默认按钮</button>
    <button class="btn-primary btn-sm">小号按钮</button>
  </div>

  <div class="btn-row">
    <button class="btn-primary" disabled>禁用状态</button>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  • .btn-base() 提供盒模型与过渡;.btn-variant(@bg, @border, @color)darken 生成 hover/active/focus 整套状态色。
  • .btn-size(@padding, @font-size) + .btn-lg / .btn-sm 修饰符:基础类 + 尺寸类叠加(类 BEM)。

Mixin 分层

Mixin 职责
btn-base 布局、圆角、cursor
btn-variant 颜色与交互态
btn-size 内边距、字号

注意点

  • Warning 等浅色底需传 @color: #333,默认 #fff 在浅黄底上对比度不足。
  • 与 Bootstrap button-variant() 同源思路,适合组件库统一出口。

实战场景

  • 后台 Primary/Danger/Warning 按钮族;改 @brand-primary 即可换肤。

真实网站: Bootstrap 4/5 的按钮系统正是这种混合模式------button-variant() 混合接收颜色参数,自动生成完整的按钮状态样式。


7.4 表单组件系统

场景: 电商、后台管理等项目中,表单是最高频的 UI 元素。通过 LESS 混合统一定义输入框、选择框、单选/复选框的焦点色、错误色、禁用色,修改一个变量即可全局同步。

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 表单组件系统</title>
  <style type="text/less">

    /* ── 表单主题变量 ── */
    @input-border:        #d9d9d9;
    @input-focus-border:  #1a73e8;
    @input-error-border:  #ea4a36;
    @input-success-border:#34a853;
    @input-disabled-bg:   #f5f5f5;
    @input-disabled-color:#bbb;
    @input-height:        36px;
    @input-radius:        4px;
    @label-color:         #333;
    @helper-color:        #999;
    @error-color:         #ea4a36;

    /* ── 基础输入框混合 ── */
    .input-base() {
      display:     block;
      width:       100%;
      height:      @input-height;
      padding:     0 12px;
      font-size:   14px;
      line-height: @input-height;
      color:       #333;
      background:  #fff;
      border:      1px solid @input-border;
      border-radius: @input-radius;
      outline:     none;
      transition:  border-color .2s, box-shadow .2s;
      box-sizing:  border-box;

      &::placeholder { color: #bbb; }

      &:focus {
        border-color: @input-focus-border;
        box-shadow:   0 0 0 3px fade(@input-focus-border, 15%);
      }

      &:disabled {
        background: @input-disabled-bg;
        color:      @input-disabled-color;
        cursor:     not-allowed;
      }
    }

    /* ── 状态变体混合 ── */
    .input-state(@border-color, @shadow-color) {
      border-color: @border-color;
      &:focus {
        border-color: @border-color;
        box-shadow:   0 0 0 3px fade(@shadow-color, 15%);
      }
    }

    /* ── 表单组容器 ── */
    .form-group {
      margin-bottom: 20px;

      label {
        display:     block;
        font-size:   14px;
        color:       @label-color;
        font-weight: 500;
        margin-bottom: 6px;

        .required {
          color:      @error-color;
          margin-left: 2px;
        }
      }

      .helper-text {
        font-size:  12px;
        color:      @helper-color;
        margin-top: 4px;
      }

      .error-text {
        font-size:  12px;
        color:      @error-color;
        margin-top: 4px;

        &::before {
          content: "⚠ ";
        }
      }
    }

    /* ── 各种输入控件 ── */
    .form-input    { .input-base(); }
    .form-textarea {
      .input-base();
      height:      auto;
      min-height:  100px;
      padding:     10px 12px;
      line-height: 1.6;
      resize:      vertical;
    }
    .form-select {
      .input-base();
      cursor:      pointer;
      appearance:  none;
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M6 8L0 0h12z' fill='%23999'/%3E%3C/svg%3E");
      background-repeat:   no-repeat;
      background-position: right 12px center;
      padding-right: 32px;
    }

    /* ── 状态类 ── */
    .form-input.is-error    { .input-state(@input-error-border,   @input-error-border); }
    .form-input.is-success  { .input-state(@input-success-border, @input-success-border); }

    /* ── 自定义复选框 ── */
    .form-checkbox {
      display:     flex;
      align-items: center;
      cursor:      pointer;
      gap:         8px;
      font-size:   14px;
      color:       @label-color;
      user-select: none;

      input[type="checkbox"] {
        width:         16px;
        height:        16px;
        accent-color:  @input-focus-border;
        cursor:        pointer;
        flex-shrink:   0;
      }
    }

    /* ── 表单布局 ── */
    .form-row {
      display: flex;
      gap: 16px;

      .form-group { flex: 1; }
    }

    /* ── 提交按钮区 ── */
    .form-actions {
      display:     flex;
      gap:         12px;
      margin-top:  8px;

      button {
        padding:       9px 24px;
        border-radius: @input-radius;
        font-size:     14px;
        cursor:        pointer;
        border:        1px solid transparent;
        transition:    all .2s;
      }

      .btn-submit {
        background:  @input-focus-border;
        color:       #fff;
        &:hover { background: darken(@input-focus-border, 10%); }
      }

      .btn-reset {
        background:   #fff;
        color:        #666;
        border-color: @input-border;
        &:hover {
          border-color: @input-focus-border;
          color:        @input-focus-border;
        }
      }
    }

    /* ── 整体页面 ── */
    body { padding: 32px; font-family: "Microsoft YaHei", sans-serif; background: #f0f2f5; }
    .form-card {
      max-width: 560px;
      background: #fff;
      border-radius: 8px;
      padding: 32px;
      box-shadow: 0 2px 12px rgba(0,0,0,.08);

      h2 {
        font-size: 18px;
        color: #333;
        margin-bottom: 24px;
        padding-bottom: 16px;
        border-bottom: 1px solid #f0f0f0;
      }
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="form-card">
    <h2>用户注册</h2>

    <div class="form-row">
      <div class="form-group">
        <label>姓名 <span class="required">*</span></label>
        <input class="form-input" type="text" placeholder="请输入真实姓名">
      </div>
      <div class="form-group">
        <label>手机号 <span class="required">*</span></label>
        <input class="form-input is-error" type="tel" placeholder="请输入手机号" value="138abc">
        <div class="error-text">手机号格式不正确</div>
      </div>
    </div>

    <div class="form-group">
      <label>邮箱</label>
      <input class="form-input is-success" type="email" value="user@example.com">
      <div class="helper-text">注册成功后将发送验证邮件</div>
    </div>

    <div class="form-group">
      <label>城市</label>
      <select class="form-select">
        <option>请选择所在城市</option>
        <option>北京</option>
        <option>上海</option>
        <option>广州</option>
      </select>
    </div>

    <div class="form-group">
      <label>个人简介</label>
      <textarea class="form-textarea" placeholder="请简单介绍自己..."></textarea>
    </div>

    <div class="form-group">
      <label class="form-checkbox">
        <input type="checkbox" checked>
        已阅读并同意用户协议
      </label>
    </div>

    <div class="form-actions">
      <button class="btn-submit">注 册</button>
      <button class="btn-reset">重 置</button>
    </div>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  • .input-base() 统一边框、圆角、过渡;:focusfade(@input-focus-border, 15%) 做与边框同源的 focus ring。
  • .input-state(@border, @shadow) 驱动 .is-error / .is-success,错误/成功态复用同一套焦点逻辑。
  • .form-textarea 调用 .input-base() 后只覆盖 heightresize 等差异项。

变量驱动

变量 影响范围
@input-focus-border 所有输入框焦点环
@input-error-color .is-error 边框与阴影

注意点

  • fade 保证光晕色相与边框一致,避免手写 rgba 色差。
  • 禁用态、只读态可再扩展 mixin 参数或修饰类。

实战场景

  • B 端表单、登录页;品牌色变更只改变量文件。

真实网站场景: 阿里云控制台、飞书的表单系统均使用类似的变量驱动方案,焦点色、错误色与品牌主题色保持一致。


7.5 导航菜单与下拉菜单

场景: 水平主导航 + 二级下拉菜单是门户网站、电商网站最核心的交互组件,用 LESS 混合封装后可以复用于多个页面。

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 导航菜单系统</title>
  <style type="text/less">

    /* ── 导航变量 ── */
    @nav-bg:           #2c3e50;
    @nav-hover-bg:     darken(@nav-bg, 8%);
    @nav-active-bg:    #1a73e8;
    @nav-text:         rgba(255,255,255,.85);
    @nav-text-hover:   #fff;
    @nav-height:       56px;
    @dropdown-bg:      #fff;
    @dropdown-border:  #e8e8e8;
    @dropdown-shadow:  0 4px 16px rgba(0,0,0,.12);
    @dropdown-hover:   #f0f8ff;

    /* ── CSS 三角形混合(课堂案例复用) ── */
    .triangle(@size, @color, @dir) when (@dir = down) {
      width: 0; height: 0;
      border-style: solid;
      border-width: @size;
      border-color: @color transparent transparent transparent;
    }
    .triangle(@size, @color, @dir) when (@dir = up) {
      width: 0; height: 0;
      border-style: solid;
      border-width: @size;
      border-color: transparent transparent @color transparent;
    }

    /* ── 全局重置 ── */
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: "Microsoft YaHei", sans-serif; font-size: 14px; }
    ul { list-style: none; }
    a  { text-decoration: none; color: inherit; }

    /* ── 主导航容器 ── */
    .main-nav {
      background: @nav-bg;
      height:     @nav-height;
      position:   relative;
      z-index:    1000;

      .nav-inner {
        max-width: 1200px;
        margin:    0 auto;
        display:   flex;
        align-items: center;
        height:    100%;
      }

      /* 一级菜单项 */
      > .nav-inner > ul {
        display:  flex;
        height:   100%;

        > li {
          position:    relative;
          height:      100%;
          display:     flex;
          align-items: center;

          > a {
            display:     flex;
            align-items: center;
            gap:         6px;
            height:      100%;
            padding:     0 20px;
            color:       @nav-text;
            font-size:   14px;
            transition:  background .2s, color .2s;
            white-space: nowrap;

            &:hover {
              background: @nav-hover-bg;
              color:      @nav-text-hover;
            }

            /* 下拉箭头(使用三角形混合) */
            .arrow {
              .triangle(4px, @nav-text, down);
              display:    inline-block;
              transition: transform .2s;
              margin-top: 2px;
            }
          }

          /* 激活状态 */
          &.active > a {
            background: @nav-active-bg;
            color:      #fff;
          }

          /* 悬停时展开下拉,箭头翻转 */
          &:hover > a .arrow {
            .triangle(4px, @nav-text-hover, up);
          }

          /* 下拉菜单 */
          .dropdown {
            position:   absolute;
            top:        100%;
            left:       0;
            min-width:  180px;
            background: @dropdown-bg;
            border:     1px solid @dropdown-border;
            border-top: 2px solid @nav-active-bg;
            border-radius: 0 0 4px 4px;
            box-shadow: @dropdown-shadow;
            opacity:    0;
            visibility: hidden;
            transform:  translateY(-6px);
            transition: opacity .2s, transform .2s, visibility .2s;

            li a {
              display:     block;
              padding:     10px 16px;
              color:       #333;
              font-size:   13px;
              transition:  background .15s;
              border-bottom: 1px solid @dropdown-border;

              &:hover {
                background: @dropdown-hover;
                color:      @nav-active-bg;
                padding-left: 20px;
              }

              &:last-child { border-bottom: none; }
            }
          }

          /* 悬停展开下拉 */
          &:hover .dropdown {
            opacity:    1;
            visibility: visible;
            transform:  translateY(0);
          }
        }
      }
    }

    /* ── 演示内容区 ── */
    .demo-content {
      padding: 40px;
      text-align: center;
      color: #999;
      font-size: 16px;
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <nav class="main-nav">
    <div class="nav-inner">
      <ul>
        <li class="active"><a href="#">首页</a></li>
        <li>
          <a href="#">商品分类 <span class="arrow"></span></a>
          <ul class="dropdown">
            <li><a href="#">数码电器</a></li>
            <li><a href="#">服装鞋包</a></li>
            <li><a href="#">食品生鲜</a></li>
            <li><a href="#">家居家装</a></li>
            <li><a href="#">运动户外</a></li>
          </ul>
        </li>
        <li>
          <a href="#">品牌专区 <span class="arrow"></span></a>
          <ul class="dropdown">
            <li><a href="#">国内品牌</a></li>
            <li><a href="#">进口品牌</a></li>
            <li><a href="#">新锐品牌</a></li>
          </ul>
        </li>
        <li><a href="#">限时特卖</a></li>
        <li><a href="#">积分商城</a></li>
        <li><a href="#">企业采购</a></li>
      </ul>
    </div>
  </nav>

  <div class="demo-content">悬停菜单项查看下拉效果</div>
</body>
</html>

【代码注释】

核心逻辑

  • 下拉显隐:opacity + visibility + transform 联动过渡,避免 display 切换无法动画的问题。
  • 父级 li:hover 展开 .dropdown-panel.triangle(down/up) Guards 控制箭头方向翻转。
  • translateY(-6px) → 0 微位移增强「落下」感;顶栏 border-top@nav-active-bg 强调品牌色。

关键 API

  • §4.4 .triangle() 复用;嵌套 & 绑定 li:hover .panel
  • 主题变量统一 @nav-bg@nav-active-bg

注意点

  • 需处理键盘焦点与移动端点击(本示例为 PC hover 教学)。
  • 多级导航注意 z-index 与面板 position: absolute

实战场景

  • 京东/天猫顶栏「全部商品分类」、频道 mega menu。

真实网站场景: 京东、天猫、苏宁易购的顶部导航菜单均采用此悬停下拉模式,导航背景色、下拉高亮色均通过主题变量统一管理。


7.6 卡片组件与骨架屏

场景: 商品列表、文章列表等场景下,卡片组件是最核心的展示单元;骨架屏则在数据加载时替代卡片,提升用户感知性能。

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 卡片组件与骨架屏</title>
  <style type="text/less">

    /* ── 卡片变量 ── */
    @card-radius:   8px;
    @card-shadow:   0 2px 12px rgba(0,0,0,.08);
    @card-shadow-hover: 0 8px 24px rgba(0,0,0,.15);
    @card-bg:       #fff;
    @tag-radius:    3px;
    @skeleton-color: #e8e8e8;
    @skeleton-highlight: #f5f5f5;

    /* ── 混合库 ── */
    .ellipsis() {
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }

    .clamp(@lines) {
      display:            -webkit-box;
      -webkit-line-clamp: @lines;
      -webkit-box-orient: vertical;
      overflow:           hidden;
    }

    .card-base() {
      background:    @card-bg;
      border-radius: @card-radius;
      box-shadow:    @card-shadow;
      overflow:      hidden;
      transition:    box-shadow .3s, transform .3s;

      &:hover {
        box-shadow: @card-shadow-hover;
        transform:  translateY(-4px);
      }
    }

    /* ── 全局 ── */
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { background: #f0f2f5; font-family: "Microsoft YaHei", sans-serif; padding: 24px; }
    a { text-decoration: none; color: inherit; }

    /* ── 商品卡片 ── */
    .product-grid {
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      gap: 16px;
      max-width: 1000px;
    }

    .product-card {
      .card-base();
      cursor: pointer;

      .card-img {
        width:    100%;
        height:   200px;
        object-fit: cover;
        background: #f0f0f0;
        display:  flex;
        align-items: center;
        justify-content: center;
        color:    #ccc;
        font-size: 40px;
        position: relative;

        .badge-tag {
          position:     absolute;
          top:          10px;
          left:         10px;
          background:   #ea4a36;
          color:        #fff;
          font-size:    11px;
          padding:      2px 6px;
          border-radius: @tag-radius;
        }
      }

      .card-body {
        padding: 12px;

        .card-title {
          font-size:   14px;
          color:       #333;
          line-height: 1.5;
          margin-bottom: 8px;
          .clamp(2);
        }

        .card-price {
          display:     flex;
          align-items: baseline;
          gap:         6px;
          margin-bottom: 8px;

          .price-now {
            font-size:   18px;
            font-weight: bold;
            color:       #ea4a36;

            &::before { content: "¥"; font-size: 13px; }
          }

          .price-origin {
            font-size:   12px;
            color:       #999;
            text-decoration: line-through;

            &::before { content: "¥"; }
          }
        }

        .card-meta {
          display:     flex;
          justify-content: space-between;
          align-items: center;
          font-size:   12px;
          color:       #999;
        }
      }
    }

    /* ── 骨架屏动画 ── */
    @keyframes skeleton-shimmer {
      0%   { background-position: -400px 0; }
      100% { background-position: 400px 0; }
    }

    .skeleton-block() {
      background: linear-gradient(
        90deg,
        @skeleton-color 25%,
        @skeleton-highlight 50%,
        @skeleton-color 75% );
      background-size: 800px 100%;
      animation: skeleton-shimmer 1.5s infinite linear;
      border-radius: 4px;
    }

    .skeleton-card {
      .card-base();

      .skeleton-img {
        .skeleton-block();
        width:  100%;
        height: 200px;
        border-radius: 0;
      }

      .skeleton-body {
        padding: 12px;

        .skeleton-line {
          .skeleton-block();
          height: 14px;
          margin-bottom: 8px;

          &.line-full  { width: 100%; }
          &.line-3-4   { width: 75%; }
          &.line-half  { width: 50%; }
          &.line-1-3   { width: 33%; }
        }

        .skeleton-price {
          .skeleton-block();
          width:  60px;
          height: 20px;
          margin-bottom: 8px;
        }
      }
    }

    /* ── 切换演示 ── */
    .section-title {
      font-size: 16px;
      color: #333;
      font-weight: 500;
      margin: 0 0 16px;
    }
    .section { margin-bottom: 32px; }

  </style>
  <script src="./less.js"></script>
</head>
<body>

  <!-- 骨架屏(加载中状态) -->
  <div class="section">
    <p class="section-title">骨架屏(数据加载中...)</p>
    <div class="product-grid">
      <div class="skeleton-card">
        <div class="skeleton-img"></div>
        <div class="skeleton-body">
          <div class="skeleton-line line-full"></div>
          <div class="skeleton-line line-3-4"></div>
          <div class="skeleton-price"></div>
          <div class="skeleton-line line-half"></div>
        </div>
      </div>
      <div class="skeleton-card">
        <div class="skeleton-img"></div>
        <div class="skeleton-body">
          <div class="skeleton-line line-full"></div>
          <div class="skeleton-line line-3-4"></div>
          <div class="skeleton-price"></div>
          <div class="skeleton-line line-half"></div>
        </div>
      </div>
      <div class="skeleton-card">
        <div class="skeleton-img"></div>
        <div class="skeleton-body">
          <div class="skeleton-line line-full"></div>
          <div class="skeleton-line line-3-4"></div>
          <div class="skeleton-price"></div>
          <div class="skeleton-line line-half"></div>
        </div>
      </div>
      <div class="skeleton-card">
        <div class="skeleton-img"></div>
        <div class="skeleton-body">
          <div class="skeleton-line line-full"></div>
          <div class="skeleton-line line-3-4"></div>
          <div class="skeleton-price"></div>
          <div class="skeleton-line line-half"></div>
        </div>
      </div>
    </div>
  </div>

  <!-- 实际内容(数据加载完成) -->
  <div class="section">
    <p class="section-title">商品列表(数据加载完成)</p>
    <div class="product-grid">
      <div class="product-card">
        <div class="card-img">
          🎧
          <span class="badge-tag">热卖</span>
        </div>
        <div class="card-body">
          <div class="card-title">头戴式无线蓝牙降噪耳机,旗舰级音质,超长续航</div>
          <div class="card-price">
            <span class="price-now">299</span>
            <span class="price-origin">599</span>
          </div>
          <div class="card-meta">
            <span>已售 12,680 件</span>
            <span>⭐ 4.9</span>
          </div>
        </div>
      </div>
      <div class="product-card">
        <div class="card-img">📱</div>
        <div class="card-body">
          <div class="card-title">智能手机 6.7英寸大屏 5000mAh电池</div>
          <div class="card-price">
            <span class="price-now">1,299</span>
            <span class="price-origin">1,999</span>
          </div>
          <div class="card-meta">
            <span>已售 5,430 件</span>
            <span>⭐ 4.8</span>
          </div>
        </div>
      </div>
      <div class="product-card">
        <div class="card-img">
          💻
          <span class="badge-tag">NEW</span>
        </div>
        <div class="card-body">
          <div class="card-title">轻薄笔记本电脑 16GB内存 512GB固态</div>
          <div class="card-price">
            <span class="price-now">4,299</span>
            <span class="price-origin">5,499</span>
          </div>
          <div class="card-meta">
            <span>已售 2,100 件</span>
            <span>⭐ 4.7</span>
          </div>
        </div>
      </div>
      <div class="product-card">
        <div class="card-img">⌨️</div>
        <div class="card-body">
          <div class="card-title">机械键盘 87键 青轴 RGB背光</div>
          <div class="card-price">
            <span class="price-now">359</span>
            <span class="price-origin">499</span>
          </div>
          <div class="card-meta">
            <span>已售 8,900 件</span>
            <span>⭐ 4.9</span>
          </div>
        </div>
      </div>
    </div>
  </div>

</body>
</html>

【代码注释】

核心逻辑

  • .skeleton-block():线性渐变底色 + @keyframes skeleton-shine 流光扫描,图片/标题/价格区统一调用。
  • .clamp(@lines)-webkit-line-clamp 参数化多行省略,标题 2 行、描述 3 行各传参即可。
  • @skeleton-color / @skeleton-highlight 控制明暗主题骨架色。

注意点

  • 骨架块尺寸应尽量与真实卡片 DOM 结构一致,减少布局跳动(CLS)。
  • line-clamp 需配合 display: -webkit-box-webkit-box-orient: vertical

实战场景

  • 列表首屏加载、商品卡片占位;暗色模式改两变量即可。

真实网站场景: 淘宝、抖音、知乎在列表页均使用骨架屏替代 loading 动画,配合 LESS/SCSS 变量驱动,保证骨架屏样式与实际卡片布局严格对应。


7.7 提示工具(Tooltip)与气泡确认框

场景: 图标按钮说明、表格操作列提示、表单字段帮助文字,Tooltip 是高频交互组件,通过 LESS 混合生成四方向版本。

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS Tooltip 工具提示</title>
  <style type="text/less">

    /* ── Tooltip 变量 ── */
    @tip-bg:       #333;
    @tip-color:    #fff;
    @tip-radius:   4px;
    @tip-size:     5px;    /* 三角箭头尺寸 */
    @tip-font:     12px;
    @tip-padding:  6px 10px;
    @tip-delay:    .1s;

    /* ── 三角形混合(课堂案例扩展) ── */
    .arrow-down(@size, @color) {
      .triangle(@size, @color, down);
    }
    .arrow-up(@size, @color) {
      .triangle(@size, @color, up);
    }

    .triangle(@size, @color, @dir) when (@dir = down) {
      content: "";
      position: absolute;
      width: 0; height: 0;
      border-style: solid;
      border-width: @size;
      border-color: @color transparent transparent transparent;
    }
    .triangle(@size, @color, @dir) when (@dir = up) {
      content: "";
      position: absolute;
      width: 0; height: 0;
      border-style: solid;
      border-width: @size;
      border-color: transparent transparent @color transparent;
    }
    .triangle(@size, @color, @dir) when (@dir = left) {
      content: "";
      position: absolute;
      width: 0; height: 0;
      border-style: solid;
      border-width: @size;
      border-color: transparent @color transparent transparent;
    }
    .triangle(@size, @color, @dir) when (@dir = right) {
      content: "";
      position: absolute;
      width: 0; height: 0;
      border-style: solid;
      border-width: @size;
      border-color: transparent transparent transparent @color;
    }

    /* ── Tooltip 核心混合 ── */
    .tooltip-base() {
      position:     absolute;
      background:   @tip-bg;
      color:        @tip-color;
      font-size:    @tip-font;
      padding:      @tip-padding;
      border-radius: @tip-radius;
      white-space:  nowrap;
      pointer-events: none;
      opacity:      0;
      transition:   opacity @tip-delay, transform @tip-delay;
      z-index:      9999;
    }

    /* ── 四方向 Tooltip ── */
    [data-tip] {
      position: relative;
      display:  inline-block;

      /* Tooltip 文字气泡 */
      &::before {
        content:  attr(data-tip);   /* 直接读取 data-tip 属性值 */
        .tooltip-base();
      }

      /* 箭头 */
      &::after { position: absolute; }

      /* 悬停显示 */
      &:hover::before,
      &:hover::after {
        opacity: 1;
      }
    }

    /* 上方 Tooltip(默认) */
    [data-tip] {
      &::before {
        bottom: calc(100% + @tip-size + 4px);
        left:   50%;
        transform: translateX(-50%) translateY(4px);
      }

      &::after {
        .triangle(@tip-size, @tip-bg, down);
        bottom: calc(100% + 4px);
        left:   50%;
        transform: translateX(-50%) translateY(4px);
      }

      &:hover::before { transform: translateX(-50%) translateY(0); }
      &:hover::after  { transform: translateX(-50%) translateY(0); }
    }

    /* 下方 Tooltip */
    [data-tip-dir="bottom"] {
      &::before {
        top:    calc(100% + @tip-size + 4px);
        bottom: auto;
        left:   50%;
        transform: translateX(-50%) translateY(-4px);
      }

      &::after {
        .triangle(@tip-size, @tip-bg, up);
        top:    calc(100% + 4px);
        bottom: auto;
        left:   50%;
        transform: translateX(-50%) translateY(-4px);
      }

      &:hover::before { transform: translateX(-50%) translateY(0); }
      &:hover::after  { transform: translateX(-50%) translateY(0); }
    }

    /* 右方 Tooltip */
    [data-tip-dir="right"] {
      &::before {
        left:   calc(100% + @tip-size + 4px);
        top:    50%;
        bottom: auto;
        transform: translateY(-50%) translateX(-4px);
      }

      &::after {
        .triangle(@tip-size, @tip-bg, right);
        left:   calc(100% + 4px);
        top:    50%;
        bottom: auto;
        transform: translateY(-50%) translateX(-4px);
      }

      &:hover::before { transform: translateY(-50%) translateX(0); }
      &:hover::after  { transform: translateY(-50%) translateX(0); }
    }

    /* ── 演示布局 ── */
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: "Microsoft YaHei", sans-serif; padding: 80px 40px; background: #f0f2f5; }

    .demo-row {
      display: flex;
      gap: 48px;
      align-items: center;
      flex-wrap: wrap;
    }

    .tip-btn {
      padding:       10px 20px;
      background:    #1a73e8;
      color:         #fff;
      border:        none;
      border-radius: 4px;
      cursor:        pointer;
      font-size:     14px;
    }

    .icon-btn {
      width:         40px;
      height:        40px;
      border-radius: 50%;
      border:        1px solid #ddd;
      background:    #fff;
      cursor:        pointer;
      font-size:     18px;
      display:       inline-flex;
      align-items:   center;
      justify-content: center;
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="demo-row">
    <!-- 上方提示(默认) -->
    <button class="tip-btn" data-tip="上方提示气泡">悬停我(上)</button>

    <!-- 下方提示 -->
    <button class="tip-btn" data-tip="下方提示气泡" data-tip-dir="bottom">悬停我(下)</button>

    <!-- 右方提示 -->
    <button class="tip-btn" data-tip="右侧提示气泡" data-tip-dir="right">悬停我(右)</button>

    <!-- 图标按钮(常见场景) -->
    <span class="icon-btn" data-tip="删除该记录">🗑️</span>
    <span class="icon-btn" data-tip="编辑信息">✏️</span>
    <span class="icon-btn" data-tip="分享链接" data-tip-dir="bottom">🔗</span>
  </div>
</body>
</html>

【代码注释】

核心逻辑

  • [data-tip] + ::before/::aftercontent: attr(data-tip) 纯 CSS 显示提示文案,无 JS。
  • data-tip-dir 属性选择器区分上下左右;.triangle() 按方向生成箭头。
  • opacity + transform 过渡实现悬停「弹出」微交互。

属性驱动

属性 作用
data-tip 提示文案
data-tip-dir top/right/bottom/left

注意点

  • 纯 CSS Tooltip 无焦点陷阱与 ARIA,生产复杂场景仍用组件库。
  • 箭头颜色需与气泡背景 @tip-bg 一致。

实战场景

  • 图标按钮说明、表格操作列;GitHub / Ant Design Table 图标提示。

真实网站场景: GitHub 仓库页面的按钮图标(Star、Fork、Watch)、Ant Design Table 组件的操作列图标,均大量使用 Tooltip 提供文字说明。


7.8 加载状态与进度条

场景: 文件上传、页面跳转、数据请求等场景需要视觉反馈,用 LESS 混合封装各类加载指示器。

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 加载状态与进度条</title>
  <style type="text/less">

    /* ── 加载变量 ── */
    @primary:       #1a73e8;
    @success:       #34a853;
    @warning:       #fbbc04;
    @danger:        #ea4a36;
    @spinner-size:  40px;
    @bar-height:    8px;
    @bar-radius:    100px;

    /* ── 加载动画关键帧 ── */
    @keyframes spin {
      to { transform: rotate(360deg); }
    }

    @keyframes pulse {
      0%, 100% { opacity: 1; transform: scale(1); }
      50%       { opacity: .4; transform: scale(.8); }
    }

    @keyframes bar-fill {
      0%   { background-position: 40px 0; }
      100% { background-position: 0 0; }
    }

    @keyframes bar-slide {
      0%   { left: -40%; width: 40%; }
      60%  { left: 100%; width: 50%; }
      100% { left: 100%; width: 50%; }
    }

    /* ── Spinner 混合 ── */
    .spinner-base(@size: @spinner-size, @border-width: 3px) {
      display:       inline-block;
      width:         @size;
      height:        @size;
      border-radius: 50%;
      border:        @border-width solid rgba(0,0,0,.1);
      animation:     spin .8s linear infinite;
    }

    /* ── 进度条混合 ── */
    .progress-bar(@color, @percent) {
      width:         100%;
      height:        @bar-height;
      background:    lighten(@color, 38%);
      border-radius: @bar-radius;
      overflow:      hidden;

      .bar-inner {
        width:         @percent;
        height:        100%;
        background:    @color;
        border-radius: @bar-radius;
        transition:    width .4s ease;
        position:      relative;
        overflow:      hidden;

        /* 条纹流光效果 */
        &.animated::after {
          content:    "";
          position:   absolute;
          top:        0; left: 0;
          width:      100%; height: 100%;
          background: repeating-linear-gradient(
            45deg,
            rgba(255,255,255,.15) 0px,
            rgba(255,255,255,.15) 10px,
            transparent 10px,
            transparent 20px
          );
          background-size: 40px 100%;
          animation:  bar-fill .8s linear infinite;
        }
      }
    }

    /* ── 全局 ── */
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: "Microsoft YaHei", sans-serif; padding: 40px; background: #f0f2f5; }

    h3 {
      font-size: 15px;
      color: #333;
      margin: 24px 0 16px;
      font-weight: 500;
    }

    /* ── Spinner 实例 ── */
    .spinner-wrap {
      display: flex;
      align-items: center;
      gap: 24px;
    }

    .spinner-primary {
      .spinner-base(40px, 3px);
      border-top-color: @primary;
    }

    .spinner-success {
      .spinner-base(40px, 3px);
      border-top-color: @success;
    }

    .spinner-danger {
      .spinner-base(40px, 3px);
      border-top-color: @danger;
    }

    .spinner-sm {
      .spinner-base(20px, 2px);
      border-top-color: @primary;
    }

    .spinner-lg {
      .spinner-base(60px, 4px);
      border-top-color: @primary;
    }

    /* ── 点状加载(三点跳动) ── */
    .dots-loader {
      display: flex;
      gap: 6px;
      align-items: center;

      span {
        width:         8px;
        height:        8px;
        border-radius: 50%;
        background:    @primary;
        animation:     pulse 1.4s ease-in-out infinite;

        &:nth-child(2) { animation-delay: .2s; }
        &:nth-child(3) { animation-delay: .4s; }
      }
    }

    /* ── 进度条实例 ── */
    .progress-list {
      display: flex;
      flex-direction: column;
      gap: 16px;
      max-width: 500px;

      .progress-item {
        display: flex;
        flex-direction: column;
        gap: 4px;

        .progress-label {
          display:  flex;
          justify-content: space-between;
          font-size: 13px;
          color: #666;
        }
      }
    }

    .progress-primary { .progress-bar(@primary, 72%); }
    .progress-success { .progress-bar(@success, 95%); }
    .progress-warning { .progress-bar(@warning, 45%); }
    .progress-danger  { .progress-bar(@danger,  20%); }

    /* ── 顶部进度条(页面跳转进度)── */
    .page-progress {
      position:   fixed;
      top:        0;
      left:       0;
      height:     3px;
      background: @primary;
      box-shadow: 0 0 8px @primary;
      z-index:    10000;
      animation:  none;

      /* 不确定进度(indeterminate) */
      &.indeterminate {
        width: 100%;
        overflow: hidden;
        background: lighten(@primary, 35%);

        &::after {
          content:    "";
          position:   absolute;
          top:        0;
          height:     100%;
          background: @primary;
          animation:  bar-slide 2s ease-in-out infinite;
        }
      }
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>

  <!-- 顶部进度条 -->
  <div class="page-progress indeterminate"></div>

  <h3>圆形加载器(Spinner)</h3>
  <div class="spinner-wrap">
    <div class="spinner-sm"></div>
    <div class="spinner-primary"></div>
    <div class="spinner-success"></div>
    <div class="spinner-danger"></div>
    <div class="spinner-lg"></div>
  </div>

  <h3>点状加载动画</h3>
  <div class="dots-loader">
    <span></span><span></span><span></span>
  </div>

  <h3>进度条</h3>
  <div class="progress-list">
    <div class="progress-item">
      <div class="progress-label"><span>编程能力</span><span>72%</span></div>
      <div class="progress-primary"><div class="bar-inner animated"></div></div>
    </div>
    <div class="progress-item">
      <div class="progress-label"><span>项目完成</span><span>95%</span></div>
      <div class="progress-success"><div class="bar-inner animated"></div></div>
    </div>
    <div class="progress-item">
      <div class="progress-label"><span>文件上传</span><span>45%</span></div>
      <div class="progress-warning"><div class="bar-inner animated"></div></div>
    </div>
    <div class="progress-item">
      <div class="progress-label"><span>磁盘使用</span><span>20%</span></div>
      <div class="progress-danger"><div class="bar-inner"></div></div>
    </div>
  </div>

</body>
</html>

【代码注释】

核心逻辑

  • .spinner-base(@size, @border-width):旋转圆环尺寸参数化,.spinner-sm/lg 一行调用。
  • .progress-bar(@color, @percent)lighten 自动生成轨道色,填充宽用 @percent 控制。
  • 条纹进度:repeating-linear-gradient + background-position 动画;顶栏不确定进度仿 NProgress 伪元素滑动。

Mixin 职责

Mixin 输出
spinner-base 加载旋转器
progress-bar 确定进度 + 条纹动画
page-progress 全宽顶部 indeterminate

实战场景

  • 上传/提交等待、路由切换顶栏;YouTube、Ant Design Progress 同类实现。

真实网站场景: YouTube 顶部红色进度条(页面跳转时)、微信网页版文件发送进度条、Ant Design Progress 组件均采用类似方案。


7.9 模态框(Modal)弹窗系统

场景: 登录框、确认删除、图片预览,模态框是任何 Web 应用都不可缺少的组件,通过 LESS 变量统一尺寸、圆角、遮罩透明度。

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS 模态框系统</title>
  <style type="text/less">

    /* ── 模态框变量 ── */
    @modal-backdrop:  rgba(0,0,0,.5);
    @modal-bg:        #fff;
    @modal-radius:    8px;
    @modal-shadow:    0 8px 40px rgba(0,0,0,.2);
    @modal-sm-width:  400px;
    @modal-md-width:  560px;
    @modal-lg-width:  800px;
    @modal-padding:   24px;
    @modal-header-border: #f0f0f0;
    @modal-footer-border: #f0f0f0;
    @close-color:     #999;
    @close-hover:     #333;

    /* ── 动画 ── */
    @keyframes modal-in {
      from { opacity: 0; transform: scale(.92) translateY(-10px); }
      to   { opacity: 1; transform: scale(1) translateY(0); }
    }

    @keyframes backdrop-in {
      from { opacity: 0; }
      to   { opacity: 1; }
    }

    /* ── 混合库 ── */
    .flex-between() {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .clearfix() {
      &::after { content: ""; display: block; clear: both; }
    }

    /* ── 全局 ── */
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: "Microsoft YaHei", sans-serif; font-size: 14px; background: #f0f2f5; }

    /* ── 遮罩层 ── */
    .modal-backdrop {
      position:   fixed;
      top: 0; left: 0; right: 0; bottom: 0;
      background: @modal-backdrop;
      z-index:    1000;
      display:    flex;
      align-items: center;
      justify-content: center;
      animation:  backdrop-in .2s ease;
    }

    /* ── 模态框容器基础 ── */
    .modal {
      background:    @modal-bg;
      border-radius: @modal-radius;
      box-shadow:    @modal-shadow;
      width:         @modal-md-width;
      max-width:     calc(100vw - 32px);
      max-height:    calc(100vh - 64px);
      display:       flex;
      flex-direction: column;
      animation:     modal-in .2s ease;

      /* 尺寸变体 */
      &.modal-sm { width: @modal-sm-width; }
      &.modal-lg { width: @modal-lg-width; }

      /* ── 头部 ── */
      .modal-header {
        .flex-between();
        padding:       @modal-padding;
        border-bottom: 1px solid @modal-header-border;
        flex-shrink:   0;

        .modal-title {
          font-size:   16px;
          font-weight: 600;
          color:       #333;
        }

        .modal-close {
          width:         28px;
          height:        28px;
          display:       flex;
          align-items:   center;
          justify-content: center;
          border-radius: 50%;
          cursor:        pointer;
          color:         @close-color;
          font-size:     18px;
          transition:    color .15s, background .15s;
          border:        none;
          background:    transparent;

          &:hover {
            color:      @close-hover;
            background: #f5f5f5;
          }
        }
      }

      /* ── 内容区 ── */
      .modal-body {
        padding:    @modal-padding;
        overflow-y: auto;
        flex:       1;
        line-height: 1.7;
        color:      #555;
        font-size:  14px;
      }

      /* ── 底部操作区 ── */
      .modal-footer {
        .flex-between();
        padding:      @modal-padding;
        border-top:   1px solid @modal-footer-border;
        flex-shrink:  0;
        gap:          12px;

        /* 默认右对齐 */
        justify-content: flex-end;

        button {
          padding:       8px 20px;
          border-radius: 4px;
          font-size:     14px;
          cursor:        pointer;
          border:        1px solid transparent;
          transition:    all .15s;

          &.btn-primary {
            background:   #1a73e8;
            color:        #fff;
            &:hover { background: darken(#1a73e8, 10%); }
          }

          &.btn-default {
            background:   #fff;
            color:        #666;
            border-color: #d9d9d9;
            &:hover {
              border-color: #1a73e8;
              color:        #1a73e8;
            }
          }

          &.btn-danger {
            background:   #ea4a36;
            color:        #fff;
            &:hover { background: darken(#ea4a36, 10%); }
          }
        }
      }
    }

    /* ── 确认框变体 ── */
    .modal-confirm {
      .modal();
      width: @modal-sm-width;

      .modal-body {
        display: flex;
        gap: 16px;
        align-items: flex-start;
        padding: @modal-padding;

        .confirm-icon {
          font-size:  24px;
          flex-shrink: 0;
          line-height: 1;
        }

        .confirm-content {
          .confirm-title {
            font-size:   15px;
            font-weight: 600;
            color:       #333;
            margin-bottom: 8px;
          }
          .confirm-desc { color: #666; font-size: 13px; }
        }
      }
    }

    /* ── 触发按钮 ── */
    .demo {
      padding: 48px;
      display: flex;
      gap: 16px;
      flex-wrap: wrap;
      align-items: center;

      button {
        padding: 10px 20px;
        border-radius: 4px;
        font-size: 14px;
        cursor: pointer;
        border: 1px solid transparent;

        &.open-btn {
          background: #1a73e8;
          color: #fff;
          &:hover { background: darken(#1a73e8, 8%); }
        }

        &.confirm-btn {
          background: #ea4a36;
          color: #fff;
          &:hover { background: darken(#ea4a36, 8%); }
        }
      }
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>

  <div class="demo">
    <button class="open-btn" onclick="document.getElementById('modal1').style.display='flex'">
      打开普通弹窗
    </button>
    <button class="confirm-btn" onclick="document.getElementById('modal2').style.display='flex'">
      打开确认框
    </button>
  </div>

  <!-- 普通模态框 -->
  <div class="modal-backdrop" id="modal1" style="display:none"
    onclick="if(event.target===this)this.style.display='none'">
    <div class="modal">
      <div class="modal-header">
        <span class="modal-title">用户信息编辑</span>
        <button class="modal-close" onclick="document.getElementById('modal1').style.display='none'">×</button>
      </div>
      <div class="modal-body">
        <p>这里是模态框的内容区域,可以放置任意内容,包括表单、图片、列表等。</p>
        <br>
        <p>点击遮罩层或右上角 × 按钮关闭弹窗。模态框支持 sm / md / lg 三种宽度规格,通过添加 `.modal-sm` 或 `.modal-lg` 类切换。</p>
      </div>
      <div class="modal-footer">
        <button class="btn-default" onclick="document.getElementById('modal1').style.display='none'">取 消</button>
        <button class="btn-primary" onclick="document.getElementById('modal1').style.display='none'">确 定</button>
      </div>
    </div>
  </div>

  <!-- 确认框 -->
  <div class="modal-backdrop" id="modal2" style="display:none"
    onclick="if(event.target===this)this.style.display='none'">
    <div class="modal modal-sm">
      <div class="modal-header">
        <span class="modal-title">删除确认</span>
        <button class="modal-close" onclick="document.getElementById('modal2').style.display='none'">×</button>
      </div>
      <div class="modal-body" style="display:flex;gap:16px;align-items:flex-start">
        <span style="font-size:24px;line-height:1">⚠️</span>
        <div>
          <div style="font-size:15px;font-weight:600;color:#333;margin-bottom:8px">确认删除该商品?</div>
          <div style="color:#666;font-size:13px">删除后数据无法恢复,请谨慎操作。</div>
        </div>
      </div>
      <div class="modal-footer">
        <button class="btn-default" onclick="document.getElementById('modal2').style.display='none'">取 消</button>
        <button class="btn-danger" onclick="document.getElementById('modal2').style.display='none'">确认删除</button>
      </div>
    </div>
  </div>

</body>
</html>

【代码注释】

核心逻辑

  • 尺寸变量 @modal-sm/md/lg-width + 修饰类嵌套覆盖 width,单 .modal 骨架复用。
  • modal-in 关键帧:scale + translateY 弹入;遮罩 fade 与内容区 overflow-y: auto 处理长表单。
  • .flex-between() 复用于 header/footer 左右分布(标题↔关闭、取消↔确认)。

结构分层

区域 LESS 要点
.modal-mask 全屏遮罩 + 居中 flex
.modal-body max-height: calc(100vh - 64px) 可滚动
.modal-footer flex-between 按钮组

实战场景

  • 删除确认、编辑弹窗;Ant Design Modal / Arco Modal 同类交互。

真实网站场景: 各类后台管理系统(若依、AntD Pro)、电商网站的商品操作确认、评论删除确认均采用此模态框模式。


7.10 标签页(Tabs)与手风琴(Accordion)

场景: 商品详情页的参数/评论/推荐切换、FAQ 帮助页的折叠问答,是内容组织的两种经典模式。

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS Tabs 与 Accordion</title>
  <style type="text/less">

    /* ── 变量 ── */
    @tab-active-color:  #1a73e8;
    @tab-border:        #e8e8e8;
    @tab-bg-hover:      #f5f5f5;
    @acc-border:        #e8e8e8;
    @acc-header-bg:     #fafafa;
    @acc-active-bg:     #f0f8ff;
    @radius:            6px;

    /* ── 混合 ── */
    .transition(@prop: all, @dur: .2s, @fn: ease) {
      transition: @prop @dur @fn;
    }

    /* ── 全局 ── */
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: "Microsoft YaHei", sans-serif; padding: 32px; background: #f0f2f5; }

    h3 { font-size: 15px; color: #333; font-weight: 500; margin: 0 0 16px; }
    .section { margin-bottom: 40px; }

    /* ════════════════════════════
       标签页(Tabs)
    ════════════════════════════ */

    .tabs {
      background: #fff;
      border-radius: @radius;
      box-shadow: 0 2px 8px rgba(0,0,0,.06);
      overflow: hidden;

      /* ── Tab 头部导航 ── */
      .tab-nav {
        display:     flex;
        border-bottom: 2px solid @tab-border;
        background:  #fff;
        overflow-x:  auto;

        &::-webkit-scrollbar { display: none; }

        .tab-item {
          padding:      14px 24px;
          font-size:    14px;
          color:        #666;
          cursor:       pointer;
          white-space:  nowrap;
          border-bottom: 2px solid transparent;
          margin-bottom: -2px;
          .transition(color);
          user-select:  none;

          &:hover   { color: @tab-active-color; }

          &.active {
            color:        @tab-active-color;
            border-bottom-color: @tab-active-color;
            font-weight:  500;
          }
        }
      }

      /* ── Tab 内容面板 ── */
      .tab-panels {
        .tab-panel {
          display:    none;
          padding:    24px;
          line-height: 1.8;
          color:      #555;
          font-size:  14px;
          animation:  fadeIn .2s ease;

          &.active { display: block; }
        }
      }
    }

    @keyframes fadeIn {
      from { opacity: 0; transform: translateY(4px); }
      to   { opacity: 1; transform: translateY(0); }
    }

    /* ── 卡片式 Tab(变体) ── */
    .tabs-card {
      .tabs();

      .tab-nav {
        background:    #f5f5f5;
        border-bottom: none;
        padding:       8px 8px 0;
        gap:           4px;

        .tab-item {
          border-radius:  @radius @radius 0 0;
          border-bottom:  none;
          margin-bottom:  0;
          background:     #e8e8e8;

          &.active {
            background:   #fff;
            color:        @tab-active-color;
            border-bottom-color: transparent;
          }
        }
      }
    }

    /* ════════════════════════════
       手风琴(Accordion)
    ════════════════════════════ */

    .accordion {
      background:    #fff;
      border:        1px solid @acc-border;
      border-radius: @radius;
      overflow:      hidden;

      .acc-item {
        border-bottom: 1px solid @acc-border;

        &:last-child { border-bottom: none; }

        /* 头部(点击展开/收起) */
        .acc-header {
          display:       flex;
          justify-content: space-between;
          align-items:   center;
          padding:       16px 20px;
          background:    @acc-header-bg;
          cursor:        pointer;
          font-size:     14px;
          color:         #333;
          font-weight:   500;
          .transition(background);
          user-select:   none;

          .acc-icon {
            font-size:  14px;
            color:      #999;
            .transition(transform);
          }

          &:hover { background: darken(@acc-header-bg, 3%); }
        }

        /* 内容区 */
        .acc-body {
          max-height: 0;
          overflow:   hidden;
          .transition(max-height, .3s, ease-in-out);

          .acc-content {
            padding:      16px 20px;
            font-size:    14px;
            color:        #555;
            line-height:  1.8;
            border-top:   1px solid @acc-border;
          }
        }

        /* 展开状态 */
        &.active {
          .acc-header {
            background: @acc-active-bg;
            color:      @tab-active-color;

            .acc-icon { transform: rotate(180deg); }
          }

          .acc-body { max-height: 500px; }
        }
      }
    }

  </style>
  <script src="./less.js"></script>
</head>
<body>

  <!-- 标准线条 Tab -->
  <div class="section">
    <h3>标准线条标签页</h3>
    <div class="tabs" id="tabs1">
      <div class="tab-nav">
        <div class="tab-item active" onclick="switchTab('tabs1',0)">商品详情</div>
        <div class="tab-item" onclick="switchTab('tabs1',1)">规格参数</div>
        <div class="tab-item" onclick="switchTab('tabs1',2)">用户评价 (128)</div>
        <div class="tab-item" onclick="switchTab('tabs1',3)">售后政策</div>
      </div>
      <div class="tab-panels">
        <div class="tab-panel active">这是商品详情内容区域,展示商品的图文详细描述信息。</div>
        <div class="tab-panel">这是规格参数内容,包含重量、尺寸、颜色、材质等技术参数。</div>
        <div class="tab-panel">这是用户评价区域,展示购买后用户的真实使用反馈。</div>
        <div class="tab-panel">7天无理由退换,30天质量问题包退,1年整机保修。</div>
      </div>
    </div>
  </div>

  <!-- 手风琴 -->
  <div class="section">
    <h3>手风琴折叠面板</h3>
    <div class="accordion">
      <div class="acc-item active">
        <div class="acc-header" onclick="toggleAcc(this)">
          如何使用 LESS 变量实现主题切换?
          <span class="acc-icon">▼</span>
        </div>
        <div class="acc-body">
          <div class="acc-content">
            通过在根级别定义颜色变量(如 @primary-color),所有引用该变量的组件都会在重编译时自动更新颜色。Ant Design 3.x 正是通过覆盖 LESS 变量并在构建时重新编译来实现主题定制功能。
          </div>
        </div>
      </div>
      <div class="acc-item">
        <div class="acc-header" onclick="toggleAcc(this)">
          LESS 混合(Mixin)和 CSS 类有什么区别?
          <span class="acc-icon">▼</span>
        </div>
        <div class="acc-body">
          <div class="acc-content">
            带括号的 Mixin(如 .clearfix())不会输出为独立的 CSS 类,只在被调用处展开代码。不带括号的(如 .clearfix)既是 CSS 类也可被调用。带括号的 Mixin 更适合作为纯复用工具,避免输出无用的 CSS 规则。
          </div>
        </div>
      </div>
      <div class="acc-item">
        <div class="acc-header" onclick="toggleAcc(this)">
          为什么 LESS 的除法运算必须加括号?
          <span class="acc-icon">▼</span>
        </div>
        <div class="acc-body">
          <div class="acc-content">
            CSS 中斜杠 / 在 font 属性(如 font: 12px/1.5 sans-serif)中有特殊含义,表示行高分隔。为避免歧义,LESS 规定除法运算必须用括号包裹,如 (100px / 2),才会被编译器识别为数学运算。
          </div>
        </div>
      </div>
      <div class="acc-item">
        <div class="acc-header" onclick="toggleAcc(this)">
          LESS 中如何实现循环生成多个类?
          <span class="acc-icon">▼</span>
        </div>
        <div class="acc-body">
          <div class="acc-content">
            LESS 没有原生循环语法,但可以通过带递归的混合模拟。定义一个调用自身的 Mixin,每次递减计数器,结合 Guards 条件在计数器为 0 时终止递归,从而批量生成 .col-1 ~ .col-12 等系列类。
          </div>
        </div>
      </div>
    </div>
  </div>

  <script>
    function switchTab(tabsId, index) {
      const tabs = document.getElementById(tabsId);
      tabs.querySelectorAll('.tab-item').forEach((el, i) => {
        el.classList.toggle('active', i === index);
      });
      tabs.querySelectorAll('.tab-panel').forEach((el, i) => {
        el.classList.toggle('active', i === index);
      });
    }

    function toggleAcc(header) {
      const item = header.parentElement;
      const isActive = item.classList.contains('active');
      document.querySelectorAll('.acc-item.active').forEach(el => el.classList.remove('active'));
      if (!isActive) item.classList.add('active');
    }
  </script>
</body>
</html>

【代码注释】

核心逻辑

  • 线条 Tab:激活态靠 border-bottom-color + color 绑定 @tab-active-color,改变量全局换色。
  • 手风琴:max-height: 0 → 500px 纯 CSS 高度过渡(足够大的上限值);.transition(@prop, @dur, @fn) 统一动效参数。
  • .tabs-card() 在基础 Tab mixin 上只覆盖圆角/边框,演示差异覆盖而非复制全文。

模式对照

组件 动画技巧
Tab 底边高亮 + 可选 card 皮肤
Accordion max-height 展开
共用 .transition() mixin

实战场景

  • 商品详情多 Tab、帮助中心 FAQ;淘宝详情页、京东帮助中心典型形态。

真实网站场景: 淘宝商品详情页的"宝贝详情/规格参数/评价(xxxx)"切换、京东的帮助中心 FAQ 折叠,均是这两种模式的典型应用。


8 LESS vs SASS/SCSS 对比

渲染错误: Mermaid 渲染失败: Parse error on line 3: ...bgraph LESS A1变量:@var ----------------------^ Expecting 'SEMI', 'NEWLINE', 'SPACE', 'EOF', 'subgraph', 'end', 'acc_title', 'acc_descr', 'acc_descr_multiline_value', 'AMP', 'COLON', 'STYLE', 'LINKSTYLE', 'CLASSDEF', 'CLASS', 'CLICK', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', 'direction_tb', 'direction_bt', 'direction_rl', 'direction_lr', 'direction_td', got 'LINK_ID'

特性 LESS SASS/SCSS
变量前缀 @ $
Mixin 定义 .mixin() @mixin name
Mixin 调用 .mixin() @include name
条件语句 when (Guards) @if / @else if / @else
循环 递归 Mixin / each()(v3.7+) @for @each @while
选择器继承 :extend(.class)(v1.4+) @extend .class
属性合并 + / +_ 标记(v1.5+) 无原生支持
属性引用 $prop 语法(v3.0+)
自定义函数 无(只有内置函数) @function
模块系统 @import + reference/inline/once 等关键字 @use / @forward(Dart Sass)
学习曲线 较低,CSS 友好 较高,需学 @mixin/@include
Node.js 生态 原生支持(less 包) 需要安装 sass / node-sass
典型使用方 Bootstrap 3、Ant Design 早期 Bootstrap 4/5、Material UI

9 工程最佳实践与文件组织

9.1 推荐的项目文件结构

复制代码
styles/
├── base/
│   ├── _reset.less       ← 重置样式
│   ├── _typography.less  ← 排版基础
│   └── _variables.less   ← 全局变量
├── components/
│   ├── _buttons.less     ← 按钮组件
│   ├── _forms.less       ← 表单组件
│   ├── _cards.less       ← 卡片组件
│   └── _nav.less         ← 导航组件
├── layout/
│   ├── _grid.less        ← 栅格系统
│   ├── _header.less      ← 页头
│   └── _footer.less      ← 页脚
├── pages/
│   ├── _home.less        ← 首页专属样式
│   └── _product.less     ← 商品页专属样式
└── main.less             ← 入口:@import 所有模块

9.2 变量命名规范

less 复制代码
/* 语义化命名(推荐) */
@color-primary:   #1a73e8;
@color-danger:    #ea4a36;
@spacing-md:      16px;
@border-radius:   4px;

/* 不推荐:值型命名 */
@blue:   #1a73e8;   /* 语义不明确,换色时变量名就过时了 */
@16px:   16px;      /* 无意义 */

9.3 Mixin 最佳实践

less 复制代码
/* 推荐:带命名空间,避免污染 */
#util {
  .ellipsis() {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .clearfix() {
    &::after { content: ""; display: block; clear: both; }
  }
}

/* 调用 */
.title { #util.ellipsis(); }
.row   { #util.clearfix(); }

9.4 嵌套深度控制

less 复制代码
/* 不推荐:超过三层嵌套 */
.page {
  .section {
    .container {
      .list {
        .item {          /* 嵌套太深 */
          .text { }      /* 编译为极长的选择器 */
        }
      }
    }
  }
}

/* 推荐:最多三层,超出用 BEM 拆分 */
.section { }
.section__list { }
.section__item { }
.section__item-text { }

9.5 Design Token 命名体系

来自 2026 工程实践:用语义化命名代替"值型命名",让变量经得起需求变更。

变量按"类别 → 状态 → 尺寸"三层命名,形成可预测的 Token 体系:

less 复制代码
/* ====== 颜色 Token ====== */
/* 品牌色 */
@color-brand-primary:     #1a73e8;
@color-brand-secondary:   #ea4a36;
/* 语义色 */
@color-success:           #34a853;
@color-warning:           #fbbc04;
@color-danger:            #ea4a36;
@color-info:              #4285f4;
/* 文本色 */
@color-text-primary:      #111;
@color-text-secondary:    #555;
@color-text-muted:        #999;
@color-text-disabled:     #ccc;
/* 背景色 */
@color-bg-base:           #fff;
@color-bg-secondary:      #f5f5f5;
@color-bg-elevated:       #fafafa;

/* ====== 间距 Token(8px 基准网格) ====== */
@space-1:  4px;    /* 极紧凑 */
@space-2:  8px;    /* 紧凑 */
@space-3:  12px;   /* 默认 */
@space-4:  16px;   /* 宽松 */
@space-6:  24px;   /* 大间距 */
@space-8:  32px;   /* 区域分隔 */
@space-12: 48px;   /* 模块分隔 */

/* ====== 圆角 Token ====== */
@radius-sm:   4px;
@radius-md:   8px;
@radius-lg:   12px;
@radius-full: 9999px;

/* ====== 字体 Token ====== */
@font-size-xs:   12px;
@font-size-sm:   13px;
@font-size-base: 14px;
@font-size-md:   16px;
@font-size-lg:   20px;
@font-size-xl:   24px;
@font-size-2xl:  32px;

/* ====== Z-index 层级 Token ====== */
@z-dropdown:  100;
@z-sticky:    200;
@z-modal:     300;
@z-toast:     400;
@z-tooltip:   500;

/* ====== 阴影 Token ====== */
@shadow-sm:  0 1px 3px rgba(0,0,0,.08);
@shadow-md:  0 4px 12px rgba(0,0,0,.12);
@shadow-lg:  0 8px 24px rgba(0,0,0,.16);
@shadow-xl:  0 16px 48px rgba(0,0,0,.20);

命名原则:

  • @color-text-primary 而非 @black------语义稳定,调色不改名
  • @space-4 对应 4 × 4px = 16px,数字即倍数,规律可记
  • @z-modal 而非 @z-300------意图清晰,排查层叠问题无需查表

9.6 LESS 与 CSS 自定义属性混合策略

CSS 自定义属性(CSS Variables)在运行时 可被 JS 动态修改,而 LESS 变量在编译时被解析为字面量。两者有明确的职责分工,混合使用可兼得二者优势:

less 复制代码
/* LESS Token(编译时定值)------做设计系统的"单一真相源" */
@brand-blue: #1a73e8;
@brand-green: #34a853;
@space-4: 16px;
@radius-md: 8px;

/* 将 LESS Token 写入 CSS Variables(供运行时使用) */
:root {
  --color-primary:  @brand-blue;
  --color-success:  @brand-green;
  --space-4:        @space-4;
  --radius-md:      @radius-md;
  /* 暗色主题下通过 JS 覆盖这些变量即可实现主题切换 */
}

/* 组件层:使用 CSS Variables 实现"主题可切换" */
.button {
  background:    var(--color-primary);
  border-radius: var(--radius-md);
  padding:       calc(var(--space-4) * 0.5) var(--space-4);
  color:         #fff;
}

/* 以下代码在 JS 中执行,即可切换到暗色主题------无需重编译 */
/* document.documentElement.style.setProperty('--color-primary', '#0d47a1'); */

分层策略总结:

层级 工具 特点 典型用途
设计 Token LESS @var 编译期常量 颜色、间距、字体的唯一来源
主题变量 CSS --var 运行时可修改 暗色模式、用户个性化主题
组件样式 var(--var) 消费 CSS 变量 按钮、卡片等具体组件
动态覆盖 JS setProperty 实时生效 主题切换、动态配色

完整可运行示例(暗色主题切换):

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>LESS + CSS Variables 主题切换</title>
  <style type="text/less">
    /* LESS Token → CSS Variable */
    @light-bg:   #ffffff;
    @light-text: #111111;
    @dark-bg:    #1c1c1e;
    @dark-text:  #ebebeb;
    @brand:      #1a73e8;
    @radius:     8px;

    :root {
      --bg:     @light-bg;
      --text:   @light-text;
      --brand:  @brand;
      --radius: @radius;
    }

    /* 暗色主题:通过 data-theme 属性切换 */
    [data-theme="dark"] {
      --bg:   @dark-bg;
      --text: @dark-text;
    }

    body {
      background: var(--bg);
      color: var(--text);
      font-family: sans-serif;
      padding: 32px;
      transition: background .3s, color .3s;
    }

    .card {
      background: var(--bg);
      color: var(--text);
      border: 1px solid rgba(128,128,128,.2);
      border-radius: var(--radius);
      padding: 20px;
      max-width: 360px;
      box-shadow: 0 2px 8px rgba(0,0,0,.08);
    }

    .btn-theme {
      background: var(--brand);
      color: #fff;
      border: none;
      border-radius: var(--radius);
      padding: 8px 18px;
      cursor: pointer;
      margin-top: 12px;
    }
  </style>
  <script src="./less.js"></script>
</head>
<body>
  <div class="card">
    <h3>LESS + CSS Variables 主题切换</h3>
    <p>LESS Token 提供设计基准,CSS Variable 在运行时动态切换</p>
    <button class="btn-theme" onclick="toggleTheme()">切换暗色 / 亮色</button>
  </div>
  <script>
    function toggleTheme() {
      const root = document.documentElement;
      root.dataset.theme = root.dataset.theme === 'dark' ? '' : 'dark';
    }
  </script>
</body>
</html>

【代码注释】

核心逻辑

  • LESS 变量在编译期 算出 @light-bg 等,写入 :root { --bg: ... }[data-theme="dark"] { --bg: ... }
  • 业务样式用 var(--bg);运行时 document.documentElement.dataset.theme = 'dark' 即可换肤,无需重新 lessc

双层 Token

时机 示例
@primary (LESS) 构建 生成 --primary
var(--primary) (CSS) 运行 组件背景色

注意点

  • IE 不支持 CSS Variables;仅需现代浏览器时可采用此混合策略(§5.4)。
  • transition: background .3s 避免主题切换闪烁。

实战场景

  • 暗色模式、跟随系统 prefers-color-scheme;与 §7.1 编译期主题互补。

9.7 Vite 项目集成 LESS

Vite 原生支持 LESS,无需额外安装 less-loader,只需安装 less 包本身:

bash 复制代码
# 安装依赖(Vite 本身已集成解析能力)
npm install -D less

vite.config.js 配置:

javascript 复制代码
import { defineConfig } from 'vite';

export default defineConfig({
  css: {
    preprocessorOptions: {
      less: {
        // 全局注入变量/混合文件(无需在每个 .less 文件中手动 @import)
        additionalData: `@import "@/styles/variables.less";
                         @import "@/styles/mixins.less";`,
        // 开启 Source Map(开发环境调试用)
        sourceMap: true,
        // LESS 全局配置项
        globalVars: {
          'brand-color': '#1a73e8',
        },
      },
    },
    // 生成模块化 CSS(防止类名冲突)
    modules: {
      scopeBehaviour: 'local',
    },
  },
});

在 Vue 单文件组件(SFC)中使用:

vue 复制代码
<template>
  <div class="card">
    <h2 class="title">Vite + LESS + Vue</h2>
    <p class="desc">变量与 Mixin 由 vite.config 全局注入</p>
  </div>
</template>

<style lang="less">
/* 无需手动 @import variables.less,vite.config 已全局注入 */
.card {
  background: @color-bg-base;
  border-radius: @radius-md;
  padding: @space-4;

  .title {
    color: @color-brand-primary;
    font-size: @font-size-lg;
  }

  .desc {
    color: @color-text-secondary;
    font-size: @font-size-base;
  }
}
</style>

package.json 构建脚本(可选):

json 复制代码
{
  "scripts": {
    "dev":   "vite",
    "build": "vite build",
    "styles:watch": "lessc src/styles/main.less dist/styles.css --watch"
  }
}

【代码注释】

核心逻辑

  • Vite css.preprocessorOptions.less.additionalData:每个 .less 文件编译前自动注入 @import "@/styles/variables.less",全局变量/mixin 免重复 import。
  • globalVars vs modifyVars:前者优先级高、不易被文件内覆盖;后者追加在末尾,适合 Ant Design 主题覆盖

配置要点

选项 用途
additionalData 全局 variables/mixins
modifyVars 覆盖 UI 库 @primary-color
javascriptEnabled: true 兼容部分旧库内联 JS

注意点

  • LESS 与 CSS Modules 可并存:LESS 管 Token/混合,Modules 管类名作用域。
  • 路径别名 @ 需在 resolve.aliasadditionalData 中一致。

实战场景

  • Vue3 + Vite + Element/Ant Design 自定义主题;Monorepo 共享 packages/styles/variables.less

10 常见问题与避坑指南

10.1 除法必须加括号

less 复制代码
/* ❌ 错误:/ 被解释为 CSS font 属性的分隔符 */
height: 100px / 2;

/* ✓ 正确:括号强制 LESS 进行数学运算 */
height: (100px / 2);

10.2 媒体查询变量转义

less 复制代码
/* ❌ 错误:冒号在 LESS 中是变量赋值符,语法歧义 */
@query: min-width: 768px;

/* ✓ 正确:使用转义字符串 */
@query: ~"min-width: 768px";

/* 使用 */
@media (@query) { .container { width: 750px; } }

10.3 Mixin 有无括号的区别

less 复制代码
/* 带括号:只能作为 mixin,不输出为 CSS 类 */
.hidden-mixin() { display: none; }

/* 不带括号:既是 CSS 类,也可以被 mixin 调用(会输出到 CSS 中) */
.visible-class { display: block; }

/* 调用 */
.item {
  .hidden-mixin();    /* 安全,.hidden-mixin 本身不出现在 CSS 中 */
  .visible-class();   /* .visible-class 的规则会出现在 CSS 中 */
}

10.4 颜色函数参数是百分比非比例

less 复制代码
/* ❌ 错误:参数不是 0-1 的比例 */
background: darken(#900, 0.1);    /* 几乎没有变化 */

/* ✓ 正确:参数是百分比 */
background: darken(#900, 10%);    /* 亮度降低 10% */

10.5 变量懒加载导致的"先用后定义"

less 复制代码
/* LESS 允许这样写------变量会在整个作用域内查找 */
.box {
  color: @color;    /* 虽然在下面定义,但 LESS 懒加载机制允许 */
}
@color: #900;       /* 在使用之后定义 */

10.6 :extend() 无法跨媒体查询生效

:extend() 的作用域被严格限制在同一 @media 块内,不能跨媒体查询进行选择器合并:

less 复制代码
@media screen {
  .base { color: blue; }

  /* ✓ 正确:.inner 与 .base 在同一 @media 块内,extend 有效 */
  .inner { &:extend(.base); }
}

/* ❌ 错误:.outside 在 @media 外部,无法扩展 @media 内的 .base */
.outside { &:extend(.base); }
/* 编译后 .outside 不会继承 .base 的样式 */

解决方案: 将公共基础样式提取到媒体查询外部,或改用 Mixin:

less 复制代码
/* ✓ 将基础样式提取到全局层,再在 @media 内覆盖 */
.base { color: blue; }

@media screen {
  .inner { &:extend(.base); }   /* 继承全局 .base ✓ */
  .base  { font-size: 16px; }   /* 媒体内额外扩展 */
}

10.7 属性合并(+)未加标记导致属性被覆盖

使用 merge 特性时,所有参与合并的声明 都必须携带 ++_ 标记,否则后者直接覆盖前者:

less 复制代码
.shadow-a() { box-shadow: inset 0 1px 3px rgba(0,0,0,.2); }  /* ❌ 没有 + */
.shadow-b() { box-shadow: 0 4px 12px rgba(0,0,0,.15); }       /* ❌ 没有 + */

.card {
  .shadow-a();
  .shadow-b();   /* ❌ 结果:只有 shadow-b,shadow-a 被覆盖 */
}
less 复制代码
/* ✓ 正确:所有参与合并的属性都加 + */
.shadow-a() { box-shadow+: inset 0 1px 3px rgba(0,0,0,.2); }
.shadow-b() { box-shadow+: 0 4px 12px rgba(0,0,0,.15); }

.card {
  .shadow-a();
  .shadow-b();   /* ✓ 结果:两个 shadow 以逗号合并 */
}

10.8 @import (reference) 导入的样式不直接输出

@import (reference) 是 LESS 的高级导入模式,用于从第三方库(如 Bootstrap)中"按需提取"样式,但其行为经常被误解:

less 复制代码
/* ❌ 常见误解:以为导入后样式自动生效 */
@import (reference) "bootstrap/less/bootstrap.less";

/* 此时 Bootstrap 的所有样式都不会出现在输出 CSS 中! */
/* 必须通过 extend 或 mixin 主动"激活"才会输出 */

/* ✓ 正确:通过 :extend 按需引入 Bootstrap 的 .navbar 样式 */
.my-nav {
  &:extend(.navbar all);
}

适用场景: 大型框架(Bootstrap/Semantic UI)引入时,只需其中少数组件的样式,可通过 (reference) + :extend 仅提取需要的部分,大幅减小最终 CSS 体积。


总结 {#总结}

LESS 通过变量混合嵌套运算函数条件 六大核心特性,将编程范式引入 CSS,从根本上解决了原生 CSS 可维护性差、复用性低的问题。在此基础上,:extend()、属性合并(+/+_)、$prop 引用、递归循环等高级特性,进一步提升了 LESS 在大型工程中的表达力。
渲染错误: Mermaid 渲染失败: Parse error on line 8: ... A --> K:extend():选择器继承,减少 CSS 体积 ----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'

核心特性速查手册:

特性 语法 核心价值 适用版本
变量 @name: value 单一真相源,全局一处改 all
混合 .mixin(@arg: default) 样式模板化复用 all
嵌套 { & } 结构清晰,减少选择器重复 all
运算 (100px / 2) 动态计算尺寸 all
条件 Guards when (@n > 0) 分支逻辑 all
导入 @import (reference/inline) 模块化 + 按需提取 v1.4+
选择器继承 &:extend(.class all) 共享规则,减少 CSS 体积 v1.4+
属性合并 + / +_ 多值叠加(阴影、变换) v1.5+
属性引用 $prop 属性值就地复用 v3.0+
列表迭代 each(@list, {...}) 批量生成工具类 v3.7+
变量变量 @@name 动态变量选择 all

在工程实践中,LESS 的最大价值在于与团队约定结合

  1. Design Token 命名体系 :语义化命名(@color-text-primary 而非 @black),Token 不随颜色值变而失效
  2. 三层架构variables.lessmixins.lesscomponents.less,通过 @import 链组装
  3. 嵌套深度控制:严格不超过三层,超出部分用 BEM 命名拆分
  4. LESS + CSS Variables 分层:LESS Token 作编译期快照,CSS Variable 作运行时主题切换
  5. Vite/Webpack 全局注入 :通过 additionalData 全局注入变量和 Mixin,组件无需手动 @import

配合现代构建工具(Webpack/Vite),LESS 能够构建出高度可维护的大型样式代码库------无论是 Bootstrap 3 的 6000 行组件库,还是 Ant Design 的完整设计系统,其背后都遵循着相同的工程哲学:用编程范式管理 CSS,让每一个设计决策只出现一次。


参考文档与延伸阅读:

相关推荐
云水一下1 小时前
HTML5 从入门到精通:实战收官——从零搭建完整静态网站,综合运用所有知识
前端·html5
不总是1 小时前
Windows 系统 Node.js 免安装版(zip)安装与配置教程(2026 最新)
前端·windows·node.js
冬奇Lab2 小时前
每日一个开源项目(第105篇):Twenty - 跳出 Salesforce 的圈套,定义现代开源 CRM
前端·后端·开源
zhangyao9403302 小时前
开发pc端时,表格的高度怎么设置才能铺满页面
前端·javascript·elementui
kjs--3 小时前
浏览器书签执行脚本
前端
之歆3 小时前
Day16_JavaScript 轮播图与事件工程实战(下篇)
服务器·开发语言·前端·javascript·网络·性能优化
沄媪3 小时前
CSRF 跨站请求伪造
前端·ctf·csrf
kyriewen4 小时前
我关掉了Copilot:因为我写的代码出现在了别人的建议里
前端·javascript·ai编程