Webpack迁到Vite第三节:Less 与 CSS 处理
本篇定位 :Less 处理链的整体搬迁------
style-resources-loader→additionalData、~前缀 → Less 编译器插件、品牌色modifyVars覆盖、Less 4.x 的math变化与 CSSurl()路径。每条链路均从 Webpack 原配置出发,对照 Vite 等价方案。
思维导图
Less 与 CSS 处理
├── 1.1 背景:Webpack 的 Less 处理链
│ ├── style-resources-loader(自动注入变量文件)
│ ├── less-loader(编译 Less → CSS)
│ └── modifyVars(覆盖 Less 变量)
├── 1.2 style-resources-loader → additionalData
│ ├── 原配置做了什么
│ │ ├── 要解决什么问题(省去手动 @import)
│ │ ├── 逐行拆解:rule/use/loader/options/patterns/injector
│ │ ├── injector 对比:prepend vs append(谁覆盖谁)
│ │ ├── 完整执行流程(5 步 loader 流水线)
│ │ └── Less 延迟求值原理(末尾变量前面能读到)
│ ├── Vite 的 additionalData 替代方案
│ │ └── 逐行解释 + 跳过 default.less 自身
│ ├── 为什么选择 append 而不是 prepend
│ │ ├── prepend 的问题(@primary-color 被 antd theme 覆盖,需 modifyVars 兜底)
│ │ └── append 的优势(匹配 Webpack injector,@primary-color 自动跟随)
│ └── 为什么 Webpack 不用 additionalData
│ ├── 架构不同(Loader 链 vs 内置预处理)
│ ├── less vs less-loader 分工(编译器 vs 集成层)
│ ├── 为什么 Vite 能省掉 less-loader(自己就是集成层)
│ └── Sass 同理(所有预处理器规律一致)
├── 1.3 ~ 前缀问题
│ ├── ~ 是什么(webpack 约定)
│ ├── Less 不认识 ~
│ ├── 方案一:Vite 层 transform 插件(失败 → 递归拦截不到)
│ ├── 方案二:Less 编译器层 pre-processor(成功)
│ │ ├── 正则逐段拆解(表格 + 对照示例)
│ │ └── 替换回调输入→输出变换
│ └── 还有更优方案吗?
│ ├── 方案 A:less-plugin-npm-import(多一个依赖,维护不活跃)
│ ├── 方案 B:resolve.alias(不可行,Less @import 在编译器飞地内)
│ │ └── 完整处理流程(5 步)→ resolve.alias 生效边界分析
│ ├── 方案 C:Vite 原生支持(理想但不存在)
│ └── 结论:维持当前方案(4 行代码,零依赖,未来可无痛迁移)
├── 1.4 modifyVars 品牌色覆盖
│ ├── 原 webpack 配置(仅 @brand-color)
│ ├── Vite 配置(仅 @brand-color,append 策略使 @primary-color 自动跟随)
│ ├── 为什么 append 下只需 @brand-color(default.less 最后加载,后定义覆盖先定义)
│ └── additionalData 和 modifyVars 的分工
├── 1.5 less 4.x math 变化
│ ├── 问题:除法编译报错
│ ├── 原因:less 4.x 默认 math='parens-division'
│ ├── 为什么 Less 要改默认值(/ 的二义性:除法 vs CSS 简写)
│ └── 修复:math='always'
└── 1.6 CSS url() 中的路径
└── ~@/assets → @/assets
1.1 背景:Webpack 的 Less 处理链
在 Webpack 中,Less 文件经过以下处理链:
.less 文件
→ style-resources-loader(注入公共变量文件)
→ less-loader(编译 Less 为 CSS)
→ css-loader(处理 CSS 中的 url() 等)
→ style-loader 或 MiniCssExtractPlugin(注入 DOM 或提取为文件)
Vite 内置了 Less 支持,但需要手动配置来替代 Webpack 的 loader 功能。
1.2 style-resources-loader → additionalData
原配置做了什么
先看完整配置:
javascript
// vue.config.js
function addStyleResource(rule) {
rule.use('style-resource')
.loader('style-resources-loader')
.options({
patterns: [path.resolve(__dirname, './src/styles/default.less')],
injector: 'append',
})
}
这段代码要解决什么问题?
项目有一个公共变量文件 src/styles/default.less,里面定义了品牌色、主题色等变量:
less
// src/styles/default.less
@brand-color: #FF8300;
@primary-color: @brand-color;
如果每个 .vue 文件或 .less 文件想用 @brand-color,就得手动写 @import '../styles/default.less'。几十上百个文件都写一遍,烦且容易漏。
style-resources-loader 就是帮你自动加这一行的------不需要你手动 import,变量自动出现在每个 Less 文件中。
逐行拆解
① function addStyleResource(rule) --- 什么是 rule?
rule 是 Webpack 配置中的一条处理规则 。在 vue.config.js 中,Vue CLI 允许你通过 chainWebpack 拿到 Webpack 的内部配置对象,然后链式修改它。rule 就是其中一条规则------这里拿到的就是"处理 Less 文件"那条规则。
你可以把它理解为一条流水线:每个 .less 文件进来,都要经过这条流水线上的一连串 loader 处理。
② rule.use('style-resource') --- 在流水线上加一个工位
.use('style-resource') 在这条流水线上新增一个处理工位,命名为 'style-resource'。这个名字是自定义的,叫什么都行('inject-vars'、'auto-import' 都行),它只是一个标识符,方便后续通过名字找到这个工位。
③ .loader('style-resources-loader') --- 指定这个工位由谁干活
.loader() 指定这个工位使用哪个 npm 包来处理文件。这里指定 style-resources-loader,它是一个社区 loader,专门做一件事:把指定的文件内容自动注入到每个被处理的文件中。
④ .options({...}) --- 告诉 loader 具体怎么干活
.options() 是给 loader 传参数。不同 loader 接受不同的参数,这里是 style-resources-loader 的两项配置:
⑤ patterns: [path.resolve(__dirname, './src/styles/default.less')] --- 要注入哪些文件
patterns是一个数组,可以指定多个文件路径。这里只注入一个文件。path.resolve(__dirname, './src/styles/default.less'):__dirname是 Node.js 的全局变量,指向当前文件所在的目录 (即vue.config.js所在的目录,项目根目录)path.resolve()把相对路径拼成绝对路径- 假设项目在
D:\project\,那最终得到的就是D:\project\src\styles\default.less
⑥ injector: 'append' --- 注入到文件的哪个位置
'append':追加到文件末尾- 另一种可选值是
'prepend':插入到文件开头
为什么选 append 而不是 prepend?这涉及到 Less 变量的覆盖规则:
less
// 假设源文件内容:
@brand-color: #000000; // 当前文件想覆盖品牌色
.box { color: @brand-color; }
// injector: 'prepend'(插入到开头):
@brand-color: #FF8300; // ← default.less 的内容被插入到开头
@brand-color: #000000; // ← 源文件自己的定义在后面
.box { color: @brand-color; } // → #000000 ✅ 源文件覆盖成功
// injector: 'append'(追加到末尾):
@brand-color: #000000; // ← 源文件自己的定义在前面
.box { color: @brand-color; }
@brand-color: #FF8300; // ← default.less 的内容被追加到末尾 → #FF8300,源文件覆盖失败!
Less 变量遵循"后定义覆盖先定义"的规则。所以:
prepend:default.less在前,源文件在后 → 源文件可以覆盖默认值append:源文件在前,default.less在后 → 默认值覆盖源文件(通常不是你想要的效果)
这个项目选 append 的原因 :Webpack 的 style-resources-loader 在 injector: 'append' 模式下将变量注入到源文件末尾。Vite 的 additionalData 延续了 append 策略(在源文件末尾注入),原因详见下文「为什么选择 append 而不是 prepend」小节。
完整执行流程
把上面的拆解串起来,当一个 .less 文件被 Webpack 处理时发生了什么:
① Webpack 遇到一个 .less 文件
↓
② 进入"处理 Less 文件"的 loader 流水线
↓
③ 第一个工位:style-resources-loader
读取 src/styles/default.less 的内容
追加到当前文件末尾
↓
源文件内容 + default.less 的内容 → 拼接后的完整文本
↓
④ 第二个工位:less-loader
把拼接后的 Less 文本编译成 CSS
↓
⑤ 后续工位:css-loader → style-loader
处理 CSS 中的 url()、注入到 DOM
一句话总结 :style-resources-loader 做的事情就是"在每个 Less 文件编译前,自动把 default.less 的内容塞进去"------省去了你手动在每个文件里写 @import 的麻烦。
为什么追加到末尾,前面的代码也能读到变量? 这得益于 Less 的**延迟求值(lazy evaluation)**机制------Less 变量在同一作用域内不是按"从上到下"顺序执行的,而是先读完整个文件,再统一解析变量引用。最后定义的值生效:
less
// 文件开头就用了 @brand-color
.header { color: @brand-color; } // → #FF8300 ✅ 能读到!
// 变量定义在文件末尾
@brand-color: #FF8300;
Less 编译器把整个文件作为一个作用域,先收集所有变量定义,再回头解析引用。所以变量定义在末尾还是开头,对"能不能读到"没有影响------只对"同名变量谁覆盖谁"有影响(后定义的覆盖先定义的)。
Vite 的 additionalData 替代方案
javascript
// vite.config.js
css: {
preprocessorOptions: {
less: {
additionalData: (source, filePath) => {
if (filePath.includes('default.less')) return source
return `${source}\n@import "${path.resolve(__dirname, 'src/styles/default.less')}";`
},
},
},
},
逐行解释:
-
additionalData: (source, filePath) => { ... }:一个函数,在每个 Less 文件编译前调用source:文件的原始内容filePath:文件的绝对路径
-
if (filePath.includes('default.less')) return source:跳过default.less自身- 避免
default.less被注入自身(Less 有导入去重,注入无意义) - 这是一个安全守卫
- 避免
-
return ...:在文件内容末尾 追加@import语句@import是 Less 的导入语法,相当于把default.less的内容添加到当前文件- 与 webpack
injector: 'append'行为一致------default.less在最后加载,后定义覆盖先定义 - 这个选择至关重要,直接决定了
modifyVars只需要覆盖@brand-color即可(见下文 modifyVars 小节)
为什么选择 append 而不是 prepend
Vite 社区通常推荐 prepend(插入到开头),因为更符合直觉:"先注入通用变量,源文件可以覆写"。但本项目选择了 append(追加到末尾),原因是与 Webpack 的 injector: 'append' 保持一致 ,且对 modifyVars 的配置有直接影响。
关键差异 :default.less 本质上就是 antd 主题文件的完整副本 + 项目定制值。antd 的每个组件文件自己也会 @import antd 的 theme 文件。两种方式下的编译结果:
prepend:
@import "default.less"; ← ① @primary-color: @brand-color
@import "antd/themes/default"; ← ② @primary-color: #1890FF(覆盖了①)
→ 需要 modifyVars 同时设置 @primary-color 兜底
append(本项目采用):
@import "antd/themes/default"; ← ① @primary-color: #1890FF
@import "default.less"; ← ② @primary-color: @brand-color(覆盖了①)
→ modifyVars 只需设置 @brand-color,@primary-color 自动跟随
选择 append 的理由:
- 项目中没有源文件覆盖
@brand-color的场景,prepend 的"允许覆盖"优势用不上 - append 让
default.less成为"最后说话的人",@primary-color自动跟随@brand-color modifyVars只需要一行'@brand-color': brandColor,逻辑更简洁- 与 Webpack 时期的
injector: 'append'行为一致,减少迁移变量
为什么 Webpack 不用 additionalData?
additionalData 是 Vite 的 API,Webpack 根本没有这个东西。
这不是"Webpack 有但不用",而是两个工具架构不同,各自提供了不同的机制:
Webpack 的哲学:Loader 链
每个文件类型对应一条 loader 流水线,功能通过串联 loader 叠加
Less 文件 → style-resources-loader → less-loader → css-loader → style-loader
想加功能?在流水线上加一个 loader 工位即可
Vite 的哲学:内置预处理 + 配置项
Less/Sass 支持是 Vite 内置的,不需要装 loader
想加功能?通过 css.preprocessorOptions 下的配置项控制
additionalData 就是其中一个配置项
但这里有一个关键问题:为什么 Vite 能省掉 less-loader?less 和 less-loader 各自到底做了什么?
less 和 less-loader 的分工
┌─────────────────────────────────────────────────────────┐
│ less(编译器) │
│ 纯粹的 Less → CSS 转换引擎,与构建工具无关 │
│ │
│ 输入:Less 源码字符串 │
│ 输出:CSS 字符串 │
│ │
│ 核心 API:less.render(source, options, callback) │
│ │
│ 它不知道: │
│ - 文件在哪?文件系统? │
│ - @import 路径怎么解析? │
│ - 处理完后 CSS 交给谁? │
│ - Source map 怎么生成? │
│ - 什么是 Webpack?什么是 Vite? │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ less-loader(集成层) │
│ Webpack 和 less 编译器之间的桥梁,Webpack 专有 │
│ │
│ 它负责: │
│ ① 从 Webpack 接收文件内容 │
│ ② 解析 @import 路径(按 Webpack 的模块解析规则) │
│ ③ 调用 less.render(),传入源码 │
│ ④ 处理 Source map │
│ ⑤ 把编译结果(CSS)交给下一个 loader │
│ ⑥ 把错误转成 Webpack 格式 │
│ │
│ 它不负责: │
│ 实际把 Less 变成 CSS ------ 那是 less 编译器的事 │
└─────────────────────────────────────────────────────────┘
一句话:less 是干活的,less-loader 是跑腿的。 less 只接受字符串输入、输出字符串,它不关心字符串从哪来、到哪去。less-loader 负责把文件内容取出来交给 less,再把结果传下去。
为什么 Vite 能省掉 less-loader?
因为 Vite 自己就是那个跑腿的 。当 Vite 遇到一个 .less 文件时:
Vite 做的事情(替代了 less-loader 的职责):
① 读取 .less 文件内容 ← Vite 的文件系统
② 解析 @import 路径 ← Vite 的模块解析器
③ 调用 less.render(source, opts) ← Vite 直接调 less 编译器
④ 处理 Source map ← Vite 内置
⑤ 把 CSS 结果传给 Vite 的 CSS 管线 ← Vite 内置
Vite 把集成层做进了自己的代码里,所以不需要一个单独的 less-loader 包。但 less 编译器本身(那个能把 Less 语法变成 CSS 的引擎)Vite 没有重写,也重写不了------那是 Less 团队维护的,几百 KB 的解析器代码。
Sass 同理
| Webpack | Vite | |
|---|---|---|
| Less | less + less-loader |
less |
| Sass/SCSS | sass + sass-loader |
sass |
| Stylus | stylus + stylus-loader |
stylus |
规律完全一致:编译器要装,集成层不用。 Vite 把所有预处理器的集成层都内置了,你只需要装编译器本身。如果项目用了 Sass,同样只需要 sass 一个包,不需要 sass-loader。
对比:
| Webpack | Vite | |
|---|---|---|
| 注入变量的方式 | style-resources-loader(第三方 loader) |
additionalData(Vite 内置配置项) |
| 本质 | 在 loader 流水线上加一个工位 | 调用 Less 编译器前,修改传入的源码字符串 |
| 配置位置 | chainWebpack → rule.use().loader().options() |
css.preprocessorOptions.less.additionalData |
| 依赖 | less + less-loader + style-resources-loader(3 个包) |
less(1 个包,Vite 内置了集成层) |
两者做的是同一件事 (在编译前把变量文件内容注入),只是实现机制不同------因为 Webpack 和 Vite 的架构完全不同。
验证效果 :所有 Less 文件(包括 node_modules 中的 antd/vant)都能读取 default.less 变量,antd 组件显示品牌色而非默认蓝色。
1.3 ~ 前缀问题
~ 是什么
~ 是 Webpack 的约定:在 CSS/Less 的 @import 中,~ 表示 node_modules 目录。
less
// webpack 中
@import '~vant/lib/style/normalize';
// 等价于
@import 'node_modules/vant/lib/style/normalize';
Less 不认识 ~
Less 编译器把 ~ 当作普通字符,尝试从当前文件所在目录去找 ~vant/lib/style/normalize,自然找不到:
Error: '~@vant/icons/src/encode-woff2.less' wasn't found.
Tried - node_modules/vant/lib/icon/~@vant/icons/src/encode-woff2.less
方案一:Vite 层 transform 插件(失败)
尝试 :创建一个 Vite 插件,在 transform 钩子中移除 ~ 前缀:
javascript
{
name: 'css-tilde-resolver',
enforce: 'pre',
transform(src, id) {
if (/\.(less|css|sass|scss)(\?|$)/.test(id)) {
return src.replace(/@import\s+['"](~[^'"]+)['"]/g, (_, p) => `@import "${p.replace(/^~/, '')}"`)
}
}
}
为什么失败 :Vite 的 transform 钩子只对入口文件 生效。当 Less 文件 A 通过 @import 引用文件 B 时,文件 B 的解析在 Less 编译器内部完成,Vite 的 transform 钩子拦截不到。
具体场景:
src/components/h5/vant/index.less ← transform 钩子生效 ✅
→ @import 'vant/lib/icon/index.less' ← transform 钩子不生效 ❌
→ @import '~@vant/icons/...' ← Less 编译器直接解析,报错!
方案二:Less 编译器层 pre-processor(成功)
javascript
less: {
plugins: [{
install(_, pluginManager) {
pluginManager.addPreProcessor({
process(src) {
return src.replace(
/@import\s+['"](~[^'"]+)['"]/g,
(_, p) => `@import "${p.replace(/^~/, '')}"`
)
},
})
},
}],
}
逐行解释:
-
plugins: [{ install(_, pluginManager) { ... } }]:注册一个 Less 插件install方法接收 Less 实例和插件管理器
-
pluginManager.addPreProcessor({ process(src) { ... } }):添加一个预处理器- 预处理器在 Less 编译器解析之前运行
- 对所有 Less 文件生效,包括
@import递归引用的文件
-
src.replace(/@import\s+['"](~[^'"]+)['"]/g, ...):正则替换完整的正则:
/@import\s+['"](~[^'"]+)['"]/g逐段拆解:
片段 含义 匹配示例 @import字面匹配 @import@import\s+一个或多个空白字符(空格、tab、换行) 或\t['"]字符类 :匹配 '或"(开引号)'或"(...)捕获组:括号内匹配的内容会被保存,供后续使用 --- ~字面匹配波浪号 ~[^'"]+否定字符类 : [^...]表示"不是这些字符"。[^'"]匹配任意一个不是引号的字符 。+表示一个或多个。所以[^'"]+= "一个或多个非引号字符"@vant/icons['"]匹配 '或"(闭引号)'或"对照一个具体例子:
@import '~@vant/icons/src/encode-woff2.less' ↑ ↑ ['"] 匹配开引号 ['"] 匹配闭引号 └──────────────────────────┘ (~[^'"]+) 捕获组:~@vant/icons/src/encode-woff2.less注意:
[^'"]+不包含引号本身------引号由外面的['"]匹配。捕获组拿到的是引号之间的内容(不含引号)。 -
替换回调
(_, p) => ...:String.replace()的第二个参数是回调函数,对每个匹配到的字符串调用一次。回调签名:
(fullMatch, capture1, capture2, ...) => replacement参数 值(以 @import '~@vant/icons/...'为例)_(第一个参数)整个匹配: @import '~@vant/icons/...'p(第二个参数)第一个捕获组 (~[^'"]+)的内容:~@vant/icons/...回调体做了两件事:
① p.replace(/^~/, '') → 去掉 p 开头的 ~ ~@vant/icons/... → @vant/icons/... ② 模板字符串拼接 → `@import "${结果}"` → @import "@vant/icons/..."最终效果:
@import '~@vant/icons/...'→@import "@vant/icons/..."注意:替换后统一用双引号。如果原文件是单引号
'~...',替换后变成双引号"...",这对 Less 编译器来说完全等价(Less 的@import同时支持单引号和双引号)。
为什么方案二更优:
- 在 Less 编译器层面处理,覆盖所有文件(包括递归 import)
- 不需要关心文件是来自
src/还是node_modules/ - 一次配置,永久生效
还有更优方案吗?
从设计理念上讲,这种自定义正则不应该暴露到外层让用户自己处理 。~ 是 Webpack 生态的约定,从 Webpack 迁到 Vite 的用户都会遇到这个问题,理应由工具链解决。以下是探索过的替代方案:
方案 A:less-plugin-npm-import(Less 社区插件)
bash
npm install less-plugin-npm-import
javascript
less: {
plugins: [new (require('less-plugin-npm-import'))()],
}
Less 社区专门为解决 ~ → node_modules 映射问题开发的插件,一行配置搞定。
优点 :语义清晰,不暴露实现细节
缺点:多一个依赖;该包维护不太活跃,Less 4.x 兼容性需验证。内部实现和一个正则替换没有本质区别,为 4 行逻辑引入一个低活跃度依赖,性价比不高。
方案 B:resolve.alias 配置 ~
resolve.alias是 Vite 的模块路径别名配置(类似 Webpack 的resolve.alias)------将 import 路径中的别名(如@、~)映射到实际文件路径。正式配置说明见第六节 §1.3。
javascript
resolve: {
alias: {
'~': path.resolve(__dirname, 'node_modules'),
},
}
不可行 。要理解为什么,需要先了解 .less 文件在 Vite 中的完整处理流程:
① Vite 读取 .less 文件
↓
② additionalData 注入(追加 default.less)
↓
③ Less 编译器(less.render()) ← 独立王国,resolve.alias 管不着
├── Less pre-processor 插件(正则去 ~)
├── 解析 @import 路径(Less 自己的文件系统查找)
└── 编译 Less → CSS
url('@/assets/...') → Less 不认识 @/,原样透传到 CSS 输出
↓
④ Vite CSS 管线 ← 从这里开始,resolve.alias 才生效
├── 解析 CSS 中的 url() 和 @import
├── apply resolve.alias(@/ → src/ 实际路径)
├── PostCSS(autoprefixer 等)
└── 路径改写(加 content hash)
↓
⑤ 开发模式:注入 <style> 标签 / 生产模式:提取 CSS 文件
关键 :resolve.alias 的生效边界不是"JS/TS vs CSS",而是 "Vite 的模块解析器能管到的地方" 。步骤 ③ 的 Less 编译器有自己独立的文件系统解析器,不经过 Vite 的模块解析器,所以 resolve.alias 在这里不生效。步骤 ④ 的 CSS 管线由 Vite 控制,resolve.alias 才起作用。
Vite 的 resolve.alias 能处理的:
JS: import X from '@/...' ✅ (Vite 模块解析器)
CSS: url('@/...') ✅ (Vite CSS 管线,Less 编译器透传过来的)
HTML: <img src="@/..."> ✅ (Vite HTML 插件)
Vite 的 resolve.alias 不能处理的:
Less: @import '~vant/...' ❌ (Less 编译器内部独立解析,飞地)
所以把 ~ 映射到 node_modules 这条路走不通------~ 出现在 Less 的 @import 中,而 @import 在 Less 编译器的"飞地"里,不归 Vite 管。
方案 C:Vite 原生支持(理想方案,但不存在)
从设计理念上讲,这个活应该 Vite 自己干 。Vite 的 Less 集成层在调用 less.render() 之前,完全可以检测到 ~ 前缀并自动处理------就像 Webpack 的 less-loader 自动处理 ~ 一样。
但现实是 Vite 没有处理。原因可能是 ~ 本质是 Webpack 的"方言",Vite 不认这个方言,期望用户迁移时把 ~ 从代码里去掉。然而 node_modules 里的第三方包(如 vant)用户改不了,这就成了死角。
结论
| 方案 | 依赖 | 语义清晰度 | 维护风险 |
|---|---|---|---|
| 当前:自定义 pre-processor | 零 | ⭐⭐(暴露实现) | 低(4 行代码,逻辑简单) |
A:less-plugin-npm-import |
+1 包 | ⭐⭐⭐(一行配置) | 中(社区维护不活跃) |
B:resolve.alias |
零 | --- | 不可行 |
| C:Vite 原生支持 | 零 | ⭐⭐⭐ | 不存在 |
推荐维持当前方案 。从设计理念上确实应该由 Vite 处理,但这是 Vite 的"未尽之事"。当前方案已在最合适的层级(Less 编译器层)用最少的代码解决了问题,未来如果 Vite 原生支持了 ~ 前缀,删掉这段配置即可,零迁移成本。
验证效果 :'~@vant/icons/...' wasn't found 类报错消失;所有 ~ 前缀的 @import(包括 node_modules 中第三方包内部递归引用的)均能正常解析编译。
1.4 modifyVars 品牌色覆盖
原 Webpack 配置
javascript
// vue.config.js
css: {
loaderOptions: {
less: {
lessOptions: {
modifyVars: { '@brand-color': process.env.brandColor },
},
},
},
},
Vite 配置
javascript
// vite.config.js
less: {
modifyVars: {
'@brand-color': brandColor, // 仅此一行即可,@primary-color 自动跟随
},
},
为什么只需要覆盖 @brand-color?
default.less 中定义了 @primary-color: @brand-color。由于 additionalData 采用 append 策略(追加到末尾),default.less 在所有文件内容之后加载,后定义覆盖先定义:
antd 组件文件编译时的实际内容:
① @import "../../../style/themes/default"; ← antd 自身的 theme,@primary-color: #1890FF
// ... antd 组件样式 ...
② @import "default.less"; ← additionalData 追加到末尾
@brand-color: #FF8300; ← modifyVars 覆盖为 brandColor
@primary-color: @brand-color; ← 引用被覆盖后的 @brand-color → 自动跟随!
Less "后定义覆盖先定义" → ② 覆盖 ① → @primary-color = @brand-color ✅
modifyVars 覆盖 @brand-color → default.less 中 @primary-color: @brand-color 自动跟随 → 无需再单独设置 @primary-color。
对比 Webpack 时期 :Webpack 的 injector: 'append' 也是同样的原理------default.less 最后加载,@primary-color 自动跟随。Vite 选择 append 而非 Vite 社区默认的 prepend,正是为了保持这一行为一致性(详见 [为什么选择 append 而不是 prepend](#为什么选择 append 而不是 prepend))。
additionalData 和 modifyVars 的分工
| 机制 | 作用范围 | 用途 |
|---|---|---|
additionalData(append) |
所有 Less 文件 | 在每个文件末尾追加 @import "default.less",让所有文件都能使用项目变量 |
modifyVars |
整个 Less 编译过程 | 覆盖 @brand-color,使品牌色可通过构建参数 --brand_color 动态配置 |
两者互补,缺一不可:
- 没有
additionalData:任何文件都无法使用项目变量(@brand-color、@primary-color等) - 没有
modifyVars:@brand-color只能是default.less中的硬编码值#FF8300,无法通过构建参数定制
品牌色不生效的实际排查过程见第八节 §1.3。
1.5 Less 4.x math 变化
问题
升级 Less 到 4.x 后,编译报错(14 个错误,全部来自 ant-design-vue 的 Less 文件)。
原因
Less 4.x 改变了 math 选项的默认值:
| 版本 | 默认值 | 行为 |
|---|---|---|
| Less 3.x | always |
所有 / 都被当作除法运算 |
| Less 4.x | parens-division |
只有括号内的 / 才是除法 |
ant-design-vue 的 Less 代码中大量使用了不带括号的除法(如 @padding / 2),在 Less 4.x 默认配置下会报错。
为什么 Less 要改默认值
核心原因是 / 在 CSS 中有两个互斥的身份:
| 身份 | 示例 | 含义 |
|---|---|---|
| 除法运算符 | @width / 2 |
Less 数学运算,编译时求值 |
| CSS 值分隔符 | font: 14px/1.5 |
CSS 简写属性,font-size / line-height 的缩写 |
Less 3.x 默认把所有 / 都当除法------包括 CSS 简写属性中的 /。这会导致:
less
// 用户想写 CSS 简写
body { font: 14px/1.5 Arial; } // 14px/1.5 是 font-size/line-height
// Less 3.x 实际编译结果:
body { font: 9.333px Arial; } // 14px ÷ 1.5 = 9.333px,完全错了!
用户必须用 ~"14px/1.5" 转义来阻止 Less 误除,这违背了"CSS 是 Less 的超集"这个设计承诺。
Less 4.x 的解决思路:只有括号包裹的 / 才是除法,裸 / 默认当 CSS 值处理。这与 CSS 自身的 calc() 函数设计一致------calc(100px / 2) 需要显式写除法。
Less 3.x: / 默认是除法 → 经常误伤 CSS 简写
Less 4.x: / 默认是 CSS → 只有括号里的才是除法,更安全
这其实是大势所趋------Sass 也经历了类似的纠偏(/ 除法 → math.div()),所有 CSS 预处理器都在从不加区分地"接管一切"转向"尊重 CSS 原生语义"。但这个项目升级是 Less 3→4 的副作用(Vue 2.6→2.7 顺带升级了依赖),antd 的代码是 Less 3.x 时代写的,仍用裸 /,所以暂时用 math: 'always' 恢复旧行为。
修复
javascript
less: {
math: 'always', // 恢复 Less 3.x 的行为
}
验证效果 :division 表达式的编译结果与 Less 3.x 一致,依赖旧行为的样式(如 @width / 2 风格的计算)正常渲染。
1.6 CSS url() 中的路径
问题
less
// 原代码(webpack)
background-image: url('~@/assets/images/transparent.png');
构建时警告:
~@/assets/images/transparent.png didn't resolve at build time
修复
less
// 修复后
background-image: url('@/assets/images/transparent.png');
为什么 :Less 的 url() 函数中同样不识别 ~ 前缀。@/ 由 Vite 的 resolve.alias 解析为 src/ 目录。
注意 :Less 的 @import 和 url() 是两种不同的东西,它们走的解析通道不同:
@import '...' |
url('...') |
|
|---|---|---|
| 谁解析 | Less 编译器 | Less 编译器透传,最终由 Vite CSS 管线处理 |
resolve.alias 生效? |
❌ 不生效(Less 编译器有独立解析器) | ✅ 生效(走 Vite 的 CSS 管线) |
~ 前缀怎么处理 |
Less 插件的 pre-processor 正则替换 | 直接去掉 ~,让 @/ 走 Vite alias |
所以 §1.3 中 ~ 的处理是针对 @import 的,而这里的 ~ 出现在 url() 中------直接去掉即可,不需要 Less 插件处理。
验证效果:构建警告消失,图片路径被正确解析为 Vite 的静态资源 URL。