开发正常但生产异常的 Bug:Vite manualChunks 循环依赖导致 ReferenceError

环境配置: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 定位报错变量来源

buildPropelement-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 核心改动

  1. element-plus 匹配提到 vue 前面 ,防止 element-plus/es/utils/vue/ 路径被 includes("vue") 抢走
  2. 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/)

循环依赖被打破,buildPropcomponentSizes 都在同一个 element-plus chunk 内部,不再存在跨 chunk 的循环引用。

五、总结与反思

5.1 经验教训

要点 说明
includes() 是宽泛匹配 id.includes("vue") 会匹配到任何路径中包含 "vue" 的模块,包括 element-plus/es/utils/vue/
manualChunks 的匹配顺序很重要 先匹配的规则先生效,应将更具体的规则放在前面
开发正常 ≠ 生产正常 Vite 开发模式使用原生 ESM,不存在 chunk 合并;生产模式经过 Rollup 打包,模块合并后可能暴露循环依赖
循环依赖的 TDZ 错误具有隐蔽性 报错位置往往不是问题根源,需要沿导入链追溯

5.2 预防措施

  1. 分包规则应精确匹配 :使用路径分隔符(如 @vue//vue/dist)代替宽泛的字符串包含
  2. 优先匹配更具体的包element-plus 等包含子路径的包应排在通用规则之前
  3. 构建后验证 :可在 CI 流程中加入循环依赖检测,如使用 rollup-plugin-circular-dependenciesmadge
  4. 关注构建警告:Rollup 打包时如果存在循环依赖,通常会输出警告信息,不应忽略

后评

本文根据我询问ai,让ai阅读源码得以解决,当中包括读取到node_modules,不得不感叹ai的强大之处,阅读源码时效率非常高,能快速找到问题出处。

当然,从发现问题到解决问题,不是一次询问,写一次提示词就解决的,不过ai给定的报错存在循环依赖这个判断是没有问题的,但刚开始给的解决方法是有问题的

相关推荐
用户11481867894844 小时前
Vue 开发者快速上手 Flutter(四)
前端
dreamsever4 小时前
OpenTelemetry可观测系统之Metrics学习
java·前端·学习
Bacon4 小时前
装上就回不去了:CodeGraph 让 AI 编程效率飙升 92%,它到底做了什么?
前端·人工智能·后端
hadeas4 小时前
Spring 技术栈学习文档(面向前端开发者)
前端
狗头大军之江苏分军4 小时前
Python 协程进化史:从 yield 到 async/await 的底层实现
前端·后端
jay神4 小时前
基于YOLOv8的交通标志识别Web系统
前端·人工智能·深度学习·yolo·机器学习·毕业设计
CAD老兵4 小时前
一张 HTML 走天下:CAD-Viewer 首创的「离线 CAD 看图」
前端·javascript·github
程序员榴莲5 小时前
Python 中的 @property:像访问属性一样调用方法
开发语言·前端·python
yingyima5 小时前
Linux定时任务:crontab vs systemd timer,到底谁更适合你的业务?
前端