深入 webpack harmony 示例:ES Module 从源码到打包产物的完整编译解析
本篇指南以本仓库 examples/harmony 示例为核心,讲解 webpack 如何把采用 ES Module(webpack 内部称之为 “harmony modules”)语法编写的多个源文件,编译、链接并打包成一个浏览器可执行的 bundle,同时演示基于 import() 的按需异步加载(Code Splitting)。读完本文,你将能读懂 webpack 生成的 harmony 模块产物结构、模块级静态分析注释([provided] / [used exports])、运行时助手函数(__webpack_require__.d / .r / .e)的含义,并理解同一源码在 development 与 production 两种模式下产物与模块合并策略的差异。
一、harmony 示例的组成与"文档模板"生成机制
examples/harmony/ 目录下的全部源文件如下:
| 文件 | 作用 |
|---|---|
| examples/harmony/example.js | 入口模块:以命名导入方式消费同步模块,并触发一个异步模块的动态加载 |
| examples/harmony/increment.js | 中间层模块:从 math 导入 add,再对外导出 increment |
| examples/harmony/math.js | 叶子模块:导出一个可接收任意参数的 add 求和函数 |
| examples/harmony/async-loaded.js | 仅通过 import() 异步加载的模块,导出 answer = 42 |
| examples/harmony/webpack.config.js | 示例专用最小配置,仅固化 chunk 命名策略 |
| examples/harmony/README.md | 由模板渲染生成的最终文档(含产物源码与构建统计) |
| examples/harmony/template.md | 文档模板:通过占位符嵌入源文件、构建产物与 stats 输出 |
这里有一个值得注意的工程细节:template.md 本身并不直接书写源码和输出内容,而是使用 _{{example.js}}_、_{{stdout}}_ 这类占位符引用磁盘上的真实文件与实际构建结果。仓库的 examples/build-common.js 会读取每个示例目录下的 template.md,依次用三种模式跑真实构建(见 examples/build-common.js):
const compilations = [
["--mode production --env production", "production"],
["--mode development --env development --devtool none", "development"],
["--mode none --env none --output-pathinfo verbose", ""]
];
每次编译完成后由 examples/template-common.js 的 replaceResults 将输出回填到模板中,最终写为 README.md。因此 examples/harmony/README.md 里呈现的代码与 stats,都是这一示例在仓库真实版本下的实况记录,具有天然的可复现性与时效性。全仓库的示例均可通过 examples/examples.js 的递归扫描与 examples/buildAll.js 批量构建。
webpack.config.js 中只做了一件事:optimization.chunkIds: "deterministic",其注释说明了原因——“保持不同模式(例如仅构建时)下的文件名一致”,这使得后文 development / production 两套 stats 中的 655.output.js 命名可以互相对照。
二、模块依赖链:一条最典型的 ESM 使用链路
示例的入口 examples/harmony/example.js 完整代码如下:
import { increment as inc } from './increment';
var a = 1;
inc(a); // 2
// async loading
import("./async-loaded").then(function(asyncLoaded) {
console.log(asyncLoaded);
});
它在同一份文件里覆盖了 ES Module 的两种最常见形态:
- 静态(顶层)导入:
import { increment as inc } from './increment',使用了as别名语法,且在调用处inc(a)直接以局部标识符使用; - 动态导入:
import("./async-loaded")返回 Promise,配合.then()在运行期按需加载,这是 webpack Code Splitting 最基础的写法。
被同步引用的中间模块 examples/harmony/increment.js:
import { add } from './math';
export function increment(val) {
return add(val, 1);
};
叶子模块 examples/harmony/math.js:
export function add() {
var sum = 0, i = 0, args = arguments, l = args.length;
while (i < l) {
sum += args[i++];
}
return sum;
}
而 examples/harmony/async-loaded.js 只做一件事:
export var answer = 42;
依赖拓扑非常清晰:
example.js (entry)
├── increment.js ──┬── math.js (同步依赖,合并进主 chunk)
└── async-loaded.js (动态依赖,切分为独立异步 chunk)
math.js 的 add 故意写成遍历 arguments 的可变参数形式,不引用任何 webpack 运行时依赖,属于纯函数模块,是观察 ESM 编译与后续 production 模式模块合并的理想素材。
三、逐段拆解构建产物 dist/output.js:harmony 模块如何被翻译
template.md 的第三节标题即为 dist/output.js,它展示的是入口 chunk 的真实产物。产物主体是一个 IIFE 包裹的 webpackBootstrap:
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ([
/* 0 */,
/* 1 */
/*!**********************!*\
!*** ./increment.js ***!
\**********************/
/*! namespace exports */
/*! export increment [provided] [no usage info] [missing usage info prevents renaming] */
/*! other exports [not provided] [no usage info] */
/*! runtime requirements: __webpack_require__, __webpack_require__.r, __webpack_exports__, __webpack_require__.d, __webpack_require__.* */
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ increment: () => (/* binding */ increment)
/* harmony export */ });
/* harmony import */ var _math__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./math */ 2);
function increment(val) {
return (0,_math__WEBPACK_IMPORTED_MODULE_0__.add)(val, 1);
};
从这段代码能观察到 webpack 对 ESM 编译的几个核心事实:
- 模块表
__webpack_modules__是一个数组:/* 0 */空位保留给运行时占位(通常编号 0 会被保留),./increment.js占据/* 1 */,./math.js占据/* 2 */; - 每个 harmony 模块以 CommonJS 风格的三参函数承载:
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => {...},ES Module 语义在内部被翻译为对exports对象的读写; - 导出被转换成 getter:先调用
__webpack_require__.r(__webpack_exports__)给exports打上__esModule与Symbol.toStringTag标记(见产物中 runtime 段make namespace object),再用__webpack_require__.d通过Object.defineProperty定义可枚举的惰性 getterincrement: () => (/* binding */ increment),保持导出与模块内变量的实时绑定关系,这与原生 ESM 的 live binding 语义一致; - 命名导入被改写为长限定名访问:
import { add } from './math'被编译成var _math__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./math */ 2),调用点add(val, 1)变成(0,_math__WEBPACK_IMPORTED_MODULE_0__.add)(val, 1)——外层括号保证add被调用时其this为undefined,忠实还原 ESM 导入函数调用时无 this 绑定的语义; - 模块头部的注释区块是静态分析元信息:
namespace exports、export increment [provided] [no usage info]、other exports [not provided]等标注,来自构建 CLI 中追加的--stats-reasons --stats-used-exports --stats-provided-exports(见 examples/build-common.js)。[provided]表示导出存在且已登记,[no usage info]表示当前分析尚未收集到消费方信息(入口场景下属正常),[not provided]则标记了该模块未提供的导出(防止外部错误访问)。
3.1 入口 chunk 的执行代码:同步调用 + 异步加载并存
在 webpack runtime 与模块表之后,产物末尾是入口模块自身的执行片段:
let __webpack_exports__ = {};
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk.
(() => {
/*!********************!*\
!*** ./example.js ***!
\********************/
/*! namespace exports */
/*! exports [not provided] [no usage info] */
/*! runtime requirements: __webpack_require__, __webpack_require__.r, __webpack_exports__, __webpack_require__.e, __webpack_require__.* */
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _increment__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./increment */ 1);
var a = 1;
(0,_increment__WEBPACK_IMPORTED_MODULE_0__.increment)(a); // 2
// async loading
__webpack_require__.e(/*! import() */ 655).then(() => (__webpack_require__(/*! ./async-loaded */ 3))).then(function(asyncLoaded) {
console.log(asyncLoaded);
});
})();
这里能清楚看到静态 import 与动态 import() 的产物差异:
- 静态导入被内联为模块表项
__webpack_require__(/*! ./increment */ 1),运行时同步返回其 exports; - 动态导入则被改写为
__webpack_require__.e(655).then(() => __webpack_require__(3)):先用 runtime 的ensure chunk(__webpack_require__.e)去加载编号为655的异步 chunk,加载完成后再__webpack_require__(3)取回其中的./async-loaded模块。整个.then()链保持了源码中 Promise 风格的调用约定; - 入口被单独包进一个 IIFE,注释明确解释了原因:需要与 chunk 中其他模块隔离作用域。
3.2 加载异步 chunk 的 runtime 链路
产物中与按需加载配套的 runtime 代码逐项印证了 chunk 的加载机制(完整代码见 examples/harmony/README.md 中的 <details> runtime 区块):
__webpack_require__.e = (chunkId) => Promise.all(...):ensure chunk的统一入口,负责聚合各 chunk 加载方式(jsonp、import 脚本、模块联邦等)返回的 Promise;__webpack_require__.u = (chunkId) => (chunkId + ".output.js"):chunk 文件名解析函数,因此异步 chunk 实际请求的是655.output.js;__webpack_require__.l:通过创建<script>标签(script.src = url)加载远程脚本,并处理 120 秒超时、错误/加载完成回调去重与内存清理;__webpack_require__.f.j+webpackJsonpCallback:JSONP 方式的 chunk 加载实现,installedChunks用0 / Promise / undefined三个状态区分“已加载 / 加载中 / 未加载”,加载成功后把新模块合并进__webpack_require__.m模块表并 resolve 对应 Promise;__webpack_require__.p = "dist/":publicPath,与构建命令行参数--output-public-path "dist/"(examples/build-common.js)对应。
3.3 异步 chunk 产物 655.output.js
依据 Info 节的构建统计,异步 chunk 文件为 655.output.js,其来源与导出被标注为:
chunk (runtime: main) 655.output.js 24 bytes [rendered]
> ./async-loaded ./example.js 6:0-24
./async-loaded.js 24 bytes [built] [code generated]
[exports: answer]
[used exports unknown]
import() ./async-loaded ./example.js 6:0-24
> ./async-loaded ./example.js 6:0-24 是该 chunk 的“起源”记录——即入口文件第 6 行第 0–24 列处的 import() 调用。由于异步 chunk 的内容要到真正被加载时才执行,webpack 对它的导出使用信息无法预先确定,因此标注为 [used exports unknown];而同步依赖的 math.js 标注同样是 [used exports unknown],因为它经由 increment.js 间接引用,是否还有别的消费方未知。
四、同一源码、两种模式:Info 小节解读
template.md 最后的 Info 节给出了两个版本的构建统计,分别是未优化(development)与 production 模式。
Unoptimized(development)
asset output.js 11.3 KiB [emitted] (name: main)
asset 655.output.js 761 bytes [emitted]
chunk (runtime: main) 655.output.js 24 bytes [rendered]
> ./async-loaded ./example.js 6:0-24
./async-loaded.js 24 bytes [built] [code generated]
[exports: answer]
[used exports unknown]
import() ./async-loaded ./example.js 6:0-24
chunk (runtime: main) output.js (main) 400 bytes (javascript) 5.34 KiB (runtime) [entry] [rendered]
> ./example.js main
runtime modules 5.34 KiB 8 modules
dependent modules 225 bytes [dependent] 2 modules
./example.js 175 bytes [built] [code generated]
[no exports]
[used exports unknown]
entry ./example.js main
webpack X.X.X compiled successfully
要点:
- 主 bundle
output.js11.3 KiB,其中业务 JavaScript 仅 400 字节,其余约 5.34 KiB 是 8 个 runtime 模块(模块缓存、JSONP chunk 加载等运行时基础代码); - 同步依赖的
increment.js与math.js被归入dependent modules 225 bytes [dependent] 2 modules; - 入口
example.js标注[no exports],因为它只导入而不导出任何内容,[used exports unknown]说明出口侧尚未形成可用的裁剪结论,这正是 development 模式不做激进优化的表现。
Production mode
asset output.js 2.01 KiB [emitted] [minimized] (name: main)
asset 655.output.js 121 bytes [emitted] [minimized]
chunk (runtime: main) 655.output.js 24 bytes [rendered]
> ./async-loaded ./example.js 6:0-24
./async-loaded.js 24 bytes [built] [code generated]
[exports: answer]
import() ./async-loaded ./example.js + 2 modules ./example.js 6:0-24
chunk (runtime: main) output.js (main) 400 bytes (javascript) 5.34 KiB (runtime) [entry] [rendered]
> ./example.js main
runtime modules 5.34 KiB 8 modules
./example.js + 2 modules 400 bytes [built] [code generated]
[no exports]
[no exports used]
entry ./example.js main
webpack X.X.X compiled successfully
与 development 模式对比,production 的关键差异一目了然:
| 对比维度 | Unoptimized(development) | Production |
|---|---|---|
output.js 体积 |
11.3 KiB | 2.01 KiB(minimized) |
655.output.js 体积 |
761 bytes | 121 bytes(minimized) |
| 同步模块的 stats 呈现 | dependent modules 225 bytes [dependent] 2 modules |
./example.js + 2 modules 400 bytes(已合并且可被摇树) |
| 导出使用标注 | [used exports unknown] |
[no exports used] |
| 异步 chunk 构建来源 | ./async-loaded.js |
./async-loaded ./example.js + 2 modules(合并且穷举了引用链) |
两处 [no exports used] 是 production 模式下 tree shaking 可以安全工作的信号:当 webpack 确认入口不需要对外导出、模块间引用链完整可达时,未被使用的导出(包括 async-loaded 中并未被消费的 answer)才有资格被压缩器移除。再叠加 scope hoisting(./example.js + 2 modules 的呈现方式表明多个模块已被提升合并且重命名空间化,这正是产品代码从 11.3 KiB 骤降到 2.01 KiB 的重要原因之一,配合压缩后主 chunk 约压缩 82%)。用户侧业务 JavaScript 均为 400 bytes 的一致数值,也印证了 chunkIds: "deterministic" 让两种模式下文件名保持稳定的配置意图。
五、动手重现该示例
整个示例构建链路依赖 webpack-cli 与本仓库源码,且 README.md 是由脚本实时渲染生成的:
- 在仓库根目录安装依赖并确保能解析到
webpack-cli(examples/build-common.js 中做了显式的require.resolve("webpack-cli")校验,缺失时会抛出Please install webpack-cli at root.); - 依次执行 examples/buildAll.js 中的模式对每个含
template.md的示例目录执行node build.js构建; - 每个示例实际执行的底层命令由 examples/build-common.js 拼装,例如 harmony 示例会以
node ../bin/webpack.js附带--entry ./example.js --output-filename output.js --stats-reasons --stats-used-exports --stats-provided-exports --output-public-path "dist/" --mode production/development等参数跑三遍(对应 examples/build-common.js 的参数常量),并把 stdout 清洗掉时间戳、版本号等易变信息(替换为XXXX:XX:XX、webpack X.X.X)后回填模板。
动手实验建议:修改 example.js 中的导入(例如去掉 as inc 别名改为直接导入、把 math 的调用挪到 async-loaded.js 中),观察 examples/harmony/template.md 渲染出的产物中对应注释(runtime requirements、[provided] / [used exports])与 chunk 归属如何随之变化,是理解 webpack ESM 编译与依赖图分析最直接的方式。
结语
examples/harmony 示例以最小但完整的模块图覆盖了 webpack 处理 ES Module 的全部关键链路:命名导入导出与别名的编译、live binding 的 getter 化翻译、静态与动态导入的差异化处理、异步 chunk 的切分与 JSONP 加载,以及 development / production 两种模式下模块分析信息([provided] / [used exports unknown] / [no exports used])与体积变化的对比。配合其 README.md 中保存的真实产物与 template.md 的模板化生成机制,它是理解“ESM 源码 → webpack 中间表示 → 可执行 bundle”这一过程的最短可行路径,也是研究 tree shaking 与 Code Splitting 行为的绝佳出发点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00