Webpack迁到Vite第八节:错误排查实录
本篇定位:迁移全过程的「病历本」------11 个真实错误的完整排查链路(现象 → 定位 → 根因 → 修复 → 验证),外加排查方法论(三类错误的本质、通用流程与工具)。遇到迁移相关问题时,从错误对比表反查最快。
思维导图
错误排查实录
├── 0. 错误排查方法论
│ ├── 0.1 三类错误的本质
│ ├── 0.2 排查通用流程
│ └── 0.3 排查工具
├── 1.1 require is not defined
│ ├── 现象:svgIcons/index.js:7
│ ├── 根因:require.context 是 Webpack 特有 API
│ └── 修复:Vite 虚拟模块(virtual:svg-icons)
├── 1.2 moment is not defined
│ ├── 现象:main.js:24
│ ├── 根因:ProvidePlugin 不存在
│ └── 修复:window.moment 全局注入
├── 1.3 品牌色不生效
│ ├── 现象:antd 组件显示蓝色
│ ├── 根因:additionalData 缺失(替代 style-resources-loader)+ modifyVars 缺失(动态品牌色)
│ └── 修复:additionalData 注入 default.less + modifyVars 动态注入 @brand-color
├── 1.4 Less ~ 前缀递归失败
│ ├── 现象:vant 的 ~@vant/icons 找不到
│ ├── 根因:Vite transform 插件对递归 import 不生效
│ └── 修复:Less 编译器 pre-processor
├── 1.5 (void 0) is not a function
│ ├── 现象:admin/statistics 页面
│ ├── 排查过程:chunk → CJS 预构建 → moment 命名导出缺失
│ ├── 尝试方案:transform 插件 → resolveId → resolve.alias
│ └── 修复:Moment Shim + 正则精确匹配 alias
├── 1.6 SVG 图标不显示
│ ├── 现象:H5 端图标空白
│ ├── 根因:viewBox 未动态提取(预防性说明)+ xlink:href 不匹配
│ └── 修复:正确取值 + 动态 viewBox
├── 1.7 VALID_MAP 导出不匹配
│ ├── 现象:构建报错
│ ├── 根因:Rollup 比 Webpack 更严格
│ └── 修复:命名导入 → 默认导入
├── 1.8 transparent.png 路径警告
│ ├── 现象:构建警告
│ ├── 根因:CSS url() 中的 ~ 前缀
│ └── 修复:移除 ~
├── 1.9 autoprefixer 缺失
│ ├── 现象:PostCSS 加载失败
│ ├── 根因:移除 @vue/cli-service 后丢失传递依赖
│ └── 修复:npm install -D autoprefixer
├── 1.10 moment/locale/zh-cn 解析失败
│ ├── 现象:import 报错
│ ├── 根因:resolve.alias 对象格式拦截了子路径
│ └── 修复:改为数组格式 + 正则精确匹配
└── 1.11 生产构建 require is not defined(antd chunk)
├── 现象:dist 部署后浏览器报 require is not defined
├── 根因:antd 1.x 混合模块,Rollup 未转换 require()
└── 修复:build.commonjsOptions.transformMixedEsModules
0. 错误排查方法论
0.1 三类错误的本质
从 Webpack 迁移到 Vite,本质上是从一个自实现模块系统切换到一套标准 ESM 工具链。迁移过程中遇到的错误,按根因可以分为三类:
| 类别 | 本质 | 例子 | 难度 |
|---|---|---|---|
| API 不兼容 | 使用了 Webpack 特有的 API,Vite 没有 | require.context、ProvidePlugin |
⭐ 低 |
| 工具链差异 | 两个构建工具对同一规范的处理方式不同 | Rollup 严格 vs Webpack 宽松、Less 编译层级 | ⭐⭐ 中 |
| 机制性差异 | 底层运行机制完全不同,需要深入理解才能解决 | CJS 预构建、resolve.alias 匹配规则 |
⭐⭐⭐⭐⭐ 高 |
这三类错误对应不同的排查深度:
API 不兼容:直接替换即可(查文档 → 找到对应 API)
工具链差异:需要理解两个工具的做法差异(对比分析)
机制性差异:需要理解底层机制(源码分析 + 调用链追踪)
0.2 排查通用流程
① 读取错误信息
├── 错误类型(ReferenceError / TypeError / 构建报错)
├── 错误位置(文件名:行号:列号)
└── 错误消息("is not defined" / "is not a function" / "not found")
② 定位到源码
├── 如果是构建产物报错(chunk-xxx.js),先映射回源文件
│ ├── 搜索 chunk 中的关键函数名、变量名
│ └── 通过 npm 包名定位到 node_modules 中的源文件
└── 如果是开发环境报错,直接看源文件
③ 理解根因
├── 这个 API 在 Webpack 中是怎么工作的?
├── Vite 为什么没有它?
└── Vite 的等价方案是什么?为什么等价?
④ 验证修复
└── 修复后,原本的报错路径是否被正确处理?
0.3 排查工具
| 工具 | 用途 |
|---|---|
git log --oneline --follow -- <file> |
追踪文件历史,了解改动背景 |
grep -rn "关键词" src/ |
搜索所有引用方,确认改动影响范围 |
node_modules/<包名>/ 源码阅读 |
定位第三方库的具体代码 |
| 浏览器开发者工具 → Source | 查看构建产物的具体代码 |
1.1 require is not defined
错误信息:
Uncaught ReferenceError: require is not defined
at index.js:7:13
错误位置 :src/assets/svgIcons/index.js:7
根因分析
什么是 require.context
require.context 是 Webpack 特有的 API,不是 JavaScript 语言的一部分,也不是 Node.js 的标准 API。它是在 Webpack 打包时由 Webpack 运行时注入到每个模块作用域中的。
javascript
// 原代码(src/assets/svgIcons/index.js)
const req = require.context('./', false, /\.svg$/)
要理解为什么 require.context 在 Vite 中不存在,需要先理解 Webpack 和 Vite 的本质区别:
Webpack 运行时环境:
浏览器运行的是 Webpack 打包后的 bundle
bundle 中每个模块被包裹在工厂函数中
每个模块作用域中都有 __webpack_require__ 函数
require.context 在编译时被转换为上下文模块,调用处替换为加载该模块的运行时调用
→ 所以 require 在浏览器中"存在"
Vite 运行时环境:
开发模式:浏览器直接加载原生 ESM 模块
浏览器原生 import 没有 require 这个概念
→ 所以 require 在浏览器中"不存在"
require.context 接受三个参数:
| 参数 | 值 | 含义 |
|---|---|---|
directory |
'./' |
要搜索的目录(相对于当前文件) |
useSubdirectories |
false |
是否搜索子目录 |
regExp |
/\.svg$/ |
匹配文件的模式 |
执行效果 :返回一个 require.context 对象,该对象有 .keys() 方法返回所有匹配文件路径的数组,每个路径可以通过 require(path) 获取模块内容。
为什么浏览器 ESM 环境没有 require
ESM(ECMAScript Modules)是 JavaScript 语言标准的一部分,由 ECMAScript 规范定义。原生 ESM 使用 import 和 export 关键字,没有 require 函数,也没有 require.context 这样的辅助 API。
浏览器原生 ESM 环境:
import X from './module.js' → ✅ 标准 ESM
const X = require('./module.js') → ❌ ReferenceError: require is not defined
require.context(...) → ❌ ReferenceError: require is not defined
修复方案
替换为 Vite 虚拟模块插件:
javascript
// 修复前(Webpack)--- src/assets/svgIcons/index.js
const req = require.context('./', false, /\.svg$/)
const requireAll = (requireContext) => requireContext.keys().map(requireContext)
requireAll(req)
// 修复后(Vite)--- src/assets/svgIcons/index.js
import 'virtual:svg-icons' // 虚拟模块,在 vite.config.js 中定义
与 import.meta.glob 的对比 :最常见的 Vite 替代方案是 import.meta.glob,但本项目 SVG 图标系统选择了更优的虚拟模块(virtual module)方案。原因如下:
| 方案 | 原理 | 缺陷 |
|---|---|---|
import.meta.glob |
每个 SVG 文件生成一个 ESM 模块,运行时逐个加载 | 86 个 SVG 对应 86 次请求,H5 端首屏性能差 |
| 虚拟模块 | 编译时将 86 个 SVG 合并为单个 SVG Sprite,一次性注入 DOM | 无额外请求,性能更优 |
虚拟模块在 vite.config.js 中定义:编译时读取所有 SVG 文件,提取 <svg> 内部内容,合并为 SVG Sprite 注入 DOM 隐藏容器。详细实现见第四节 §1.2。
组件中通过 <use :xlink:href="#icon-xxx" /> 引用对应的 SVG 图标。
修复效果 :require is not defined 报错消失,构建通过;SVG Sprite 由虚拟模块在编译时合并注入,图标正常显示。
见 Webpack迁到Vite第四节:SVG 图标系统 的详细说明。
补充说明:Vue 模板中的 require() 调用
src/ 目录中 Vue 模板有大量 :src="require('@/assets/images/...')" 用法(如 upload.vue、SignatureResult.vue、home.vue 等 20+ 个文件):
html
<img :src="require('@/assets/images/success.png')" />
这些 require() 调用不需要手动迁移 。@vitejs/plugin-vue2 在编译 Vue 模板时,会自动检测 :src="require(...)" 这样的表达式,将其转换为 import 语句,Vite 的静态资源处理管线会正确解析这些图像路径。具体来说:
- Vue 模板编译时,
@vitejs/plugin-vue2提取模板中的require()调用 - 将其转换为标准的 ESM
import路径 - Vite 的静态资源处理管线接管,解析
@/别名,产出带 hash 的静态资源 URL
因此,Vue 模板中 :src="require('@/assets/images/xxx.png')" 的写法在 Vite 下可以正常工作,无需逐个文件修改。这是 @vitejs/plugin-vue2 提供的向下兼容处理。
1.2 moment is not defined
错误信息:
ReferenceError: moment is not defined
at main.js:24
错误位置 :src/main.js:24 --- moment.locale('zh-cn')
根因分析
Webpack 的 ProvidePlugin 机制
ProvidePlugin 是 Webpack 的一个插件,它在编译阶段 自动为每个模块注入 import 语句。它不是运行时 API,而是一个编译时宏。
javascript
// webpack.config.js
new webpack.ProvidePlugin({
moment: 'moment', // 遇到 moment 变量,自动 import 'moment'
_: 'lodash', // 遇到 _ 变量,自动 import 'lodash'
})
它做的事情是:在编译每个模块时,检测到 moment 标识符,自动在前面加上 import moment from 'moment'。moment 变量在源码中就像"全局变量"一样可用,但实际是通过模块导入实现的。
ProvidePlugin 编译时效果:
源码:
moment.locale('zh-cn') // 直接使用,没有 import
编译后(Webpack 自动注入):
import moment from 'moment' // ← 自动注入
moment.locale('zh-cn')
整个过程中,开发者不需要手动写 import 语句
为什么 Vite 没有 ProvidePlugin
Vite 没有内置的自动注入机制。Vite 的设计哲学是显式优于隐式------每个模块使用的依赖应该显式声明,而不是靠构建工具偷偷注入。Vite 也没有提供官方等价的 ProvidePlugin 插件。
项目中依赖 ProvidePlugin 的变量
在 src/main.js 中,moment 和 _(lodash)被直接使用:
javascript
// src/main.js(错误发生时的状态)
import moment from 'moment' // 第 2 行:虽已导入
import _ from 'lodash' // 第 3 行:虽已导入(后已优化为 lodash-es 按需导入,见下文)
// ...
moment.locale('zh-cn') // 第 24 行:报错!
等一下,这里有一个关键问题:main.js 第 2 行明明已经 import moment from 'moment',为什么第 24 行还会报错?
这是因为 moment.locale('zh-cn') 这行代码在 main.js 中,而 main.js 本身已经导入了 moment。但问题在于:这个错误可能来自其他文件 ------项目中有 28 个文件引用了 moment(见 grep 结果),但并非所有文件都导入了它。Webpack 的 ProvidePlugin 让所有这些文件不需要显式 import 就能使用 moment。
此外,更关键的是 _(lodash):项目中有 30+ 处直接使用 _ 全局变量,但这些文件都没有显式 import lodash。
修复方案
在 main.js 中显式挂到全局:
javascript
// src/main.js
import moment from 'moment'
import _ from 'lodash'
// 替代 ProvidePlugin:显式挂到 window 上
window.moment = moment
window._ = _
为什么用 window.moment 而不是逐个文件添加 import
这是一个风险与收益的权衡:
| 方案 | 改动文件数 | 风险 | 长期维护 |
|---|---|---|---|
| 逐个文件加 import | 30+ 个文件 | 高(遗漏一处就报错) | 差(新增文件也要记得加) |
| 全局挂载 | 1 个文件 | 低(污染全局命名空间,但 moment 和 _ 是稳定的全局名) |
好(一次性配置) |
为什么选择全局挂载:
- 项目中有 28 个文件 直接使用
moment全局变量(均未编写 import 语句,webpack 时代靠ProvidePlugin注入) - 项目中有 30+ 处直接使用
_(lodash)全局变量 - 逐个文件改 30+ 个文件 vs 改 1 个文件,后者风险更低
moment和_是众所周知的库名,作为全局变量不会引起混淆
注意 :window.moment = moment 必须写在 moment.locale('zh-cn') 之前,且 moment 的 import 必须已完成。
注意 :
window._ = _的全量导入是过渡方案------解决了_ is not defined的报错,但整个 lodash(未压缩 ~73KB,gzip ~27KB)被打入 vendor chunk。后续已优化为lodash-es按需导入(仅导入实际使用的 5 个方法,gzip ~8KB),详见第九节 §1.6。
修复效果 :moment is not defined 报错消失;全项目 28 个 moment 引用文件 + 30+ 处 _ 全局调用无需逐个添加 import。
1.3 品牌色不生效
错误现象 :ant-design-vue 组件(如按钮、标签页)显示默认蓝色 #1890FF,而不是品牌色 #FF8300。
排查过程
第一步:检查 default.less 注入
❌ Vite 中没有 webpack 的 style-resources-loader
→ default.less 未注入到 antd 的 .less 文件,@primary-color 未被覆盖
第二步:加入 additionalData
✅ 品牌色恢复为 #FF8300
→ additionalData 替代了 style-resources-loader,品牌色生效
第三步:检查构建参数 --brand_color
❌ 构建参数不生效,始终显示 #FF8300
→ webpack 原来还有 less-loader 的 modifyVars,Vite 中也需要对应配置
根因分析
品牌色生效依赖两个独立机制的配合:additionalData 在 Less 编译前将 default.less 注入到每个 .less 文件末尾(提供所有变量覆盖定义,包括 @primary-color: @brand-color),modifyVars 在 Less 编译时以最高优先级注入动态 @brand-color 值(来自构建参数 --brand_color)。两者互补------additionalData 提供结构,modifyVars 提供动态值。仅靠 additionalData 无法动态覆盖(它只能拼接固定文本),仅靠 modifyVars 无法提供完整的变量覆盖链(@primary-color 等派生变量不会自动生效)。
详细设计原理(Webpack 原配置、双通道分工表、Less 惰性求值、编译流程)见第三节 §1.4。
修复效果 :additionalData 恢复静态品牌色(antd 组件显示 #FF8300),modifyVars 恢复动态品牌色(--brand_color 构建参数生效)。两者缺一不可。
1.4 Less ~ 前缀递归失败
错误信息:
[less] '~@vant/icons/src/encode-woff2.less' wasn't found.
Tried - node_modules/vant/lib/icon/~@vant/icons/...
排查过程
第一反应 :创建一个 Vite transform 插件,在 Vite 层面移除 ~ 前缀。
测试结果:
- 项目源文件
src/components/h5/vant/index.less中的~被正确处理 ✅ - 但
vant/lib/icon/index.less(被@import的文件)内部的~没有被处理 ❌
根因分析
~ 前缀的两种用法
~ 是 Webpack 的约定,在 CSS/Less 中有两种用途:
① 指向 node_modules 中的包:
less
// Webpack 中
@import '~vant/lib/style/normalize';
// 等价于
@import 'node_modules/vant/lib/style/normalize';
② 配合 @ alias 在 url() 中引用项目资源:
css
/* 项目迁移前的历史代码:src/components/pc/PhotoShop.vue:389(`~` 已在迁移中移除,见 §1.8) */
background-image: url('~@/assets/images/transparent.png');
~ 的作用 :告诉 webpack 的 css-loader 把后面的路径当作模块请求(而非普通 URL),交给 webpack resolver 处理:
url('~@/assets/images/transparent.png')
↑ ~ → css-loader 识别为"模块路径",交给 webpack resolver
↑ @ → webpack alias(vue.config.js: resolve('src'))
→ 最终解析为:src/assets/images/transparent.png
→ webpack 处理该图片(hash、copy 到 dist、返回正确路径)
如果没有 ~,url('@/assets/...') 会被 css-loader 当作普通 URL 原样输出到 CSS 中,不走 alias 解析,最终浏览器请求 /@/assets/... 导致 404。
迁移到 Vite:
Vite 的 CSS/Less 处理原生支持 resolve.alias 中定义的路径,不需要 ~ 前缀。所以 url('~@/...') 改为 url('@/...') 即可。
⚠️ 注意:上述 Less
pre-processor插件仅处理@import中的~(正则:/@import\s+['"](~[^'"]+)['"]/g),不处理url()中的~@。代码中残留的url('~@/...')需要手动修改为url('@/...')。
为什么 Vite transform 插件只能处理入口文件
Vite 的 transform 钩子工作流程如下:
src/components/h5/vant/index.less → Vite 读取文件
↓
transform 钩子执行 ✅ 可以处理
↓
Vite 将修改后的内容传给 Less 编译器
↓
Less 编译器解析 @import
↓
读取 vant/lib/icon/index.less ← Less 编译器自己读取文件
↓
Vite 的 transform 钩子 ❌ NO!
↓
Less 编译器继续解析 vant/lib/icon/index.less 内部的 @import
↓
发现 ~@vant/icons/... ← Less 不认识 ~
↓
Error!
关键 :Less 的 @import 递归解析在 Less 编译器内部完成,它使用自己的文件系统读取器,不经过 Vite 的插件管道 。Vite 的 transform 钩子只对 Vite 直接读取的文件生效。
Less 递归 import 的完整链路
src/components/h5/vant/index.less
└── @import "vant/lib/icon/index.less" ← Vite 读取 → transform ✅
└── @import "~@vant/icons/src/encode-woff2.less" ← Less 自读 → transform ❌
└── ... 找不到 ~@vant/icons
修复方案
使用 Less 编译器层的 pre-processor 插件:
javascript
less: {
plugins: [{
install(_, pluginManager) {
pluginManager.addPreProcessor({
process(src) {
return src.replace(
/@import\s+['"](~[^'"]+)['"]/g,
(_, p) => `@import "${p.replace(/^~/, '')}"`
)
},
})
},
}],
}
为什么 pre-processor 能处理所有文件
pre-processor 是 Less 编译器自身的插件机制,在 Less 编译器开始解析每个文件之前运行。它对 Less 编译器读取的每个文件都生效 ,包括递归 @import 的文件:
Less 编译器处理流程(带 pre-processor):
① 读取文件内容
② 运行 pre-processor 插件(修改内容) ← 在这里移除 ~
③ 解析 @import 语句
④ 对被 @import 的文件,回到步骤 ①
→ 每个文件都在步骤 ② 被处理,包括递归加载的文件
对比:
| 方案 | 入口文件 | 递归 import 文件 | 结果 |
|---|---|---|---|
| Vite transform 插件 | ✅ | ❌ | 失败 |
| Less pre-processor | ✅ | ✅ | 成功 |
修复效果 :'~@vant/icons/...' wasn't found 类报错消失;入口文件与递归 @import 文件中的 ~ 前缀都被 pre-processor 处理,vant 的 Less 依赖链正常编译。
详细分析见 Webpack迁到Vite第三节:Less 与 CSS 处理。
1.5 (void 0) is not a function(最复杂的问题)
错误现象
错误信息:
TypeError: (void 0) is not a function
at chunk-XAFG54LA.js:1174:60
at Array.findIndex
at validator2
at assertProp
at validateProp
触发页面 :/admin/statistics(管理后台数据统计页面)
完整调用链
Vue2._init(组件初始化)
→ initState
→ initProps
→ validateProp
→ assertProp
→ validator2(antd 的 prop 校验器)
→ Array.findIndex(callback)
→ callback 调用 undefined(val) ← 报错!
排查过程
第一步:定位 chunk 文件
错误堆栈指向 chunk-XAFG54LA.js:1174:60。这是 Vite 开发模式依赖预构建 生成的产物文件(node_modules/.vite/deps/chunk-*.js),不是源文件。需要查看该位置的具体代码。
javascript
// chunk-XAFG54LA.js:1174(简化)
return !(0, import_isNil.default)(val) && !(void 0)(val);
// ^^^^^^^^^
// 这里是 undefined!
第二步:理解 (void 0) 是什么
void 0 是 JavaScript 中获取 undefined 的一种方式。void 运算符对表达式求值后返回 undefined,所以 void 0 就是 undefined。
(void 0)(val) 等价于 undefined(val) --- 调用 undefined 作为函数,这当然会抛出 TypeError: (void 0) is not a function。
关键问题 :为什么 (void 0) 会出现在这里?它原本应该是什么?
第三步:理解 Vite 预构建的 CJS→ESM 转换
Vite 在开发环境下使用 esbuild 对 CJS 依赖做预构建(转换为 ESM)。这个转换的局限在于:esbuild 无法静态枚举 CJS 模块的命名导出,预构建产物只有 default 一个导出。
moment 的实际导出结构(node_modules/moment/moment.js,UMD 包装,简化):
javascript
// moment 的 CJS 导出(简化)
;(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? module.exports = factory() // ← 导出的是一个函数调用的返回值(moment.js:8)
: global.moment = factory()
}(this, (function () {
function hooks() { ... } // hooks 即 moment 函数本体
function isMoment(obj) { ... }
hooks.isMoment = isMoment; // ← 属性在 factory 函数内部赋给局部变量 hooks(moment.js:5885)
return hooks;
})));
esbuild 转换后的实际预构建产物(node_modules/.vite/deps/moment_moment__js.js):
javascript
// 预构建产物的真实形态
import { require_moment } from "./chunk-XXXXXXXX.js";
export default require_moment(); // 只有 default 导出,没有任何命名导出
为什么 isMoment 提取不出来 :module.exports = factory() 是一次函数调用,esbuild 无法静态得知返回值上有哪些属性;而 hooks.isMoment = isMoment 发生在 factory 函数内部、赋值对象是局部变量 hooks,静态分析完全不可见。所以预构建产物中不可能有 isMoment 命名导出。
当 ant-design-vue(同为预构建产物、ESM)中 import * as moment from 'moment' 指向这个只有 default 导出的包装模块时,esbuild 对命名空间属性访问 moment.isMoment 做静态解析 :已知导出集合中没有 isMoment,该表达式被直接替换为 (void 0)------这就是报错字面是 (void 0) is not a function、而不是 moment.isMoment is not a function 的原因。
第四步:定位到 antd 的源代码
追踪到 ant-design-vue 的 moment-util.js。该文件导出了 TimeType、TimesType、TimeOrTimesType 三个 prop 校验器,内部都调用 moment.isMoment。对照报错栈(at Array.findIndex)与 chunk 代码(!(0, import_isNil.default)(val) && !(void 0)(val)),崩溃点在 TimesType validator 的 findIndex 回调中(TimeType 的 validator 没有 findIndex,且参数名为 value 而非 val):
javascript
// ant-design-vue/es/_util/moment-util.js
import * as moment from 'moment' // ← 导入 moment 命名空间
import isNil from 'lodash/isNil'
export var TimesType = {
validator: function(value) {
if (Array.isArray(value)) {
return value.length === 0 || ... || value.findIndex(function(val) {
return !isNil(val) && !moment.isMoment(val)
// ^^^^^^^^^^^^^^^^
// 预构建后 = (void 0)
// (void 0)(val) → TypeError!
}) === -1
}
return false
}
}
import * as moment from 'moment' 导入的是 moment 模块的命名空间对象 。在 ESM 中,import * as 返回一个包含所有导出的对象。但预构建后的 moment 只有 default 导出,没有 isMoment,所以 moment.isMoment 被 esbuild 静态替换为 (void 0)。
第五步:理解完整调用链
antd 组件初始化时,Vue 校验 props
→ 对 Type 为 TimesType 的 prop,调用其 validator 函数(报错栈中的 validator2)
→ validator 中执行 value.findIndex(callback)
→ callback 中调用 !isNil(val) && !moment.isMoment(val)
→ moment.isMoment 在预构建时已被替换为 (void 0)
→ (void 0)(val) 抛出 TypeError
尝试过的方案
| 方案 | 做法 | 为什么失败 |
|---|---|---|
| 1. transform 插件 | 改 import * as → import |
预构建在插件之前运行,antd 的代码已经被 esbuild 处理过了 |
| 2. resolve.alias(对象格式) | { moment: 'shim' } |
子路径 moment/locale/zh-cn 也被拦截 |
| 3. resolveId 插件 | resolveId(id) { if (id==='moment') ... } |
预构建时不调用 resolveId |
| 4. resolve.alias(数组+正则) | [{ find: /^moment$/, replacement: 'shim' }] |
✅ 成功 |
方案一:transform 插件 --- 为什么失败
javascript
// 想法:在 Vite transform 阶段拦截 antd 的代码,把 import * as 改为 import
{
name: 'fix-moment-import',
transform(src, id) {
if (id.includes('moment-util')) {
return src.replace(
"import * as moment from 'moment'",
"import moment from 'moment'"
)
}
}
}
失败原因 :ant-design-vue 是 node_modules 依赖,开发模式下由 optimizeDeps 预构建(esbuild)------预构建先于 Vite 插件(包括 transform)执行 ,预构建产物不再经过用户的 transform 钩子。当 transform 插件有机会运行时,antd 的代码已经是 esbuild 处理后的产物,import * as moment from 'moment' 的原始形态已不存在,正则无法命中。(生产构建中该依赖由 manualChunks 拆分为独立 chunk,如 ant-design-vue-[hash].js。)
方案二:resolve.alias 对象格式 --- 为什么失败
javascript
// 尝试
resolve: {
alias: {
moment: path.resolve(__dirname, 'src/shims/moment.js')
}
}
失败原因 :Vite 的 resolve.alias 对象格式使用包名边界的前缀匹配 ------如果 key 是 'moment',它会匹配恰好等于 moment,或以 moment/ 开头 的导入路径,因此 moment/locale/zh-cn、moment/moment.js 等子路径全部被拦截。
'moment' 对象格式 alias 的匹配规则(恰好相等,或以 'moment/' 开头):
'moment' → ✅ 匹配(恰好相等)
'moment/locale/zh-cn' → ✅ 匹配(以 'moment/' 开头)
'moment/moment.js' → ✅ 匹配(以 'moment/' 开头)
'/node_modules/moment/...' → ❌ 不匹配(绝对路径,不以 'moment/' 开头)
匹配后 alias 做的是前缀替换 (importee.replace(find, replacement)):moment/locale/zh-cn 被改写为 src/shims/moment.js/locale/zh-cn------一个不存在的路径(shim 是单文件,没有 locale 子模块),import 'moment/locale/zh-cn' 因此解析失败。
方案三:resolveId 插件 --- 为什么失败
javascript
// 尝试
{
name: 'moment-alias',
resolveId(id) {
if (id === 'moment') {
return path.resolve(__dirname, 'src/shims/moment.js')
}
}
}
失败原因 :resolveId 插件在 Vite 的依赖预构建阶段不会被调用 。预构建使用 esbuild 自己的解析器,不经过 Vite 的插件管道。当 antd 被预构建时,import * as moment from 'moment' 中的 'moment' 没有被 alias 拦截。
最终修复
方案四:resolve.alias 数组格式 + 正则精确匹配
javascript
// vite.config.js
resolve: {
alias: [
{ find: '@', replacement: path.resolve(__dirname, 'src') },
// 精确匹配 'moment',不匹配 'moment/locale/zh-cn' 等子路径
{ find: /^moment$/, replacement: path.resolve(__dirname, 'src/shims/moment.js') },
],
}
为什么数组格式可以 :数组格式的 alias 项支持 find 字段为正则表达式 。/^moment$/ 中的 ^ 表示字符串开头,$ 表示字符串结尾,合起来表示精确匹配。
/^moment$/ 匹配:
'moment' → ✅ 匹配(恰好是 'moment')
'moment/locale/zh-cn' → ❌ 不匹配('moment' 后面还有 '/locale/zh-cn')
'moment/moment.js' → ❌ 不匹配
'/node_modules/moment/...' → ❌ 不匹配
Moment Shim 文件
javascript
// src/shims/moment.js
// moment shim:补充 isMoment 命名导出,兼容 ant-design-vue 的 import * as 用法
import moment from 'moment/moment.js'
// 重新导出默认函数
export default moment
// 补充 isMoment 作为命名导出
export const isMoment = moment.isMoment.bind(moment)
为什么 shim 导入 moment/moment.js 而不是 moment:
如果 shim 里写 import moment from 'moment':
shim.js 被 alias 匹配 'moment' → 返回 shim.js
shim.js 中 import moment from 'moment' → 再次被 alias 匹配 → 返回 shim.js
→ 无限循环!❌
所以 shim 写 import moment from 'moment/moment.js':
'moment/moment.js' 不被 /^moment$/ 匹配
→ 直接加载原始 moment 包 ✅
为什么 bind(moment):
moment.isMoment是一个函数,它内部可能使用this引用 moment 对象- 当 antd 的代码中
moment.isMoment(value)被调用时,this应该指向 moment 对象 .bind(moment)确保isMoment函数内部的this始终指向正确的 moment 对象
修复后的效果
修复前(预构建的 moment 模块):
javascript
__export(moment_exports, {
default: () => moment_default
// 没有 isMoment!
})
修复后(shim 替换后的 moment 模块):
javascript
__export(moment_exports, {
default: () => moment_default,
isMoment: () => isMoment // ← 有了!
})
注意 :这个修复需要同时处理 alias 配置(让 moment 指向 shim)和 shim 文件(补充 isMoment 导出)。两者缺一不可。
预构建在 dev server 启动/流转中的位置(为何"先于插件执行"),见第十节 §2.1。
1.6 SVG 图标不显示
错误现象:H5 端所有 SVG 图标空白。
排查过程
坑一:虚拟模块生成的 SVG Sprite 未正确注入 DOM
SVG 图标系统通过虚拟模块 virtual:svg-icons 在编译时合并所有 SVG 文件为 SVG Sprite,并注入到页面 DOM 中。如果虚拟模块的 load 钩子代码有缺陷,生成的 SVG Sprite 可能为空或格式错误,导致 <use> 引用找不到对应的 <symbol>。
vite.config.js 中的虚拟模块插件负责:
- 读取
src/assets/svgIcons/目录下所有.svg文件 - 提取每个文件的
<svg>标签内部内容 - 生成
<symbol id="icon-xxx" viewBox="...">元素 - 合并为完整 SVG Sprite 注入到页面隐藏容器中
javascript
// 虚拟模块插件的核心逻辑
const symbols = readdirSync(svgDir)
.filter((f) => f.endsWith('.svg'))
.map((f) => {
const raw = readFileSync(path.resolve(svgDir, f), 'utf-8')
const name = f.replace('.svg', '')
// 动态提取原始 SVG 的 viewBox 属性
const viewBoxMatch = raw.match(/viewBox="([^"]+)"/)
const viewBox = viewBoxMatch ? ` viewBox="${viewBoxMatch[1]}"` : ''
const inner = raw.replace(/<svg[^>]*>/, '').replace('</svg>', '').trim()
return `<symbol id="icon-${name}"${viewBox}>${inner}</symbol>`
}).join('')
坑二:viewBox 硬编码(潜在风险说明,非本项目实际遭遇)
注:坑一展示的虚拟模块代码从最初就已包含动态 viewBox 提取(
raw.match(/viewBox="([^"]+)"/)),本项目未实际遭遇 viewBox 硬编码问题。以下为通用最佳实践说明,避免未来迭代中退化为硬编码。
javascript
// ❌ 错误:硬编码 viewBox,所有图标变形
<symbol id="icon-xxx" viewBox="0 0 1024 1024">...</symbol>
// ✅ 正确:动态提取原始 SVG 的 viewBox 属性
const viewBox = raw.match(/viewBox="([^"]+)"/)?.[1] || ''
根因 :项目中的 SVG 图标 viewBox 各不相同(0 0 20 20、0 0 16 16、0 0 1024 1024 等)。硬编码 1024 1024 后,小坐标系的图标(如 viewBox="0 0 20 20")里的图形在 1024×1024 的坐标系中只占极小一块,渲染出来就是缩成一个小点。
坑三:xlink:href 引用路径错误
SvgIcon.vue 组件通过 <use :xlink:href="iconName" /> 引用 SVG 图标,其中 iconName 计算属性返回 #icon-${name}。如果 name prop 传递的值与 symbol 的 id 属性不匹配,浏览器找不到对应的 <symbol>,图标不会显示。
html
<!-- 虚拟模块生成的 symbol -->
<symbol id="icon-close">...</symbol>
<!-- SvgIcon 组件引用 -->
<use xlink:href="#icon-close" /> <!-- 必须匹配 symbol 的 id -->
组件使用方式:
html
<svg-icon name="close" />
<!-- 对应 #icon-close -->
修复效果 :H5 端图标正常渲染;虚拟模块将 SVG Sprite 注入隐藏容器,所有 <use> 引用命中对应的 <symbol>。
详细分析见 Webpack迁到Vite第四节:SVG 图标系统。
1.7 VALID_MAP 导出不匹配
错误信息:
"VALID_MAP" is not exported by "src/constant/valid.js"
根因分析
源文件结构
javascript
// src/constant/valid.js
const VALID_MAP = { /* ... 大量校验规则 ... */ }
export default VALID_MAP // 默认导出
valid.js 使用 export default VALID_MAP 导出------这是默认导出。
错误的导入方式
javascript
// 某个组件(示意)
import { VALID_MAP } from '@/constant/valid' // ❌ 命名导入
import { VALID_MAP } 是命名导入 ,它要求源文件有 export const VALID_MAP = ... 或 export { VALID_MAP }。但源文件是 export default VALID_MAP,只有默认导出,没有命名导出。
Webpack vs Rollup 的差异
| 构建工具 | 行为 | 原因 |
|---|---|---|
| Webpack | 警告但不报错,运行时 VALID_MAP 为 undefined |
Webpack 对 ESM 缺失命名导出宽松处理 |
| Rollup(Vite 的生产构建) | 直接报错,拒绝构建 | Rollup 严格遵循 ESM 规范 |
为什么 Webpack 不报错 :Webpack 的模块系统是自实现的,对 ESM 缺失命名导出采取宽松处理------import { VALID_MAP } 只在编译时产生警告(export 'VALID_MAP' was not found in ... (possible exports: default)),运行时该绑定的值为 undefined,不中断构建。这种宽松客观上纵容了不规范的代码。
为什么 Rollup 报错 :Rollup 严格遵循 ESM 规范。ESM 规范规定:export default X 只创建一个默认导出,不创建命名导出。import { VALID_MAP } 要求源文件必须有名为 VALID_MAP 的导出,否则就是模块不存在该导出。
修复方案
将命名导入改为默认导入:
javascript
// 修复前
import { VALID_MAP } from '@/constant/valid'
// 修复后
import VALID_MAP from '@/constant/valid'
验证 :通过 grep 搜索所有使用了 VALID_MAP 的文件,确认导入方式是否统一。
bash
grep -rn "import.*VALID_MAP" src/ | grep -v node_modules
结果:项目中有 15+ 个文件使用 VALID_MAP,所有文件都使用默认导入 import VALID_MAP from '@/constant/valid',没有使用命名导入的。说明这个错误只出现在迁移初期的某个文件中,且已被修复。
1.8 transparent.png 路径警告
警告信息:
~@/assets/images/transparent.png didn't resolve at build time
根因分析
问题代码
less
// src/components/pc/PhotoShop.vue:389
background-image: url('~@/assets/images/transparent.png');
~ 前缀在 CSS url() 中同样不被 Less 识别。但这里和 @import 中的 ~ 处理方式不同:
| 上下文 | ~ 的处理 |
解析路径 |
|---|---|---|
@import '~vant/...' |
Less 编译器处理 | Less 编译器自己的文件系统解析 |
url('~@/assets/...') |
Less 编译器透传 | Vite CSS 管线处理 |
CSS url() 的解析流程
url('~@/assets/images/transparent.png')
↓ Less 编译器
url('~@/assets/images/transparent.png') ← Less 透传,不做处理
↓ Vite CSS 管线
Vite 尝试解析 ~@/assets/images/transparent.png
→ 不认识 ~ 前缀
→ 未解析的路径
→ 构建时警告
为什么 @/ 在 url() 中能工作
@ 是 Vite 的 resolve.alias 配置的路径别名:
javascript
resolve: {
alias: [
{ find: '@', replacement: path.resolve(__dirname, 'src') },
]
}
Vite CSS 管线会解析 CSS 中的 url() 路径,并应用 resolve.alias 规则。所以 url('@/assets/images/transparent.png') 会被解析为 url('/src/assets/images/transparent.png')。
修复方案
直接移除 ~ 前缀:
less
// 修复前
background-image: url('~@/assets/images/transparent.png');
// 修复后
background-image: url('@/assets/images/transparent.png');
注意 :这里的 @/ 在 Less 编译器层面不会被解析(Less 不认识 @ 别名),但 Less 编译器会原样透传 url() 中的内容到 CSS 输出。然后 Vite 的 CSS 管线会处理这个 url() 路径,应用 resolve.alias 规则。
修复效果 :构建警告消失,transparent.png 被 Vite CSS 管线正常解析为带 hash 的静态资源 URL。
1.9 autoprefixer 缺失
错误信息:
Failed to load PostCSS config: Cannot find module 'autoprefixer'
根因分析
什么是传递依赖
npm 的依赖关系有两种:
| 类型 | 含义 | 安装位置 |
|---|---|---|
| 直接依赖 | package.json 中声明的依赖 |
dependencies 或 devDependencies |
| 传递依赖 | 依赖的依赖(间接依赖) | 自动安装在 node_modules 中 |
autoprefixer 原本是 @vue/cli-service 的传递依赖:
@vue/cli-service(直接依赖)
├── postcss-loader(负责在 webpack 中接入 PostCSS,自身不携带 autoprefixer)
└── autoprefixer(@vue/cli-service 的直接依赖,^10.2.4)
→ 自动出现在 node_modules 中
→ 项目中可以直接使用
当 @vue/cli-service 被移除后,它的所有传递依赖也一并被移除:
npm uninstall @vue/cli-service
→ 删除 node_modules/@vue/cli-service
→ 删除 node_modules/autoprefixer(因为它是 @vue/cli-service 的传递依赖,不再被需要)
→ 但项目中还在使用 autoprefixer!
→ Error: Cannot find module 'autoprefixer'
为什么 Vite 需要 autoprefixer
Vite 本身不内置 PostCSS 的 autoprefixer 功能。虽然 Vite 内置了 PostCSS 支持,但具体的 PostCSS 插件(如 autoprefixer)需要用户自己安装。
Vite 的 PostCSS 配置加载流程:
Vite 构建时
├── 检查项目根目录是否有 postcss.config.js 或 postcss.config.cjs
├── 如果找到,加载配置并应用其中的 PostCSS 插件
└── 如果没找到,使用 Vite 内置的 PostCSS 配置(只含基础功能,不含 autoprefixer)
本项目在 webpack 和 Vite 中均存在 postcss.config.js,内容一致:
javascript
// postcss.config.js(webpack 项目与 Vite 项目内容相同)
module.exports = {
plugins: {
autoprefixer: {},
},
}
autoprefixer: {} --- 空对象不等于"无配置"
autoprefixer: {} 中的空对象表示使用全部默认行为 ,并非"没有配置"。Autoprefixer 会自动读取项目的 browserslist 来确定需要哪些厂商前缀:
autoprefixer: {} ← 空对象 = 使用默认配置
│
└── 自动读取 package.json 中的 browserslist
│
├── "> 1%" → 全球使用率 > 1% 的浏览器
├── "last 2 versions" → 每个浏览器最新 2 个版本
└── "not ie <= 10" → 排除 IE10 及以下
│
▼
Autoprefixer 根据 caniuse 数据自动决定需要哪些前缀
│
▼
编译结果示例:
.box { display: -webkit-box; display: flex }
.btn { -webkit-border-radius: 2px; border-radius: 2px }
如果没有 autoprefixer: {}(即 postcss.config.js 不配置 autoprefixer 插件),这些厂商前缀就不会被添加,旧版 Android WebView、老 iOS Safari 等设备上可能出现样式兼容性问题。
autoprefixer: {}和autoprefixer自动读取的browserslist是互补关系 :browserslist定义目标浏览器范围,autoprefixer 根据这个范围决定加哪些前缀。两者缺一不可。
在 webpack 时代,autoprefixer 是 @vue/cli-service 的传递依赖(@vue/cli-service 直接依赖 autoprefixer,而非经由 postcss-loader------postcss-loader 自身的依赖只有 cosmiconfig、klona、semver),自动出现在 node_modules 中,postcss.config.js 引用它时无需在 package.json 中显式声明。迁移到 Vite 后,@vue/cli-service 被移除,autoprefixer 作为其传递依赖也被一并移除------但 postcss.config.js 仍然引用 autoprefixer,导致 Vite 加载配置时报错 Cannot find module 'autoprefixer'。
修复方案
将 autoprefixer 设为直接依赖:
bash
npm install -D autoprefixer
为什么是 -D(devDependencies) :autoprefixer 是构建时工具,只在开发环境和构建时使用,生产环境不需要。
修复效果 :npm run build 正常加载 PostCSS 配置,CSS 自动前缀功能恢复。
1.10 moment/locale/zh-cn 解析失败
错误信息:
Failed to resolve import "moment/locale/zh-cn" from "src/main.js"
根因分析
问题代码
javascript
// src/main.js:7
import 'moment/locale/zh-cn'
为什么 resolve.alias 对象格式会拦截子路径
这与 §1.5 的排查同源:resolve.alias 对象格式按包名边界的前缀匹配 (key 为 moment 时,连 moment/locale/zh-cn 等子路径也会命中),数组格式则支持 find 正则、可用 /^moment$/ 精确匹配。完整的匹配规则对比,以及"子路径被前缀替换为无效路径"的失败分析见 §1.5「方案二:resolve.alias 对象格式 --- 为什么失败」,此处不再重复。
为什么 shim 文件不包含 locale
src/shims/moment.js 的内容:
javascript
import moment from 'moment/moment.js'
export default moment
export const isMoment = moment.isMoment.bind(moment)
这个 shim 只做了两件事:重新导出 moment 的默认函数,补充 isMoment 命名导出。它没有 也不应该 导出 locale/zh-cn 子模块,原因如下:
① moment/locale/zh-cn 是副作用模块,必须加载真实 moment
javascript
// node_modules/moment/locale/zh-cn.js(第 9 行)
factory(require('../moment'))
locale 文件通过 require('../moment') 拿到 moment 对象,将中文语言包注册上去。这个相对路径 ../moment 是相对于 node_modules/moment/locale/zh-cn.js 的位置解析的------它必须指向 node_modules/moment/moment.js(真实的 moment 包),才能正确注册语言包。
若被对象格式 alias 拦截,moment/locale/zh-cn 会被前缀替换为 src/shims/moment.js/locale/zh-cn 这样的不存在路径,直接报 Failed to resolve import(即本节的错误信息)------locale 文件根本没有机会执行 require('../moment') 的副作用注册。所以 locale 必须绕过 alias、走真实包。
② shim 和 locale 操作的是同一个 moment 实例
shim: import moment from 'moment/moment.js' ← 拿到的 moment(单例)
locale: require('../moment') ← 同一个 moment(单例)
→ moment.locale('zh-cn') ← 注册到同一个单例上
→ shim 的 export default moment ← 已包含中文语言包
moment 是单例模块------node_modules/moment/moment.js 无论被 import 多少次,返回的都是同一个对象。所以:
'moment/locale/zh-cn'走真实 moment → 注册 locale 到单例'moment'走 shim → shim 内部import moment from 'moment/moment.js'拿到的也是同一个单例 → 已包含中文 locale
③ 结论 :正则 /^moment$/ 的 $ 结尾锚点是关键------它让 locale 子模块不被 shim 拦截,直接走 real moment 包执行副作用注册。shim 只需处理 'moment' 自身的导入(补充 isMoment),locale 由真实 moment 包独立处理。
修复方案
将对象格式改为数组格式,使用正则精确匹配:
javascript
// 修复前(对象格式,前缀匹配)
alias: {
moment: path.resolve(__dirname, 'src/shims/moment.js')
}
// 修复后(数组格式,正则精确匹配)
alias: [
{ find: '@', replacement: path.resolve(__dirname, 'src') },
{ find: /^moment$/, replacement: path.resolve(__dirname, 'src/shims/moment.js') },
]
$ 的作用 :正则中的 $ 表示字符串结尾。/^moment$/ 只匹配恰好是 moment 的字符串,不匹配 moment/locale/zh-cn。
| 导入路径 | 对象格式 { moment: 'shim' } |
数组格式 { find: /^moment$/ } |
|---|---|---|
'moment' |
✅ 匹配 → shim | ✅ 匹配 → shim |
'moment/locale/zh-cn' |
✅ 匹配 → shim(错误!) | ❌ 不匹配 → 原始包 |
'moment/moment.js' |
✅ 匹配 → shim(错误!) | ❌ 不匹配 → 原始包 |
修复后验证:moment 全局使用链路
修复后对项目全局核查,确认 moment 的引入链路一致、无重复导入。
全项目仅 3 处 import:
src/main.js:2 import moment from 'moment' → alias /^moment$/ → shims/moment.js
src/main.js:7 import 'moment/locale/zh-cn' → 绕过 alias → moment/locale/zh-cn.js
src/shims/moment.js:2 import moment from 'moment/moment.js' → 绕过 alias → moment/moment.js
数据流 :shim、locale、window.moment 三者均指向同一个 moment 单例。
moment/moment.js(真实包,单例)
↑ import ↑ require('../moment')
shims/moment.js moment/locale/zh-cn.js
│ export default │ 注册 zh-cn 到单例
↓ ↓
main.js → window.moment = moment(全局可用,locale 已就绪)
验证清单:
| 验证点 | 结论 |
|---|---|
shim、locale、window.moment 指向同一 moment 单例 |
✅ |
alias /^moment$/ 正确隔离 shim 与子路径 |
✅ |
window.moment 替代 webpack ProvidePlugin,所有项目文件通过全局访问,无冗余 import |
✅ |
| locale 副作用注册到单例后,shim 导出的 moment 自动包含 locale | ✅ |
与 webpack 的对比:
| Webpack | Vite | |
|---|---|---|
| moment 导入 | ProvidePlugin 自动注入 |
main.js 显式 import + window.moment |
| locale 导入 | side-effect import | side-effect import(相同) |
| 项目文件使用方式 | 全局 moment |
全局 window.moment(行为一致) |
1.11 生产构建后 require is not defined(ant-design-vue chunk)
错误现象
执行 npm run build 后,将 dist 目录部署到 HTTP 服务器,浏览器控制台报错:
Uncaught ReferenceError: require is not defined
at ant-design-vue-xxx.js:7:106831
排查过程
- 定位文件 :
ant-design-vue-xxx.js是manualChunks配置中拆分出的 ant-design-vue 独立 chunk - 定位代码 :chunk 中第 7 行、第 106831 列位置,存在一个裸
require()调用 - 定位来源 :ant-design-vue 1.x 内部部分模块混用了
import和require()------同一个文件里既有 ESM 的import,又有 CJS 的require()
根因
Rollup 的 @rollup/plugin-commonjs 在处理 ant-design-vue 时,默认只处理纯 CJS 模块 (文件中只有 require 和 module.exports)。但如果一个文件同时包含 import 和 require()(称为"混合模块"),插件默认跳过它,导致 require() 原样保留在输出中。
浏览器不认识 require(),报错。
修复
在 vite.config.js 的 build 中配置 commonjsOptions:
javascript
build: {
commonjsOptions: {
transformMixedEsModules: true, // 处理混用 import 和 require() 的模块
include: [/node_modules/], // 覆盖所有 node_modules 依赖
},
},
transformMixedEsModules: true 告诉 Rollup 的 commonjs 插件:即使文件中有 import 语句,也把其中的 require() 转换为 ESM 格式。include 确保所有 node_modules 中的依赖都被处理。
修复效果 :生产构建产物中不再有裸 require() 调用,部署后浏览器正常执行,require is not defined 报错消失。
关键教训
@rollup/plugin-commonjs默认跳过混合模块(同时包含import和require),这是性能优化但可能产生遗漏- 旧版 UI 库(如 ant-design-vue 1.x)常有混合模块,需要在构建时显式处理
- 这个错误只在生产构建后出现,开发模式(
vite)不会触发------因为开发模式走的是optimizeDeps预构建路径,esbuild 处理 CJS 的方式不同
错误对比表
| # | 错误 | 难度 | 类别 | 关键教训 | 涉及文件 |
|---|---|---|---|---|---|
| 1 | require is not defined |
⭐ | API 不兼容 | require.context 是 Webpack 特有 API,用 Vite 虚拟模块替代 |
src/assets/svgIcons/index.js |
| 2 | moment is not defined |
⭐ | API 不兼容 | ProvidePlugin 需手动替代,用 window.moment 全局注入 |
src/main.js |
| 3 | 品牌色不生效 | ⭐⭐ | 工具链差异 | ① additionalData 替代 style-resources-loader 注入 default.less(两者作用范围相同,均覆盖 node_modules);② modifyVars 实现动态品牌色(--brand_color) |
vite.config.js |
| 4 | ~ 前缀递归失败 |
⭐⭐⭐ | 机制性差异 | Vite 插件 vs Less 插件的层级差异,Less 编译器是"飞地" | vite.config.js |
| 5 | (void 0) is not a function |
⭐⭐⭐⭐⭐ | 机制性差异 | CJS 预构建 + 命名导出缺失,排查链条最长(5 步) | src/shims/moment.js、vite.config.js |
| 6 | SVG 图标不显示 | ⭐⭐ | 工具链差异 | 虚拟模块 SVG Sprite 注入 + viewBox 动态提取(预防性说明)+ xlink:href 匹配 |
src/assets/svgIcons/index.js、vite.config.js |
| 7 | VALID_MAP 导出不匹配 |
⭐ | 工具链差异 | Rollup 严格遵循 ESM 规范,Webpack 宽容 | src/constant/valid.js(导入方组件为示意案例) |
| 8 | transparent.png 警告 |
⭐ | 工具链差异 | CSS url() 中的 ~ 不被 Less 识别,直接移除 |
src/components/pc/PhotoShop.vue |
| 9 | autoprefixer 缺失 |
⭐ | 机制性差异 | 传递依赖陷阱,@vue/cli-service 移除后连带删除 |
package.json |
| 10 | moment/locale 解析失败 |
⭐⭐ | 机制性差异 | resolve.alias 对象格式前缀匹配,改用正则精确匹配 |
vite.config.js |
| 11 | 生产构建 require is not defined |
⭐⭐ | 机制性差异 | antd 1.x 混合模块,Rollup 默认不转换,需 commonjsOptions.transformMixedEsModules |
vite.config.js |
各错误揭示的迁移规律
API 不兼容类(1-2):直接替换 API 即可,难度最低。但需要知道"Webpack 特有 API"列表。
工具链差异类(3、6-8):需要理解两个工具对同一功能的实现差异。通常可以通过对比配置找到答案。
机制性差异类(4-5、9-11):需要深入理解底层机制。这类错误最耗时,但解决后对迁移的理解会加深一层。
修复模式总结
| 修复模式 | 示例 | 适用场景 |
|---|---|---|
| API 替换 | require.context → Vite 虚拟模块 |
构建工具特有 API |
| 全局注入 | window.moment = moment |
替代 ProvidePlugin |
| 配置双通道 | additionalData + modifyVars |
互补性配置 |
| 层级下沉 | Vite 插件 → Less pre-processor | 不同处理层级 |
| Shim 文件 | src/shims/moment.js |
补充缺失的命名导出 |
| 精确匹配 | 对象 alias → 数组正则 alias | 路径匹配精确度控制 |
| 直接依赖化 | 传递依赖 → 直接依赖 | 避免传递依赖陷阱 |
| 导入方式修正 | import { VALID_MAP } → import VALID_MAP |
命名导入与默认导出不匹配 |
| 路径前缀移除 | CSS url() 中 ~ 前缀直接删除 |
Webpack 特有 ~ 前缀清理 |
| 构建配置开关 | commonjsOptions.transformMixedEsModules: true |
混合模块(同时含 import 和 require)的转换 |