1. 问题背景从 Webpack 到 Rspack 的迁移陷阱最近在将一个大型 Electron 项目从 Webpack 构建工具迁移到 Rspack 时遇到了一个令人困惑的问题。项目在 Webpack 下运行良好但切换到 Rspack 后主进程启动时直接白屏崩溃报错信息显示ReferenceError: Cannot access __rspack_default_export before initialization这个错误特别令人费解因为业务代码完全没有改动配置几乎是原样从 Webpack 迁移过来的Webpack 构建的版本运行完全正常作为团队的技术负责人我花了整整两天时间深入排查这个问题最终发现这竟然是一个由const和var声明差异引发的血案。下面我将详细分享这个问题的定位过程、根本原因和解决方案。2. 问题定位抽丝剥茧的过程2.1 确认循环依赖链首先根据报错堆栈我定位到了一条循环依赖链product.ts → log/index.ts → xbotLog.ts → remoteLog.ts → product.ts具体来说remoteLog.ts文件顶部引入了productimport product, { isOversea, isPrivate, isStandalone } from root/common/product;而product.ts又通过log模块间接依赖了remoteLog.ts形成了一个完整的循环依赖闭环。2.2 Webpack 为何能正常运行这个循环依赖并不是新引入的它在 Webpack 构建环境下已经存在并运行了很长时间。我仔细对比了两边的配置同样的babel-loaderbabel/preset-typescript配置相同的splitChunks优化策略一致的target: electron-main目标环境相同的output.libraryTarget: commonjs输出格式配置几乎一模一样为什么 Webpack 能正常运行而 Rspack 会报错2.3 构建产物对比分析为了找出差异我决定对比两者的构建产物。这个决定最终揭示了问题的真相。Webpack 生成的模块导出代码__webpack_require__.d(__webpack_exports__, { default: () (__WEBPACK_DEFAULT_EXPORT__) }); // ... var __WEBPACK_DEFAULT_EXPORT__ product;Rspack 生成的模块导出代码__webpack_require__.d(__webpack_exports__, { default: () (__rspack_default_export) }); // ... /* export default */ const __rspack_default_export (product);关键区别一目了然Webpack 使用var声明导出变量而 Rspack 使用const。这个看似微小的差异正是导致问题的根本原因。3. 根本原因Temporal Dead Zone暂时性死区3.1 var 与 const/let 的本质区别这不是简单的打包工具配置差异而是 JavaScript 语言层面var和const/let的本质区别。var 的行为console.log(a); // 输出 undefined不会报错 var a 1;var声明会被提升hoisting到函数作用域顶部变量在声明前就已存在只是值为undefined。const/let 的行为console.log(b); // 抛出 ReferenceError: Cannot access b before initialization const b 1;const和let虽然也会被提升但在赋值语句执行之前处于暂时性死区Temporal Dead ZoneTDZ任何访问都会抛出ReferenceError。3.2 循环依赖下的连锁反应理解了 TDZ 后我们就能解释循环依赖场景下发生了什么。两个打包器都使用__webpack_require__.d来注册模块导出其本质是定义了一个 getter// 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的时候。3.3 循环依赖执行流程详解让我们详细看看循环依赖情况下的执行流程product.ts开始执行→ 遇到import log→log/index.ts开始执行→ 遇到import xbotLog→xbotLog.ts开始执行→ 遇到import remoteLog→remoteLog.ts开始执行→ 遇到import product→ 发现product正在执行中返回当前未完成的exports 对象→ 访问product.default→ 触发 getter → 尝试读取__rspack_default_export→ const还未赋值处于 TDZ → 抛出 ReferenceError如果是var声明→ 访问product.default→ 触发 getter → 读取__WEBPACK_DEFAULT_EXPORT__→var已提升值为undefined→ 不会报错后续product执行完毕后值会正确更新4. 解决方案从临时修复到根本解决4.1 快速修复方案Rspack 提供了output.environment配置来控制生成代码使用的 ES 特性级别// 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 行为一致。修改后的构建产物中/* export default */ const __rspack_default_export (product);变成了/* export default */ var __rspack_default_export (product);这样修改后循环依赖不再报错问题得到解决。整个过程只改了 3 行配置无需任何业务代码改动。4.2 谁对谁错技术选择的哲学严格来说Rspack 的做法更正确。ES Module 规范中导入绑定import binding本身就不应该在模块初始化完成前被访问。使用const能让这类隐患在运行时尽早暴露。而 Webpack 用var的做法更宽容——循环依赖下虽然不报错但你拿到的是undefined。如果代码恰好在初始化阶段就使用了这个值去做判断或调用方法可能会产生更隐蔽、更难调试的 bug。4.3 根本解决方案虽然output.environment.const: false是一个低成本的兼容方案但循环依赖本身才是问题的根源。从长远来看我们应该延迟引用将产生循环的 import 改为函数内require()只在真正需要时才加载模块解耦重新组织模块结构打破依赖环路检测工具在 CI 中集成circular-dependency-plugin或madge等工具把循环依赖扼杀在代码审查阶段5. 技术对比与经验总结5.1 Webpack 与 Rspack 行为对比对比项WebpackRspackdefault export 声明var __WEBPACK_DEFAULT_EXPORT__const __rspack_default_export循环依赖行为提升为undefined静默通过TDZ 直接报错兼容修复—output.environment.const: false5.2 迁移经验与最佳实践不要假设兼容性即使号称高度兼容的工具在实现细节上也可能有重要差异重视构建产物分析当遇到诡异问题时直接对比构建产物往往能快速定位问题理解语言特性深入理解 JavaScript 的语言特性如 TDZ能帮助解决看似复杂的问题技术债务管理循环依赖等技术债务可能在工具迁移时突然暴露要建立机制预防和消除5.3 性能与稳定性的权衡Rspack 基于 Rust 实现确实带来了显著的构建性能提升。在我们的项目中完整构建时间从 Webpack 的 45 秒降低到了 18 秒热更新速度也有明显改善。但这次经历提醒我们在追求性能的同时也不能忽视稳定性和兼容性问题。6. 扩展思考前端构建的未来6.1 工具生态的演进Rspack 代表了前端构建工具的新一代尝试它试图在 Webpack 的生态基础上通过 Rust 实现获得性能提升。类似的工具还有Vite基于原生 ES Modules 和 RollupTurbopackWebpack 作者的新项目同样基于 Rustesbuild极快的 JavaScript 打包器6.2 模块系统的设计考量这次问题本质上是模块系统设计的问题。现代前端开发中我们需要更加注意模块的初始化顺序循环依赖的危害静态分析与动态导入的结合6.3 团队协作的建议对于正在进行构建工具迁移的团队我建议逐步迁移不要一次性全量切换可以先在非关键路径试用建立监控对构建产物进行差异对比和运行时监控知识共享组织内部技术分享让团队成员理解工具差异7. 实操指南如何安全迁移到 Rspack基于这次经验我总结了一个安全迁移到 Rspack 的步骤指南7.1 迁移前准备在项目中安装circular-dependency-plugin检测现有循环依赖使用webpack-bundle-analyzer分析现有构建结构建立性能基准构建时间、产物大小等7.2 配置迁移从简单配置开始逐步添加复杂功能特别注意output.environment相关配置对比 Webpack 和 Rspack 的构建产物差异7.3 测试策略单元测试确保基本功能正常集成测试验证模块交互E2E 测试检查运行时行为7.4 监控与优化监控生产环境的运行时错误持续优化构建配置定期检查并消除循环依赖8. 常见问题与解决方案8.1 迁移后出现奇怪的运行时错误问题现象代码在 Webpack 下正常但在 Rspack 下报错解决方案检查是否是循环依赖导致的问题对比构建产物找出行为差异尝试调整output.environment配置8.2 构建速度没有明显提升可能原因配置不正确没有充分利用 Rspack 的优势项目结构特殊某些功能在 Rspack 中效率不高解决方案检查是否启用了持久化缓存尝试逐步禁用插件找出性能瓶颈参考 Rspack 官方性能优化指南8.3 某些 Webpack 插件不兼容解决方案检查 Rspack 官方文档看是否有替代方案联系插件作者询问兼容性计划考虑自己实现简化版功能9. 个人经验与反思在这次迁移过程中我深刻体会到几个重要的工程实践原则理解底层原理如果不了解 TDZ 的概念这个问题可能会困扰我们更长时间工具不是银弹即使像 Rspack 这样的新工具也有自己的特点和限制技术债务迟早要还循环依赖这样的问题越早解决成本越低我还发现在技术决策过程中我们需要在多个维度进行权衡性能 vs 稳定性开发体验 vs 长期维护成本新技术的收益 vs 迁移风险最后对于正在考虑从 Webpack 迁移到 Rspack 的团队我的建议是做好充分准备但不必因噎废食。Rspack 确实带来了显著的性能提升只要我们理解它的特点就能充分发挥其优势。
阅读完成 · 觉得有帮助?