从 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 可以快速恢复,但别忘了回头清理那些循环依赖------它们本来就不应该存在。

相关推荐
yuanyxh2 小时前
macOS 应用 - 纯对话生成
前端·macos·ai编程
大家的林语冰2 小时前
ES5 凉凉,Babel 8 正式发布,默认不再编译为 ES5 和 CJS......
前端·javascript·前端工程化
光影少年3 小时前
react批量更新、同步/异步更新场景
前端·react.js·掘金·金石计划
假如让我当三天老蒯3 小时前
模块化:ES Module 与 CommonJS 的区别
前端·面试
用户40950115773173 小时前
Private Forge v2.0 发布:12大前端业务场景技能系统
前端
weedsfly4 小时前
异步编程全景与事件循环——彻底搞懂 JS 执行机制
前端·javascript
用户059540174464 小时前
AI Agent记忆测试踩坑实录:Mock骗了我一周,Mem0+pytest一招破局
前端·css
用户1733598075374 小时前
纯前端 PDF 数字签名实战:Vue 3 + pdf-lib 在浏览器里完成签名嵌入
前端·javascript
IT_陈寒5 小时前
SpringBoot自动配置的坑,我爬了三天才出来
前端·人工智能·后端
Avan_菜菜11 小时前
AI 能写代码了,为什么我反而开始要求它先写文档?
前端·github·ai编程