news 2026/9/8 23:21:05

webpack harmony-interop 示例全解析:ES Modules 与 CommonJS 之间双向互操作原理与实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
webpack harmony-interop 示例全解析:ES Modules 与 CommonJS 之间双向互操作原理与实践

webpack harmony-interop 示例全解析:ES Modules 与 CommonJS 之间双向互操作原理与实践

【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through "loaders", modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack

本篇文章围绕 webpack 官方示例 harmony-interop 展开,完整演示并剖析 ES2015+(Harmony)模块语法与 CommonJS 模块在同一个 bundle 中「双向互操作」的边界行为:ESM 如何以默认导入、命名导入、命名空间导入三种方式引入 CommonJS 模块,CommonJS 又如何反向require一个 ESM 模块并读取其default与命名导出。通过阅读源码模块、编译产物与两种模式下的构建统计信息,你将理解 webpack 的 interop 运行时辅助函数(__webpack_require__.n.d.r)与export *的特殊语义,进而在混合模块体系的真实项目中写出行为正确的跨模块导入代码。

示例总览:一套文件、两条互操作链路

该示例所在目录为 examples/harmony-interop,不依赖任何自定义 loader 或复杂配置,是 webpack 官方 examples 中专门用于说明「模块体系互操作」的最小样例。目录内共 6 个源文件,构成一条环环相扣的导入关系链:

文件模块体系扮演角色
example.jsESM(Harmony)入口,消费 CommonJS 与 reexport 模块
fs.jsCommonJS被 ESM 以三种语法导入;被 reexport
reexport-commonjs.jsESMexport *转发 CommonJS 的导出
example2.jsCommonJS反向require一个 ESM 模块
harmony.jsESM提供default与命名导出,被 CommonJS 消费

目录中的 README.md 是本次讲解的正文主文档,template.md 是该系列示例的渲染模板(其中以_{{xxx}}_占位符的方式按固定顺序嵌入各源码文件与产物 dist/output.js 的代码与构建日志,最终生成带完整运行注释的 README),因此通读 README 即可得到与本示例完整等价的信息。示例在整个 examples 仓库中登记于 examples/README.md 的 Harmony 分组之下,与 harmony(纯 ESM 基础用法)、harmony-library(ESM 打包为库)、harmony-unused(未被使用导出的剔除)互为补充,分别回答互操作链路上的不同子问题。

ESM 导入 CommonJS:三种语法、同一种访问语义

入口模块 example.js 先演示了第一个方向的互操作——「Harmony 模块导入 CommonJS 模块」。目标模块 fs.js 是一个非常朴素的 CommonJS 模块,核心仅一行导出:

// fs.js —— 一个典型的 CommonJs 模块 exports.readFile = function() {}; // 使用 module.exports 写法也是等价的, // webpack 不关心你用的是哪种语法

示例在入口中同时使用 ES Modules 的三种导入语法,把同一个 CommonJS 模块分别接进来:

// example.js —— harmony 模块 import fs from "./fs"; // 默认导入 import { readFile } from "./fs"; // 命名导入 import * as fs2 from "./fs"; // 命名空间导入 fs.readFile("file"); // 经默认导入访问 readFile("file"); // 经命名导入直接访问 fs2.readFile("file"); // 经命名空间导入访问

值得先明确的关键点是:webpack 处理这三种语法的结果完全相同——import fsimport { readFile }import * as fs2都指向同一个 CommonJS 模块的exports对象。也就是说,对一个 CommonJS 模块而言,fsfs2.readFile、命名导入readFile只是同一批运行时属性的三种取用方式,不会像 ESM 那样产生「默认导出是独立实体」的语义差别。fs.js 源码中的注释也点明了这一点:exports.readFilemodule.exports = {...}等价、AMD 模块与 CommonJS 模块等价,webpack 在解析时对它们一视同仁。

从编译产物的运行时视角可以更清楚地看到这种「统一」:dist 产物中该模块被包装成以__unused_webpack_module, exports为参数的模块函数,exports.readFile = ...原样执行(见 examples/harmony-interop/README.md 中 fs.js 模块编号 1 的代码注释default exports/readFile [provided]),而入口中三种导入最终被编译为:

_fs__WEBPACK_IMPORTED_MODULE_0__.readFile("file"); // import fs → fs.readFile (0,_fs__WEBPACK_IMPORTED_MODULE_0__.readFile)("file"); // import { readFile } → 直接调用 _fs__WEBPACK_IMPORTED_MODULE_0__.readFile("file"); // import * as fs2 → fs2.readFile

三行调用全部收敛到同一个_fs__WEBPACK_IMPORTED_MODULE_0__命名空间对象上,证明在 webpack 的模块实现里,CommonJS 的exports天然就是它的「默认导出 + 命名导出 + 命名空间」三位一体。

当默认导入遇到 CommonJS:__webpack_require__.n的兼容取法

上面的入口里import fs from "./fs"之所以能直接以fs.readFile("file")调用成功,依赖于 webpack 注入的运行时辅助函数。示例文档展示了名为compat get default export的 runtime 模块——即__webpack_require__.n

// webpack/runtime/compat get default export // 为兼容非 harmony 模块而提供的 getDefaultExport 函数 __webpack_require__.n = (module) => { const getter = module && module.__esModule ? () => (module['default']) : // ESM 模块:取 default 属性 () => (module); // CommonJS 模块:整体就是默认导出 __webpack_require__.d(getter, { a: getter }); return getter; };

这段逻辑精确地编码了 interop 的判定规则:如果被导入模块带有__esModule标记(说明它原本是 ESM 或被 ESM 化处理过),默认导出取module.default;否则把整个 CommonJSexports对象当作默认导出返回。由于 fs.js 是纯 CommonJS、没有__esModule,因此import fs from "./fs"拿到的就是 fs.js 的整个exportsfs.readFile自然可调。

而在同一份产物中,import fs from "./example2"(一个 CommonJS 的副作用导入)则被编译为:

var _example2__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_example2__WEBPACK_IMPORTED_MODULE_2__);

/*#__PURE__*/注释用于告知压缩器该调用无副作用、在结果未被使用时可以安全删除,这也是为何 Pure ESM 的import "./example2"纯副作用导入在开启压缩后可能被完全消除。可见 webpack 在编译期就把「该用什么 interop 取法」固化进了产物代码里,运行期不再做模块体系判断。

CommonJS 反向 require ESM:为什么读的是.default与命名属性

示例还覆盖了相反方向:CommonJS 模块 example2.js 反向require一个 Harmony 模块 harmony.js:

// example2.js —— CommonJs 模块 var module = require("./harmony"); // require 一个 harmony 模块 var defaultExport = module.default; // ESM 的 default 以属性形式暴露 var namedExport = module.named; // 命名导出同样以属性形式暴露

harmony.js 的源码只有两行:

// harmony.js —— 只是几个导出 export default "default"; export var named = "named";

那么 CommonJS 侧为什么能通过module.default取到 ESM 的默认导出?看产物中 harmony.js 的编译结果便知,webpack 给该模块追加了"use strict"声明,并注入两个运行时调用:

__webpack_require__.r(__webpack_exports__); // 打上 __esModule 与 Symbol.toStringTag 标记 __webpack_require__.d(__webpack_exports__, { // 定义可枚举的 getter "default": () => (__WEBPACK_DEFAULT_EXPORT__), named: () => (/* binding */ named) });

配合文档中给出的两个运行时辅助函数实现(见 examples/harmony-interop/README.md 的/* webpack runtime code */折叠块):

  • __webpack_require__.d(exports, definition):遍历definition,对exports上尚不存在的每个 key 用Object.defineProperty定义enumerable: true的 getter——这就是 ESM 命名导出「以属性形式暴露」的底层机制;
  • __webpack_require__.r(exports):给exports打上Symbol.toStringTag = 'Module'__esModule = true标记,使模块在运行期可被识别为 ESM 命名空间。

借助这两个函数,ESM 的default导出在运行期就是一个名为default的普通可枚举属性,CommonJS 侧通过require得到整个命名空间后,直接读取module.default/module.named即可。这也是互操作能「双向」成立的根基:webpack 用运行时属性统一了两种模块体系的导出承载形式,而非在语法层面做特殊适配

export *转发 CommonJS:一条值得单独记住的边界规则

第三个模块 reexport-commonjs.js 演示的是 ESM 通过export *转发一个 CommonJS 模块的导出:

// reexport-commonjs.js —— reexport 一个 CommonJs 模块 export * from "./fs"; // 注意:default 导出不会经由 export * 被再导出 // (这不是 interop 特有行为,而是所有 export * 的通用规则) // // 注意:reexport 一个 CommonJS 模块是特殊场景, // 因为在该模块中我们没有任何关于其导出的信息

注释本身即是本小节最重要的两条结论,结合产物可进一步解读:

  1. export *永远不转发default。正如注释所强调的,这并非 interop 特例,而是 ES 规范层面export *的通用语义——无论被转发的是 ESM 还是 CommonJS 模块,default都不在其中。若下游需要默认导出,必须显式地import x from "./fs"; export default x;
  2. 转发 CommonJS 是「无信息」的静态再导出。产物中 reexport-commonjs.js 被编译为仅携带单条导出的命名空间对象:
__webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, { readFile: () => (/* reexport safe */ _fs__WEBPACK_IMPORTED_MODULE_0__.readFile) }); /* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./fs */ 1);

注意产物头部注释的措辞:reexport safe(重导出安全)与missing usage info prevents renaming(缺少使用信息导致无法重命名)。这正对应源码注释中的「no information about exports」——由于 fs.js 是 CommonJS,静态分析无法获知其确切的导出集合,因此 webpack 采用_fs__WEBPACK_IMPORTED_MODULE_0__.readFile这种「转发到运行期再取值」的访问方式(模块间建立的是运行时依赖而非静态的确定性绑定),同时也放弃了在该链路上对导出名的压缩重命名优化。相比之下,harmony、harmony-unused 等纯 ESM 示例中的命名导出由于信息完整,可以享受更激进的静态分析与 tree-shaking。

入口模块对 reexport 模块的使用同样印证了这点——import { readFile as readFile2 } from "./reexport-commonjs"在产物中被编译为(0, _reexport_commonjs__WEBPACK_IMPORTED_MODULE_1__.readFile)("file"):外层括号 +0,前缀是 ESM 命名空间属性调用的标准编译形态,它保证函数体内的this不会被隐式绑定到命名空间对象上,与前面命名导入 CommonJS 的调用编译结果完全一致,说明对 webpack 而言「命名空间对象上的属性调用」路径统一,不区分来源模块体系。

构建产物结构解读:模块缓存、模块数组与运行时分工

示例文档附带了完整的 dist/output.js 产物(webpack 版本较新的生成形态),从中可以梳理出一个 bundle 的骨架与 harmony-interop 四个模块 + runtime 的落位:

  • 模块数组__webpack_modules__[0]保留为空槽(webpack 保留),[1]fs.js、[2]reexport-commonjs.js、[3]example2.js、[4]harmony.js 按依赖拓扑依次入槽;
  • 模块缓存与加载函数__webpack_require__:维护__webpack_module_cache__,命中缓存直接返回cachedModule.exports,否则创建{ exports: {} }执行模块函数后返回,这是 CommonJSrequire语义在 bundle 内的忠实重现(产物注释自述no module.id needed / no module.loaded needed,该版本精简了字段);
  • 四个 runtime 辅助函数__webpack_require__.n(兼容默认导出取法)、__webpack_require__.d(定义导出 getter)、__webpack_require__.ohasOwnProperty简写)、__webpack_require__.r(标记 ESM 命名空间)。其中n内部复用了do,形成清晰的依赖链;
  • 入口模块:由于以import编写的入口被识别为 ESM 命名空间,产物注释说明「This entry needs to be wrapped in an IIFE because it needs to be in strict mode.」——即入口以 IIFE +"use strict"包裹,开头即执行__webpack_require__.r(__webpack_exports__),随后按声明顺序执行各__webpack_require__调用完成模块加载与副作用执行。

每个模块头部都有编译期生成的注释块(如 fs.js 标注export readFile [provided]missing usage info prevents renaming;example2.js 标注unknown exports (runtime-defined);harmony.js 标注namespace exports),这些注释直接服务于可调试性,也向后端链路(如 Stats、代码覆盖率工具)提供结构化信息。

构建两种模式对比:Unoptimized 与 Production 的统计差异

示例文档在末尾给出了同一示例在两种模式下的构建统计。其具体构建方式与其他 examples 相同:在仓库根目录执行yarnyarn setup并安装webpack-cli后,进入示例目录运行node build.js(全量构建可用npm run build:examples,逻辑见 examples/buildAll.js,它会遍历 examples/examples.js 扫描出的全部含template.md的目录并串行执行构建)。模板中的_{{stdout}}__{{production:stdout}}_占位符会被替换为两种模式的 stdout 内容(参见 template-common.js 的占位符替换逻辑),因此 README 中呈现的是真实构建输出。文档中版本号被统一归一化为webpack X.X.X

Unoptimized(开发/默认模式)输出

asset output.js 6.99 KiB [emitted] (name: main) chunk (runtime: main) output.js (main) 1.13 KiB (javascript) 883 bytes (runtime) [entry] [rendered] > ./example.js main dependent modules 785 bytes [dependent] 4 modules runtime modules 883 bytes 4 modules ./example.js 374 bytes [built] [code generated] [no exports] [used exports unknown] entry ./example.js main webpack X.X.X compiled successfully

Production(生产模式)输出

asset output.js 895 bytes [emitted] [minimized] (name: main) chunk (runtime: main) output.js (main) 1.13 KiB (javascript) 991 bytes (runtime) [entry] [rendered] > ./example.js main dependent modules 484 bytes [dependent] 3 modules runtime modules 991 bytes 3 modules ./example.js + 1 modules 675 bytes [built] [code generated] [no exports] [no exports used] entry ./example.js main webpack X.X.X compiled successfully

对比这两份统计可以得到互操作链路上的关键量化事实:

  1. 产物体积相差近 8 倍(6.99 KiB → 895 bytes)。生产模式下资源被最小化,模块注释、可读性包装与冗余 runtime 被压缩剔除;runtime modules体积反而从 883 字节增至 991 字节,说明 minimizer 对运行时代码的处理包含一定的封装成本,但总收益仍极为显著。
  2. 「+ 1 modules」的合并:生产模式下入口由独立模块变为./example.js + 1 modules的合并展示,dependent modules由 4 个降为 3 个,反映压缩阶段对纯副作用导入import "./example2"的处理——由于未被使用且可能被判定为无保留价值,example2.js 的载荷被合并/折叠,这正是 Compilation 在 production 目标下自动启用FlagDependencyUsagePluginSideEffectsFlagPlugin等优化后的结果。
  3. 静态信息量对比:开发模式标注[used exports unknown](未启用 usage 分析),生产模式标注[no exports used](usage 分析认定入口无导出被外部使用)。这个变化说明即便存在 CommonJS↔ESM 互操作,webpack 依然会对整个依赖图执行导出使用分析——只是对export *转发这类「无导出信息」的链路,分析会保守地保留运行期取值,而不会冒险删除属性访问。

总结:互操作规则的实践清单

将本示例的全部事实收敛为可在真实混合工程中直接套用的清单:

  • ESM 侧导入 CommonJS:默认导入、命名导入、命名空间导入最终都指向同一份exports;想整体拿到 CommonJS 模块,默认导入是最自然的表达,其底层由__webpack_require__.n依据__esModule标记分派取法。
  • CommonJS 侧 require ESM:ESM 模块经__webpack_require__.r+__webpack_require__.d被「物化」为带default与各命名属性的命名空间对象,CommonJS 只需按属性名读取即可。
  • export *的边界:永远不会转发default(ES 通用语义);转发 CommonJS 时因静态信息缺失,webpack 退化为运行期转发(reexport safe),并放弃该路径上的压缩重命名优化。
  • 生产模式行为差异:usage/sideEffects 分析同样作用于混合依赖图,副作用导入可能被合并或删除,产物经压缩后可缩小近一个数量级。
  • 进一步阅读:harmony 与 harmony-unused 可看纯 ESM 的静态优化上限;mixed 可看 CommonJS 与 AMD 的混用;dll 与 harmony-library 则展示了 ESM 语法在库打包与 DLL 场景下的形态。

【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through "loaders", modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 23:20:50

2026大模型学习路线图:从零基础到项目实战,程序员小白必看指南,存一下吧很难找全的!_ai大模型学习路线图

本文提供AI大模型系统学习路线,分四个阶段:入门阶段掌握Python、数学基础及机器学习;中级阶段深入学习算法并实践项目;进阶阶段学习自然语言处理、计算机视觉等;高级阶段探索深度强化学习和生成模型。文章还包含学习资…

作者头像 李华
网站建设 2026/9/8 23:20:15

免费抓包嗅探下载视频号与抖音资源:res-downloader 快速上手指南

免费抓包嗅探下载视频号与抖音资源:res-downloader 快速上手指南 【免费下载链接】res-downloader 视频号、小程序、抖音、快手、小红书、直播流、m3u8、酷狗、QQ音乐等常见网络资源下载! 项目地址: https://gitcode.com/GitHub_Trending/re/res-downloader …

作者头像 李华
网站建设 2026/9/8 23:17:49

Isaac Lab 3.0.0实战:齿轮插入任务Sim2Real迁移全攻略

把机械臂从仿真搬到真实产线,中间隔着的那道鸿沟到底有多大,没做过的人真的很难体会。我见过太多团队,仿真里跑得飞快的策略,一上真机就像丢了魂一样——位姿偏一点就怼不进去,光照变一下就抓不准孔位,摩擦…

作者头像 李华
网站建设 2026/9/8 23:15:53

用DOM与getComputedStyle提取网站设计风格,生成DESIGN.md规范

1. 为什么我需要从别人的网站里“扒”出设计风格先说个场景:我去年接手一个老项目,客户指着隔壁竞品的官网说“就照这个风格改,但我也说不清具体是啥风格”。和客户反复确认的过程非常痛苦——他说“高级感”,你理解为深色&#x…

作者头像 李华
网站建设 2026/9/8 23:12:15

HTML+CSS+JS+jQuery+Bootstrap响应式动态展示网站模板实战拆解

简介:这是一份融合HTML、CSS、JavaScript、jQuery与Bootstrap的完整响应式网站模板,适合前端初学者系统学习,也适合开发者作为项目起步的基底。资源围绕“活力旅程”主题,包含首页、关于、服务、作品集、联系等典型页面&#xff0…

作者头像 李华