Less 核心原理:一个 CSS 编译器 的完整工作流程
Less 的本质是一个用 JavaScript 编写的、针对 CSS 领域的编译器。它接收你写的 .less 代码,经过一系列编译步骤,最终输出浏览器能直接执行的标准 CSS 代码。
Less 的本质是一个用 JavaScript 编写的、针对 CSS 领域的编译器。它接收你写的 .less 代码,经过一系列编译步骤,最终输出浏览器能直接执行的标准 CSS 代码。
它所有的语法特性(变量、嵌套、混合等),本质上都是在编译阶段执行的语法糖,最终都会被转换成原生 CSS。
一、Less 完整编译流水线(5 个核心阶段)
任何一段 Less 代码,都会经过以下 5 个步骤变成 CSS:
plaintext
Less 源代码 → 词法分析 → 语法分析 → AST 转换 → 代码生成 → CSS 输出
1. 词法分析(Tokenization)
作用:把 Less 代码字符串拆分成一个个有意义的 "单词"(Token)
例子:
less
@primary: #6366f1; .btn { background: @primary; }会被拆分成:
@、primary、:、#6366f1、;、.、btn、{、background、:、@、primary、;、}关键:这一步会识别出 Less 的特殊语法(如
@变量、&父选择器),和普通 CSS 语法区分开
2. 语法分析(Parsing)
作用:把 Token 流转换成抽象语法树(AST)
AST 是一个树形数据结构,完整描述了 Less 代码的结构和逻辑
比如上面的代码会生成这样的 AST 结构:
plaintext
Root ├─ VariableDeclaration: @primary = #6366f1 └─ Ruleset: .btn └─ Declaration: background = @primary
3. AST 转换(Transformation)
这是 Less 最核心的阶段,所有 Less 特有的语法特性,都是在这一步被转换成原生 CSS 的:
- 变量替换:把所有
@primary替换成它的值#6366f1 - 嵌套展开:把嵌套的选择器展开成完整的 CSS 选择器
- 混合展开:把混合(Mixin)的代码复制到调用的地方
- 运算执行:计算所有的数学表达式和颜色函数
- 导入合并:把所有
@import的文件合并成一个 AST
4. 代码生成(Code Generation)
作用:遍历转换后的 AST,生成标准的 CSS 代码字符串
上面的例子最终会生成:
css
.btn { background: #6366f1; }
5. 输出与优化
- 可选步骤:压缩 CSS 代码、添加浏览器前缀、生成 Source Map
- 最终输出一个或多个
.css文件,供浏览器使用
二、Less 核心特性的底层实现原理
1. 变量系统:懒加载 + 作用域提升
这是 Less 最独特、也是最容易踩坑的特性,和 JavaScript 的变量提升机制几乎完全一样。
原理:
- Less 会先扫描整个作用域的所有变量声明,再执行代码
- 变量的最终值,是作用域内最后一次赋值的值
- 变量有函数级作用域,内部作用域的变量会覆盖外部作用域的变量
例子:
less
// 编译前
@color: red;
.box { color: @color; } // 你以为是 red?实际是 blue!
@color: blue;
.container {
@color: green;
.inner { color: @color; } // 这里是 green,内部作用域覆盖外部
}
css
/* 编译后 */
.box { color: blue; }
.container .inner { color: green; }
为什么这么设计?
- 为了和 CSS 的 "后声明覆盖先声明" 的层叠规则保持一致
- 允许你在文件末尾覆盖前面定义的变量,这也是为什么 "在导入 Bootstrap 之前定义变量就能覆盖主题" 的原理
2. 嵌套语法:简单的字符串拼接
嵌套是 Less 最受欢迎的特性,但它的实现原理极其简单:就是把父选择器和子选择器用空格拼接起来。
原理:
- 遇到
{时,把当前选择器压入选择器栈 - 遇到
}时,把当前选择器从栈中弹出 - 每个声明的选择器,都是栈中所有选择器的拼接结果
&符号会被直接替换成当前栈中的选择器字符串
例子:
less
// 编译前
.card {
&__header { padding: 16px; }
&:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
}
css
/* 编译后 */
.card__header { padding: 16px; }
.card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
3. 混合(Mixin):代码复制粘贴
混合本质上就是编译期的代码复制粘贴,没有任何运行时开销。
原理:
- 当 Less 编译器遇到
.mixin()调用时,会找到对应的混合定义 - 把混合内部的所有代码,原封不动地复制到调用的位置
- 如果混合有参数,会先进行参数替换,再复制代码
例子:
less
// 编译前
.btn-base(@bg) {
padding: 8px 20px;
background: @bg;
border-radius: 8px;
}
.btn-primary {
.btn-base(#6366f1);
color: white;
}
css
/* 编译后 */
.btn-primary {
padding: 8px 20px;
background: #6366f1;
border-radius: 8px;
color: white;
}
关键区别:和 CSS 类的区别
- CSS 类是运行时的:多个元素共用同一个类,浏览器只解析一次
- Less 混合是编译期的:每次调用都会复制一份代码,会增加 CSS 文件体积
- 所以:通用样式用 CSS 类,需要参数的样式用混合
4. 运算与函数:编译期计算
Less 中所有的数学运算和颜色函数,都是在编译阶段执行的,不会给浏览器带来任何负担。
例子:
less
// 编译前
@base: 16px;
@primary: #6366f1;
.box {
padding: @base * 2; // 编译时计算:16px * 2 = 32px
border-color: darken(@primary, 10%); // 编译时计算颜色
}
css
/* 编译后 */
.box {
padding: 32px;
border-color: #4f46e5;
}
5. 导入(@import):编译期合并
Less 的 @import 和原生 CSS 的 @import 完全不同:
- 原生 CSS 的
@import是运行时的:浏览器会发起多个 HTTP 请求下载 CSS 文件 - Less 的
@import是编译期的:编译器会把所有导入的文件合并成一个 CSS 文件
这也是为什么用 Less 可以把 CSS 拆分成多个模块,而不会影响性能的原因。
三、Less 的两种运行模式
1. 浏览器端实时编译(开发模式)
原理:浏览器下载
less.js文件,然后在客户端实时编译 Less 代码流程:
- 浏览器下载 HTML 页面
- 发现
<link rel="stylesheet/less">标签,下载 Less 文件 - 执行
less.js,在浏览器中编译 Less 代码成 CSS - 把编译后的 CSS 插入到页面中
优点:开发方便,不需要搭建构建环境
缺点:性能差,编译过程会阻塞页面渲染,不适合生产环境
2. Node.js 预编译(生产模式)
原理:在服务器端或开发机上提前把 Less 代码编译成 CSS 文件
流程:
- 开发者写 Less 代码
- 运行 Less 编译器(命令行、VSCode 插件、构建工具)
- 生成 CSS 文件
- 浏览器直接下载并执行 CSS 文件
优点:性能好,编译过程不影响用户体验
缺点:需要搭建构建环境
四、Less 原理带来的关键启示
Less 没有运行时:所有 Less 特性都在编译阶段执行,最终输出的是纯 CSS
- 这意味着你不能在 Less 中操作 DOM、不能获取浏览器信息、不能响应用户事件
- 这些都需要用 JavaScript 来做
变量是静态替换:Less 变量不是 CSS 变量,编译后就变成了固定的值
- 如果你需要动态修改主题,应该使用 CSS 变量(
--primary-color),而不是 Less 变量
- 如果你需要动态修改主题,应该使用 CSS 变量(
混合会增加代码体积:频繁使用混合会导致 CSS 文件变大
- 对于不需要参数的通用样式,优先使用 CSS 类和
:extend()继承
- 对于不需要参数的通用样式,优先使用 CSS 类和
作用域提升是特性不是 bug:理解了这一点,你就不会再为 "变量值不对" 而困惑
- 这也是为什么 "在导入 Bootstrap 之前定义变量就能覆盖主题" 的根本原因
五、和 SCSS 原理的核心区别
Less 和 SCSS 虽然功能相似,但底层实现有一个关键区别:
- Less 是解释型编译器:边解析边执行,所以支持变量作用域提升
- SCSS 是编译型编译器:先完整解析整个文件,再从上到下执行,所以变量必须先定义后使用
这就是两者变量行为不同的根本原因。
需要我给你画一张Less 编译流程的可视化流程图,或者写一个极简版的 Less 编译器(100 行代码),让你更直观地理解它的工作原理吗?