从 Webpack 迁移到 Rspack 后,循环依赖为什么炸了?一个 const vs var 引发的血案

背景

最近在做公司 Electron 项目的构建工具迁移------从 Webpack 切换到 Rspack。Rspack 号称"基于 Rust 的高性能 JavaScript 打包工具",API 与 Webpack 高度兼容,大部分配置可以直接平迁。

迁移过程确实很顺利,配置几乎原样搬过来,构建速度也有了明显提升。但打包后一运行,主进程直接白屏崩溃:

javascript 复制代码
ReferenceError: Cannot access '__rspack_default_export' before initialization

诡异的是,完全相同的业务代码,用 Webpack 打包就没有任何问题。

定位过程

1. 确认循环依赖链

根据报错堆栈,定位到了一条循环依赖链:

bash 复制代码
product.ts → log/index.ts → xbotLog.ts → remoteLog.ts → product.ts

remoteLog.ts 顶层 import 了 product

typescript 复制代码
import product, { isOversea, isPrivate, isStandalone } from '@root/common/product';

product.ts 又通过 log 模块间接依赖了 remoteLog.ts,形成了闭环。

2. 但 Webpack 为什么没事?

这条循环依赖不是新写的,一直存在,Webpack 下跑了很久都没出过问题。两边的配置几乎一模一样:

  • 同样的 babel-loader + @babel/preset-typescript
  • 同样的 splitChunks 配置
  • 同样的 target: 'electron-main'
  • 同样的 output.libraryTarget: 'commonjs'

那差异到底在哪?

3. 对比构建产物

把两边的构建产物拉出来一看,答案就在眼前。

Webpack 生成的模块导出代码:

javascript 复制代码
__webpack_require__.d(__webpack_exports__, {
  "default": () => (__WEBPACK_DEFAULT_EXPORT__)
});
// ...
var __WEBPACK_DEFAULT_EXPORT__ = product;

Rspack 生成的模块导出代码:

javascript 复制代码
__webpack_require__.d(__webpack_exports__, {
  "default": () => (__rspack_default_export)
});
// ...
/* export default */ const __rspack_default_export = (product);

看到了吗?一个用 var,一个用 const

根因:Temporal Dead Zone(暂时性死区)

这不是什么配置差异,而是 JavaScript 语言层面 varconst/let 的本质区别。

var 的行为

javascript 复制代码
console.log(a); // undefined(不报错)
var a = 1;

var 声明会被提升(hoisting) 到函数作用域顶部,变量在声明前就已存在,值为 undefined

const/let 的行为

javascript 复制代码
console.log(b); // ReferenceError: Cannot access 'b' before initialization
const b = 1;

const/let 虽然也会被提升,但在赋值语句执行之前处于暂时性死区(TDZ) ,任何访问都会抛 ReferenceError

循环依赖下的连锁反应

理解了 TDZ,再来看循环依赖场景下发生了什么。

两个打包器都使用 __webpack_require__.d 来注册模块导出,本质是定义了一个 getter:

javascript 复制代码
// 运行时源码(Webpack 和 Rspack 一致)
__webpack_require__.d = (exports, definition) => {
  for (var key in definition) {
    Object.defineProperty(exports, key, {
      enumerable: true,
      get: definition[key]  // ← 这是一个惰性 getter
    });
  }
};

当注册 "default": () => (__rspack_default_export) 时,它只是定义了一个 getter 函数,并不会立即求值 。真正求值发生在别的模块访问 .default 的时候。

循环依赖的执行流程

arduino 复制代码
1. product.ts 开始执行
2.   → import log → log 开始执行
3.     → import xbotLog → xbotLog 开始执行
4.       → import remoteLog → remoteLog 开始执行
5.         → import product → 发现 product 正在执行中,返回当前(未完成的)exports
6.         → 访问 product.default → 触发 getter → 读取 __rspack_default_export
7.         → 💥 const 还没赋值,处于 TDZ → 报错!

如果是 var

markdown 复制代码
6.         → 访问 product.default → 触发 getter → 读取 __WEBPACK_DEFAULT_EXPORT__
7.         → var 已提升,值为 undefined → 不报错,后续 product 执行完毕后值会正确

修复

知道了根因,修复方案就很简单了。Rspack 提供了 output.environment 配置来控制生成代码使用的 ES 特性级别:

javascript 复制代码
// rspack.main.js
module.exports = {
  output: {
    path: outputFilePath,
    filename: '[name].js',
    libraryTarget: 'commonjs',
    environment: {
      const: false,  // 使用 var 代替 const/let
    },
  },
  // ... 其他配置不变
};

设置 environment.const: false 后,Rspack 会将生成代码中所有的 const/let 退化为 var,与 Webpack 行为一致。

修改后重新构建,产物中的:

javascript 复制代码
/* export default */ const __rspack_default_export = (product);

变成了:

javascript 复制代码
/* export default */ var __rspack_default_export = (product);

循环依赖不再报错,问题解决。只改了 3 行配置,零业务代码改动。

思考

谁对谁错?

严格来说,Rspack 的做法更"正确" 。ES Module 规范中,导入绑定(import binding)本身就不应该在模块初始化完成前被访问。使用 const 能让这类隐患在运行时尽早暴露。

而 Webpack 用 var 的做法更"宽容"------循环依赖下虽然不报错,但你拿到的是 undefined,如果代码恰好在初始化阶段就用了这个值去做判断或调用方法,可能会产生更隐蔽的 bug。

治本之道

output.environment.const: false 是一个低成本的兼容方案,但循环依赖本身才是根源。长远来看,更好的做法是:

  1. 延迟引用 :将产生循环的 import 改为函数内 require(),只在真正需要时才加载
  2. 解耦模块:重新组织模块结构,打破依赖环路
  3. 检测工具 :在 CI 中集成 circular-dependency-pluginmadge 等工具,把循环依赖扼杀在 MR 阶段

总结

对比项 Webpack Rspack
default export 声明 var __WEBPACK_DEFAULT_EXPORT__ const __rspack_default_export
循环依赖行为 提升为 undefined,静默通过 TDZ 直接报错
兼容修复 --- output.environment.const: false

从 Webpack 迁移到 Rspack 时,如果遇到 Cannot access '__rspack_default_export' before initialization 错误,大概率是已有的循环依赖被 Rspack 更严格的代码生成方式暴露了出来。加一行 environment.const: false 可以快速恢复,但别忘了回头清理那些循环依赖------它们本来就不应该存在。

相关推荐
审小匠OpenCPAi23 分钟前
银行流水核查怎么自动化?单边匹配、双向勾稽与图聚类异常检测的工程对比
java·前端·人工智能·python·审计
Revolution6127 分钟前
一个公共表格组件,是怎么一步步失控的
前端·前端工程化
腻害兔43 分钟前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:CRM 客户关系模块深度解析——从线索到回款,一套完整的 B2B 销售闭环是怎么搭的?
java·前端·javascript·vue.js·产品经理·ai编程
whyfail1 小时前
前端学 Spring Boot(2):一次点击,如何穿过整个后端?
前端·spring boot·后端
AI多Agent协作实战派1 小时前
AI多Agent协作系统实战(二十三):Agent读HEARTBEAT.md不读AGENTS.md——openclaw的文件加载之谜
前端·数据库·人工智能·uni-app
2601_955760072 小时前
如何用 Claude Opus 5 API 批量扩展长尾关键词和文章选题
前端·python·搜索引擎
KaMeidebaby3 小时前
卡梅德生物技术快报|bli亲和力检测gst:告别批量跑胶:BLI实时酶切监测技术加速GST融合蛋白下游流程优化
前端·网络·数据库·人工智能·算法
触底反弹3 小时前
🚀 删了数据刷新又回来?3 组件 × 4 回调 × 3 坑讲透 React 父子通信
前端·javascript·react.js
耳东小鹿4 小时前
对象常用方法
前端
程序员海军4 小时前
一个 AI 应用开发程序员的一天,都在屏幕前忙些什么?
前端·后端·程序员