webpack通过esm封装层桥接cjs与esm混用:将module.exports整体作为命名空间,提升exports.default为export default,并设__esmodule标记;配合resolve.mainfields、esmoduleinterop等配置确保运行时与类型系统一致。

Webpack 在 TypeScript 项目中处理 CommonJS(CJS)与 ECMAScript Module(ESM)混用时,核心不是“自动修复”,而是通过模块解析、运行时包装和编译配置协同作用来桥接差异。冲突往往不发生在 TypeScript 编译阶段(那是 tsc 或 ts-loader 的事),而是在 Webpack 打包后的运行时行为上暴露——比如 import X from 'cjs-pkg' 拿到的是一个带 .default 的对象,而非预期的构造函数或值。
Webpack 如何包装 CJS 模块供 ESM 导入
当你在 TS/ESM 文件中写 import lodash from 'lodash'(而 lodash 是纯 CJS 包),Webpack 不会原样暴露 module.exports。它会做两件事:
- 为该模块生成一个“ESM 封装层”:把
module.exports整体作为命名空间对象,并将exports.default(如果存在)提升为export default; - 若 CJS 模块没有显式设置
exports.default(如module.exports = { a: 1 }),Webpack 仍会添加__esModule: true标记,并让default指向整个module.exports对象。
这就是为什么有时你 import React from 'react' 能成功——React 的 CJS 包虽无 exports.default,但 Webpack 默认把它整个导出对象当作默认导出。
关键配置项:resolve.exportsFields 和 resolve.mainFields
Webpack 决定加载哪个模块入口,依赖 package.json 中的字段优先级。默认配置下:
-
mainFields: ['browser', 'module', 'main']—— 优先找module字段(通常指向 ESM 入口),再 fallback 到main(通常是 CJS); -
exportsFields: ['exports']—— 启用 Node.js 的exports字段映射(更细粒度控制 ESM/CJS 入口)。
如果你发现 Webpack 错误加载了 ESM 版本导致浏览器报错(如 require is not defined),可临时禁用 module 字段:mainFields: ['browser', 'main'],强制走 CJS 入口。
TS 编译层必须配合:esModuleInterop
TypeScript 编译器本身不理解 Webpack 的运行时包装逻辑。若你在 tsconfig.json 中未开启:
-
"esModuleInterop": true—— 它会让 TS 在编译时为 CJS 模块自动生成兼容的default导出包装(类似Object.defineProperty(exports, "default", { value: exports })); -
"allowSyntheticDefaultImports": true—— 仅影响类型检查,不生成代码,建议与esModuleInterop同时启用。
缺了它,TS 可能报 Module has no default export,即使 Webpack 实际能运行。这不是 Webpack 的问题,而是类型系统与运行时脱节。
避免 __esModule 冲突的实战技巧
某些 CJS 库手动设置了 exports.__esModule = true,但内容仍是传统结构(如 exports.foo = ...)。这会导致 Webpack 误判为“伪 ESM”,进而错误处理默认导出。解决方法:
- 在 Webpack 配置中用
resolve.alias指向该库的未标记版本(如有); - 使用
normalizr类插件重写模块导出; - 更稳妥的做法:改用
import * as pkg from 'pkg'显式按命名空间导入,再取pkg.default或pkg.xxx。
不复杂但容易忽略










