前言
在使用 Vite + Vue 3 构建项目时,我们遇到了两个典型的构建问题。
问题一:共享组件被错误命名为第一个入口点
问题描述
在生产环境构建后,发现 Organization 组件被打包成了 VisitList.v-10eaf360b4.js,但实际上这个文件包含的是 Organization 组件的代码。这导致运行时出现错误:
arduino
Uncaught (in promise) SyntaxError: The requested module './VisitList.v-10eaf360b4.js' does not provide an export named 'O'
这里不得不吐槽,因为一些原因,我司CDN上没有不允许出现这种同名文件,哪怕哈希值不一样也不行,会默认读第一个文件,其实就算被打包出来多个文件,哈希值不一样的话,正常是不会受影响的。
问题分析
Rollup 的代码分割策略
当使用函数形式的 manualChunks 配置时,Rollup 会根据以下规则进行代码分割:
- 共享模块被多个入口点使用:如果共享模块被 3 个或更多入口点使用,Rollup 会创建一个独立的 chunk
- 共享模块只被 2 个动态导入使用:如果共享模块只被 2 个动态导入的路由组件使用,Rollup 会将其合并到第一个入口点的 chunk 中,并使用第一个入口点的名称
具体场景
在我们的项目中:
Organization组件位于src/components/Organization/Organization.jsx- 被
VisitList和BindClientList两个动态导入的路由组件使用 - Rollup 将其命名为
VisitList.v-xxx.js(第一个入口点的名称)
为什么其他组件没问题?
对比其他组件:
DateSelect:被 4 个组件使用(包括 1 个静态导入),正确命名为DateSelectLocationSheet:被 6 个动态导入的路由组件使用,正确命名为LocationSheet
这说明 Rollup 的默认行为是:只有当共享模块只被 2 个动态导入使用时,才会使用第一个入口点的名称。
解决方案
通过显式指定共享组件的 chunk 名称来解决:
javascript
manualChunks(id) {
// 优先处理 src/components/ 下的共享组件
// 避免只被 2 个动态导入路由组件使用的共享组件被错误命名为第一个入口点名称
if (id.includes('/src/components/') || id.includes('\\src\\components\\')) {
// 提取组件名称(例如:src/components/Organization/Organization.jsx -> Organization)
// 匹配模式:components/ComponentName/ComponentName.jsx
const match = id.match(/[/\\]components[/\\]([^/\\]+)[/\\]\1\.(jsx?|vue)$/);
if (match) {
return match[1]; // 返回组件名称作为 chunk 名称
}
}
// ... 其他配置
}
问题二:循环依赖导致的组件解析失败
问题描述
在生产环境运行时,应该跳转页面,但是在加载组件时报错:
javascript
SurveyPopup.jsx:26 TypeError: Cannot read properties of undefined (reading '__vccOpts')
at vue-router.mjs:2215:55
错误发生在 router.push 尝试加载 SurveyImg 或 SurveyVideo 路由组件时。
问题分析
查看代码结构,代码中形成了循环依赖链
为什么构建时没报错?
- 开发环境:Vite 的 HMR 机制可能掩盖了这个问题
- 构建过程:Rollup 在构建时可能没有完全解析这个循环依赖
- 运行时:当 Vue Router 尝试动态加载组件时,由于循环依赖导致组件解析失败
__vccOpts 错误的含义
__vccOpts 是 Vue 3 编译后的组件选项的内部属性。当组件解析失败时,resolvedComponent 为 undefined,访问 undefined.__vccOpts 就会报错。
解决方案
将常量提取到单独的文件中,打破循环依赖
为什么之前没问题?
- 构建配置变更 :函数形式的
manualChunks改变了 Rollup 的代码分割行为,对循环依赖的处理更加严格 - Vite 版本升级:新版本的 Vite/Rollup 对循环依赖的检测和处理更加严格
最佳实践建议
1. 避免循环依赖
- 分离常量和组件:将常量、工具函数等提取到独立文件
- 使用 barrel exports 时注意 :
index.js只用于导出,不要包含业务逻辑 - 检查工具 :可以使用工具检测循环依赖,如
madge、dependency-cruiser
2. 显式控制代码分割
- 使用 manualChunks:对于重要的共享组件,显式指定 chunk 名称
- 监控构建产物:定期检查构建后的 chunk 文件,确保命名正确
- 文档化:记录哪些组件需要特殊处理
3. 升级时的注意事项
- 测试构建产物:升级构建工具后,务必测试生产环境构建
- 检查变更日志:关注构建工具的变更日志,了解行为变化
- 渐进式升级:不要一次性升级多个依赖
总结
这两个问题都反映了现代前端构建工具的复杂性:
- 代码分割策略:需要理解 Rollup/Vite 的默认行为,并在必要时显式控制
- 循环依赖:虽然构建工具可能不会报错,但会在运行时导致问题
- 版本升级:工具升级可能改变默认行为,需要充分测试