Webpack迁到Vite第八节:错误排查实录

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 的静态资源处理管线会正确解析这些图像路径。具体来说:

  1. Vue 模板编译时,@vitejs/plugin-vue2 提取模板中的 require() 调用
  2. 将其转换为标准的 ESM import 路径
  3. 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 中的虚拟模块插件负责:

  1. 读取 src/assets/svgIcons/ 目录下所有 .svg 文件
  2. 提取每个文件的 <svg> 标签内部内容
  3. 生成 <symbol id="icon-xxx" viewBox="..."> 元素
  4. 合并为完整 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

排查过程

  1. 定位文件 :ant-design-vue-xxx.js 是 manualChunks 配置中拆分出的 ant-design-vue 独立 chunk
  2. 定位代码 :chunk 中第 7 行、第 106831 列位置,存在一个裸 require() 调用
  3. 定位来源 :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)的转换
相关推荐
haluhalu.1 小时前
深入理解 AI Agent(1.2):Harness 工程——模型之外的竞争力
前端·数据结构·人工智能·python·langchain·个人开发
data analyse 4562 小时前
服务端统计时区切换的日期归并
大数据·前端·数据库·人工智能·数据分析
misty youth2 小时前
前端知识速查
前端·react.js·web
雪芽蓝域zzs2 小时前
从零搭建【pnpm+Vue3+TS+Setup+VueRouter4+Pinia+Axios+ElementPlus (unplugin 按需)】仿若依后台
前端·vue.js
燕落南枝2 小时前
HTML 列表标签详解:有序列表、无序列表与描述列表
前端·html
派小心.3 小时前
DAU和MAU怎么计算?先确定活跃事件、用户键和统计窗口
大数据·服务器·前端·人工智能·数据分析
代码猎人3 小时前
前端怎么配合后端完成bug修复
前端
叫我铁汁儿吧3 小时前
JS作用域
前端·面试
定栓3 小时前
Webpack迁到Vite第七节(四):全家桶移除 — 保留项、脚本与数据收尾
前端·webpack·node.js