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,让每一个设计决策只出现一次。


参考文档与延伸阅读:

相关推荐
mldong5 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排6 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60616 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角7 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!7 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰8 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi9 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen10 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马10 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu10 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss