环境配置:vue3+vite+element-plus+pinia
一、问题现象
项目在开发环境(npm run dev)运行一切正常,但构建部署到生产环境后,浏览器控制台报出以下错误:
javascript
Uncaught ReferenceError: Cannot access 'buildProp' before initialization
错误指向 element-plus-CLZbZCGX.js 第 627 行: 如果需要展示打包后能看的代码。需要在vite.config.js中配置这个是代码混淆,既然将打包后的代码编译为较难读的情况,比如将buildProp变量名该为El
js
defineConfig({
build: {
minify: false, //该选择默认使用esbuild
},
});
js
const _prop = buildProp({
type: definePropType(Boolean),
default: null,
});
二、排查过程
2.1 寻找引起报错的代码
因为在项目中上一个版本代码是正常运行,但这次改动后部署就报错,最终定位到新增的一段权限校验逻辑
js
actions:{
query(){
return new Promise(async (resolve, reject) => {
const result = await api1();
const res = await api2()
if (!result.data) {
ElMessageBox({
message: "您无权限访问",
type: "warning",
closeOnClickModal: false,
closeOnPressEscape: false,
callback: (val, action) => {
if (val === "confirm") {
router.back();
}
},
});
}
})
}
}
这段代码中,如果换成Elmessage就不会报错
2.2 定位报错变量来源
buildProp 在 element-plus chunk 中被大量使用,但并未在该文件内定义。查看文件头部导入:
js
// element-plus-CLZbZCGX.js 第1行
import { p as buildProp, q as definePropType, y as buildProps, ... } from "./vue-vendor-vAf0kzTJ.js";
buildProp 是从 vue-vendor chunk 导入的。
2.3 发现反向依赖
继续检查 vue-vendor 的头部导入,发现一个关键的反向引用:
js
// vue-vendor-vAf0kzTJ.js 第2行
import { c as componentSizes } from "./element-plus-CLZbZCGX.js";
vue-vendor 竟然从 element-plus 导入了 componentSizes!这形成了循环依赖:
arduino
vue-vendor ──import { componentSizes }──▶ element-plus
▲ │
└───────export { buildProp }──────────────┘
2.4 追问:vue-vendor 为什么会依赖 element-plus?
vue-vendor 中有一段看似"不该出现"的代码:
js
// vue-vendor-vAf0kzTJ.js 第8787行
const isValidComponentSize = (val) => ["", ...componentSizes].includes(val);
这段代码来自 element-plus/es/utils/vue/validator.mjs,它引用了 componentSizes(定义在 element-plus/es/constants/size.mjs)。
为什么 Element Plus 的工具函数会跑到 vue-vendor chunk 里? 答案就在分包配置中。
2.5 什么是循环依赖
循环依赖就是a引用的b的变量,b引用a的变量导致的问题,报错一般是Cannot access 'buildProp' before initialization
这是es6基础语法const、let关键字报错,就是TDZ(暂时性区域)
js
console.log(a); //报错,TDZ
const a = 1;
而循环依赖为
js
// A.js
import b from "./B.mjs";
// export const a = "Hello from A";
console.log(b); // 这里使用 B 导出的内容
const a = "Hello from A";
export default a;
//B.js
import a from "./A.mjs";
// export const b = "Hello from B";
const b = "b";
export default b;
console.log(a); // 这里使用 A 导出的内容
三、根因分析
3.1 问题配置
vite.config.js 中的 manualChunks 配置如下:
js
manualChunks: (id) => {
if (id.includes("node_modules")) {
// Vue 核心库
if (
id.includes("vue") ||
id.includes("pinia") ||
id.includes("vue-router")
) {
return "vue-vendor";
}
// Element Plus
if (id.includes("element-plus")) {
return "element-plus";
}
// ...
}
};
3.2 匹配顺序导致的误判
关键问题在于 id.includes("vue") 排在 id.includes("element-plus") 前面。
Element Plus 的工具模块路径为 element-plus/es/utils/vue/,这些路径同时包含 "vue" 和 "element-plus" 两个关键词。由于 includes("vue") 先被判断,以下模块全部被错误地归入了 vue-vendor chunk:
| 模块路径 | includes("vue") |
includes("element-plus") |
实际被分到 |
|---|---|---|---|
element-plus/es/utils/vue/props/runtime.mjs |
✅ | ✅ | vue-vendor (先匹配到 "vue") |
element-plus/es/utils/vue/validator.mjs |
✅ | ✅ | vue-vendor (先匹配到 "vue") |
element-plus/es/utils/vue/icon.mjs |
✅ | ✅ | vue-vendor (先匹配到 "vue") |
element-plus/es/utils/vue/install.mjs |
✅ | ✅ | vue-vendor (先匹配到 "vue") |
element-plus/es/constants/size.mjs |
❌ | ✅ | element-plus |
element-plus/es/components/... |
❌ | ✅ | element-plus |
3.3 循环依赖的形成
源码中的依赖关系:
javascript
validator.mjs → import { componentSizes } from '../../constants/size.mjs'
打包后,由于 validator.mjs 被分到了 vue-vendor,而 size.mjs 被分到了 element-plus,就变成了:
javascript
vue-vendor chunk(含 validator.mjs) → import { componentSizes } from element-plus chunk(含 size.mjs)
element-plus chunk(含组件代码) → import { buildProp } from vue-vendor chunk(含 runtime.mjs)
循环依赖形成。
3.4 为什么开发环境不报错?
Vite 开发模式使用的是 原生 ESM 按需加载,每个模块都是独立的文件,浏览器通过 HTTP 请求按需加载,模块之间的依赖关系由浏览器原生处理,不存在"chunk 合并"的概念,因此不会产生循环依赖问题。
而生产环境经过 Rollup 打包后,多个模块被合并到同一个 chunk 文件中,模块的执行顺序由 chunk 文件内的代码书写顺序决定。当循环依赖存在时,某个变量可能在被导入方使用时尚未执行到定义语句,触发 TDZ(暂时性死区)错误。
3.5 报错的执行时序
javascript
1. 浏览器开始加载 vue-vendor chunk
2. vue-vendor 第2行:import { componentSizes } from "element-plus" → 触发加载 element-plus chunk
3. 浏览器开始加载 element-plus chunk
4. element-plus 第1行:import { buildProp } from "vue-vendor" → vue-vendor 已在加载中,获取其未完成的导出
5. element-plus 第627行:执行 const _prop = buildProp({...}) → buildProp 尚未初始化!
❌ Uncaught ReferenceError: Cannot access 'buildProp' before initialization
buildProp 定义在 vue-vendor 的第 8684 行,但 vue-vendor 在第 2 行就触发了 element-plus 的加载。此时 vue-vendor 还没执行到 buildProp 的定义处,element-plus 拿到的是一个处于 TDZ 的 const 变量,访问即报错。
3.6 为什么Elmessage不报错?
因为使用了Elmessage,它不使用isValidComponentSize方法,不使用,构建打包的结果就是不会在vue-vendor当中有引入element-plus当中的componentSizes
整体逻辑为
js
//element-plus
import {isValidComponentSize} from 'vue-vendor'
const componentSizes = ["", "default", "small", "large"];
.....
...
const _sfc_main = defineComponent({
name: "ElMessageBox",
props: {
buttonSize: {
type: String,
validator: isValidComponentSize
},
}
})
//vue-vendor
import {componentSizes} from 'element-plus'
const isValidComponentSize = (val) => ["", ...componentSizes].includes(val);
因为在vendor当中完全不使用isValidComponentSize方法,所以将isValidComponentSize直接合并到element-plus文间当中最优,而element-plus/es/utils/vue/validator.mjs当中有isValidComponentSize,但因为带vue导致送到的vue-vendor。
四、修复方案
4.1 核心改动
element-plus匹配提到vue前面 ,防止element-plus/es/utils/vue/路径被includes("vue")抢走vue匹配更精确 ,用@vue/或路径分隔符限定,避免宽泛匹配
4.2 修复代码
js
// vite.config.js
build: {
rollupOptions: {
output: {
manualChunks: (id) => {
if (id.includes("node_modules")) {
// Element Plus 必须优先匹配
// 避免 element-plus/es/utils/vue/ 被误分到 vue-vendor
if (id.includes("element-plus")) {
return "element-plus";
}
// Vue 核心库 --- 精确匹配,避免误伤其他包含 "vue" 字符串的包
if (
id.includes("@vue/") ||
id.includes("vue/dist") ||
id.includes("pinia/") ||
id.includes("vue-router/")
) {
return "vue-vendor";
}
// ECharts
if (id.includes("echarts")) {
return "echarts";
}
// 工具库
if (
id.includes("axios") ||
id.includes("lodash") ||
id.includes("dayjs")
) {
return "utils";
}
// 图标库
if (id.includes("@element-plus/icons")) {
return "icons";
}
// 其他第三方库
return "vendor";
}
},
},
},
},
4.3 修复后的依赖关系
bash
vue-vendor chunk(仅含 vue/pinia/vue-router) ← 不再包含 element-plus 的任何模块
▲
│
element-plus chunk(含所有 element-plus 代码,包括 utils/vue/ 和 constants/)
循环依赖被打破,buildProp 和 componentSizes 都在同一个 element-plus chunk 内部,不再存在跨 chunk 的循环引用。
五、总结与反思
5.1 经验教训
| 要点 | 说明 |
|---|---|
includes() 是宽泛匹配 |
id.includes("vue") 会匹配到任何路径中包含 "vue" 的模块,包括 element-plus/es/utils/vue/ |
manualChunks 的匹配顺序很重要 |
先匹配的规则先生效,应将更具体的规则放在前面 |
| 开发正常 ≠ 生产正常 | Vite 开发模式使用原生 ESM,不存在 chunk 合并;生产模式经过 Rollup 打包,模块合并后可能暴露循环依赖 |
| 循环依赖的 TDZ 错误具有隐蔽性 | 报错位置往往不是问题根源,需要沿导入链追溯 |
5.2 预防措施
- 分包规则应精确匹配 :使用路径分隔符(如
@vue/、/vue/dist)代替宽泛的字符串包含 - 优先匹配更具体的包 :
element-plus等包含子路径的包应排在通用规则之前 - 构建后验证 :可在 CI 流程中加入循环依赖检测,如使用
rollup-plugin-circular-dependencies或madge - 关注构建警告:Rollup 打包时如果存在循环依赖,通常会输出警告信息,不应忽略
后评
本文根据我询问ai,让ai阅读源码得以解决,当中包括读取到node_modules,不得不感叹ai的强大之处,阅读源码时效率非常高,能快速找到问题出处。
当然,从发现问题到解决问题,不是一次询问,写一次提示词就解决的,不过ai给定的报错存在循环依赖这个判断是没有问题的,但刚开始给的解决方法是有问题的