Webpack迁到Vite第三节:Less 与 CSS 处理

Webpack迁到Vite第三节:Less 与 CSS 处理

本篇定位 :Less 处理链的整体搬迁------style-resources-loader → additionalData、~ 前缀 → Less 编译器插件、品牌色 modifyVars 覆盖、Less 4.x 的 math 变化与 CSS url() 路径。每条链路均从 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')}";`
      },
    },
  },
},

逐行解释:

  1. additionalData: (source, filePath) => { ... }:一个函数,在每个 Less 文件编译前调用

    • source:文件的原始内容
    • filePath:文件的绝对路径
  2. if (filePath.includes('default.less')) return source:跳过 default.less 自身

    • 避免 default.less 被注入自身(Less 有导入去重,注入无意义)
    • 这是一个安全守卫
  3. 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 的理由:

  1. 项目中没有源文件覆盖 @brand-color 的场景,prepend 的"允许覆盖"优势用不上
  2. append 让 default.less 成为"最后说话的人",@primary-color 自动跟随 @brand-color
  3. modifyVars 只需要一行 '@brand-color': brandColor,逻辑更简洁
  4. 与 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(/^~/, '')}"`
          )
        },
      })
    },
  }],
}

逐行解释:

  1. plugins: [{ install(_, pluginManager) { ... } }]:注册一个 Less 插件

    • install 方法接收 Less 实例和插件管理器
  2. pluginManager.addPreProcessor({ process(src) { ... } }):添加一个预处理器

    • 预处理器在 Less 编译器解析之前运行
    • 对所有 Less 文件生效,包括 @import 递归引用的文件
  3. 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

    注意:[^'"]+ 不包含引号本身------引号由外面的 ['"] 匹配。捕获组拿到的是引号之间的内容(不含引号)。

  4. 替换回调 (_, 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。

相关推荐
ShineWinsu1 小时前
对于JavaScript:初识JavaScript以及输入输出
java·开发语言·前端·javascript·css·面试·html
码银2 小时前
【web】基于HTML/CSS,JavaScript + Three.js做3D流程图
前端·css·html
定栓2 小时前
Webpack迁到Vite第十节:vite.config.js 开发与构建流转全链路
vue.js·webpack·vite
定栓3 小时前
Webpack迁到Vite第二节:JSX 到 Render 函数
vue.js·webpack·vite
定栓3 小时前
Webpack迁到Vite第一节:模块系统迁移:require → import
运维·前端·webpack
定栓4 小时前
Webpack迁到Vite第四节:SVG 图标系统
前端·webpack·node.js
定栓5 小时前
Webpack迁到Vite第六节:构建配置详解(vite.config.js)
前端·javascript·webpack