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

相关推荐
miss2 小时前
JavaScript 异步循环完全指南:从踩坑到最佳实践
前端
山_雨2 小时前
前端重连机制
前端
Cache技术分享2 小时前
355. Java IO API -去除路径中的冗余信息
前端·后端
牛马1112 小时前
Flutter CustomPaint
开发语言·前端·javascript
炽烈小老头2 小时前
函数式编程范式(三)
前端·typescript
ruoyusixian2 小时前
chrome二维码识别查插件
前端·chrome
fengfuyao9852 小时前
一个改进的MATLAB CVA(Change Vector Analysis)变化检测程序
前端·算法·matlab
yuhaiqiang3 小时前
为什么这道初中数学题击溃了所有 AI
前端·后端·面试
djk88883 小时前
支持手机屏幕的layui后台html模板
前端·html·layui