Vite拆包后Chunk级别的循环依赖分析及解决方案

当我们的源码中存在隐性的循环引用,在单文件打包(或 Dev 模式)时被 Rollup/Webpack 内部消化解决了;但当你配置了代码分割(Code Splitting),强制把它们拆成不同的 .js 文件(Chunk)时,浏览器在加载这两个相互依赖的文件时就会陷入死锁或引用 undefined

举一个例子: 假设有两个模块 a.tsb.ts,以及它们被打包成的两个 Chunk:ChunkA.jsChunkB.js

  • 场景a 引用了 bb 引用了 a
  • 不拆包时 :它们在一个文件里,Rollup可以通过变量提升(Hoisting)或函数包装来处理这种引用,通常能运行(只要不是在模块顶层立即执行代码)。
  • 拆包后 : 浏览器加载 ChunkA.jsChunkA 头部声明 import ... from 'ChunkB.js'。 浏览器暂停 ChunkA 的执行,去加载 ChunkBChunkB 头部声明 import ... from 'ChunkA.js'。 死锁:ChunkA 还没执行完(没导出东西),ChunkB 就想要它的导出值。于是报错(通常是 ReferenceError 或 undefined)。

如何解决

解决方案一:修改分包策略

既然是因为拆开了才报错,最直接的办法就是强行把这两个冤家合并回同一个包里

你需要修改 vite.config.ts 中的 build.rollupOptions.output.manualChunks

ts 复制代码
// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // id 是文件的绝对路径
          
          // 场景:假设你发现 utils/a.ts 和 utils/b.ts 发生了循环引用
          if (id.includes('src/utils/a') || id.includes('src/utils/b')) {
            return 'utils-merged'; // 强制把它们打包到同一个 utils-merged.js 中
          }

          // 场景:或者是某个特定的库拆分导致的问题
          if (id.includes('node_modules/some-complex-lib')) {
            return 'vendor-lib';
          }
        }
      }
    }
  }
})

原理:只要它们在同一个 .js 文件里,Rollup 就能像以前一样处理它们的内部循环引用,执行就不会报错了

解决方案二:治理"全家桶"导出

这是拆包循环依赖最常见的罪魁祸首:index.ts

  • 场景

src/components/index.ts 导出了所有组件:export * from './Button'; export * from './Modal';

Button.tsx 引用了 Modal:它不想写 ../Modal,而是图方便写了 import { Modal } from '.'; (引用了 index)。

  • 结果index 依赖 ButtonButton 依赖 index

  • 拆包灾难: Vite 可能会把 index.ts 识别为一个公共 chunk,而把 Button 单独拆分。结果 CommonChunk 和 ButtonChunk 互相引用。

  • 修复方法 : 在模块内部引用兄弟模块时,坚决不要通过 index.ts 绕圈子,直接引用文件路径。

ts 复制代码
// ❌ 错误:在 Button.tsx 中引用同级组件
import { Modal } from './index'; // 或者 import { Modal } from '.';

// ✅ 正确:直接引用文件
import { Modal } from './Modal'; 

解决方案三:提取公共依赖(根本解决)

如果是逻辑代码导致的循环(a用到b的常量,b用到a的函数):

做法:创建一个新的文件 c.ts

把 a 和 b 共同需要的部分(类型、常量、纯函数)移动到 c.ts。 a 引用 c,b 引用 c。 循环链条被打断:A -> C <- B。 这样无论 Vite 怎么拆包,c 都会被单独拆出来(或者合并),a 和 b 都不再互相依赖,依赖树变成了有向无环图(DAG)。

如果是拆包导致node包之间的循环,则需要找到对应的包,让其放到公共的vendor中,就是方案一~

怎么分析、快速定位错误

其实这才是写这篇文章的初心,因为这种拆包导致的循环引用是很隐蔽的,打包时甚至不会报错或者显示警告,尤其当你去拆分node_module出现问题时,控制台报错的和真实循环引用可能完全不一样,所以分析起来会很难受,完全靠预览时控制台的报错和经验

通过控制台报错+rollup-plugin-visualizer

查看控制台报错定位具体的代码、然后通过rollup-plugin-visualizer查看生成的包结构,进而排查解决,这种方案可能会考验你对整体打包内容和代码的理解,很可能要靠半猜半试

通过vite插件

尝试过vite-plugin-circular-dependency,发现该插件是在打包前 扫描代码关系的,不能分析chunk的,所以自己写了一个vite-plugin-chunk-cycle-detector(这碟饺子的醋来了~),原理是通过打包模块获取每个chunk的信息,进而得知chunk的引入引出关系,最后通过使用 Kahn 算法检测是否有环。

  • 使用
js 复制代码
//vite.config.ts
import { defineConfig } from 'vite'
import chunkCycleDetectorPlugin from 'vite-plugin-chunk-cycle-detector'

export default defineConfig({ 
  plugins: [
    chunkCycleDetectorPlugin({ 
      circleImportThrowError: true, // 检测到循环时抛出错误中断构建
      showCircleImportModuleDetail: true // 显示具体模块间的依赖关系
    })
  ]
}) 

假如有环则在打包时会输出:

bash 复制代码
⚠️ 检测到 1 个 chunk 间的循环依赖:

  🔄 循环1: chunkA → chunkB → chunkA
     chunkA 中的模块 src/a.js 引用了 chunkB 中的模块 src/b.js
     chunkB 中的模块 src/b.js 引用了 chunkA 中的模块 src/a.js

这样就很方便可以去分析了,(不过注意,该插件目前使用的是vite6版本)

为什么打包时没有错误或者警告

最后聊聊这个最坑的点,因为这是完全符合规范的(Vite/Rollup 的构建逻辑是基于 ESM (ECMAScript Modules) 标准的):

  • 标准允许循环引用 :在 ESM 标准中,模块 A 引用模块 B,模块 B 引用模块 A,这在语法上是完全合法的。浏览器会建立一个引用链接,只要不在模块初始化的瞬间去调用还没准备好的值,就不会报错。
  • 构建器的视角 :Rollup 在拆包时,看到生成了两个 Chunk(文件),它们头部互相 import 对方。对于构建器来说,这是一个合法的 ESM 依赖图,它认为浏览器能搞定,所以它构建成功(Build Success) ,所以连警告都没有

只有当你发布生产包并用浏览器打开页面白屏了你才后知后觉,然后发现控制台的 Cannot access 'SomeComponent' before initialization或者是 Cannot read properties of undefined (reading 'xxx')

相关推荐
于慨20 小时前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz20 小时前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
从前慢丶20 小时前
前端交互规范(Web 端)
前端
CHU72903520 小时前
便捷约玩,沉浸推理:线上剧本杀APP功能版块设计详解
前端·小程序
GISer_Jing20 小时前
Page-agent MCP结构
前端·人工智能
王霸天20 小时前
💥别再抄网上的Scale缩放代码了!50行源码教你写一个永不翻车的大屏适配
前端·vue.js·数据可视化
小领航20 小时前
用 Three.js + Vue 3 打造炫酷的 3D 行政地图可视化组件
前端·github
@大迁世界20 小时前
2026年React大洗牌:React Hooks 将迎来重大升级
前端·javascript·react.js·前端框架·ecmascript
PieroPc20 小时前
一个功能强大的 Web 端标签设计和打印工具,支持服务器端直接打印到局域网打印机。Fastapi + html
前端·html·fastapi
悟空瞎说20 小时前
深入 Vue3 响应式:为什么有的要加.value,有的不用?从设计到源码彻底讲透
前端·vue.js