webpack 原生 ES Module 输出实战:从 examples/module 剖析 output.module 库构建
【免费下载链接】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
导读
本文以当前仓库的 examples/module 示例文档为主线,完整讲解如何用 webpack 将多个 ES Module 源文件打包为单个可直接被现代浏览器/运行时以原生import消费的 ES 模块产物,并在其中使用“重新导出、跨模块共享可变状态、库方式导出”等典型手法。读完本文,你将掌握output.module、experiments.outputModule、output.library.type: "module"的配置关系与适用前提,并通过 development/production 两套真实产物与构建统计信息,理解 webpack 内部的作用域提升(scope hoisting)、导出分析(usedExports)和压缩优化究竟做了什么。
1. 示例文档与源码的关系
examples目录是 webpack 官方仓库自带的“最小可运行教学单元”:每个子目录通常由一组源文件、一份 template.md(或直接是渲染后的 README.md)与一份webpack.config.js组成。其中 examples/module/template.md 是生成最终 README 的文档骨架,文件内使用_{{example.js}}_这类占位符,由示例构建工具链在执行时把真实源码内容、真实构建产物与真实 stats 回填进去,最终渲染为可供阅读的 examples/module/README.md。
因此,本文以渲染后的 examples/module/README.md 与目录中的真实源码、配置为准展开;仓库根目录的 examples/buildAll.js 即批量驱动这些示例执行的入口脚本。examples/module目录只包含以下五个文件:
- example.js:入口模块(entry),同时充当“库入口”;
- methods.js:对
counter的二次导出 + 工具函数; - counter.js:维护可变状态并暴露增、减、重置方法的模块;
- webpack.config.js:让产物成为原生 ES 模块的核心配置;
- README.md / template.md:本文所述的配套说明文档。
2. 三个源文件:ES Module 之间如何协作
整个示例的逻辑规模很小,却精准覆盖了模块化的三个关键形态。
2.1 counter.js:模块级可变状态
counter.js 内部维护一个模块作用域变量:
export let value = 0; export function increment() { value++; } export function decrement() { value--; } export function reset() { value = 0; }关键点:value是export let,它允许其他模块通过“引用共享”读取最新值,而decrement在本例中虽被导出却从未被使用——这正好为后文讲解usedExports的“未被引用的导出不会进入产物”埋下伏笔。
2.2 methods.js:命名重导出的中间层
methods.js 演示了 ES Module 的**重导出(re-export)**语法:
export { reset as resetCounter } from "./counter"; export const print = value => console.log(value);它把counter.js的reset改名成resetCounter重新导出,同时自身定义print。这种“门面模块”写法在大型应用中用于收敛内部 API、防止深层路径直接泄漏。
2.3 example.js:入口,先消费再外放
example.js 既是 webpack 的入口,也通过具名导出把自己变成“可被外部 import 的库”:
import { increment as inc, value } from "./counter"; import { resetCounter, print } from "./methods"; print(value); inc(); inc(); inc(); print(value); resetCounter(); print(value); export { inc, print };它演示了三条链路:直接读取并修改counter的状态、经由methods间接重置状态、以及把inc与print作为库对外导出。由于模块级value在多次调用间持续累积,输出依次为0、3、0。
3. webpack.config.js:让产物成为原生 ES 模块
3.1 完整配置与注释
examples/module/webpack.config.js 是全文的灵魂配置:
"use strict"; /** @type {import("webpack").Configuration} */ const config = { output: { module: true, library: { type: "module" } }, optimization: { usedExports: true, concatenateModules: true }, experiments: { outputModule: true } }; module.exports = config;3.2 三个关键开关的源码级关系
从配置校验与默认值代码可以看出三者必须配合使用:
experiments.outputModule: true(开关总闸):它是实验性特性的“总开关”。在 lib/config/defaults.js 中可以看到该选项会按 target 自动默认开启(D(experiments, "outputModule", universal || deno || bun),即 target 为 universal、deno、bun 时为true);而一旦开启,运行时代码与产物将按“ES 模块环境”来生成(lib/WebpackOptionsApply.js 中据此选择module-import模块类型等)。output.module: true(开启 ESM 输出):表示产物使用import/export语法输出。默认值同样由实验开关推导,见 lib/config/defaults.js:F(output, "module", () => Boolean(outputModule))。同时在 lib/WebpackOptionsApply.js 中有明确校验——只有在experiments.outputModule开启时,output.module: true、library.type: "module"等才被允许,否则 webpack 会直接抛出配置校验错误:'output.module: true' is only allowed when 'experiments.outputModule' is enabledlibrary type "module" is only allowed when 'experiments.outputModule' is enabledoutput.library.type: "module"(决定如何暴露库):声明最终产物是一个“以模块方式暴露导出”的库——即 bundle 末尾会把入口模块的export { inc, print }原样保留为整个文件对外部消费方暴露的具名导出,这也是浏览器<script type="module">与 ESM 打包工具能够直接import { inc } from "output.js"的原因。
3.3 optimization 两项优化为何被显式打开
usedExports: true:启用导出使用情况分析。webpack 会标记每个导出是否被引用,未被使用的导出(如本示例的decrement)将不会被打包进产物。它默认只在 production 模式打开(lib/config/defaults.js 中D(optimization, "usedExports", production)),此处显式开启是为了让 development 构建也体现该行为。concatenateModules: true:启用模块作用域提升 / scope hoisting,把相互内联安全的多模块合并进同一个函数作用域,减少运行时开销。它同样默认仅 production 开启(lib/config/defaults.js)。对应的插件是 lib/optimize/ModuleConcatenationPlugin.js,在 lib/WebpackOptionsApply.js 中按该开关注册。
适用前提提醒:
output.module产物面向原生支持 ESM 的环境(现代浏览器、Deno、Bun,或 Node 的.mjs/"type":"module"场景);如果需要兼容旧浏览器或 CJS 生态,应改用library.type: "commonjs2"或"umd"等,不要开启output.module。
4. 产物逐行解读:development(未压缩)输出
示例文档展示的dist/output.js(development、未压缩,775 bytes)是被concatenateModules深度合并后的结果。剥离文件头注释后,产物核心如下:
;// ./counter.js let value = 0; function increment() { value++; } function decrement() { value--; } function counter_reset() { value = 0; } ;// ./methods.js const print = value => console.log(value); ;// ./example.js print(value); increment(); increment(); increment(); print(value); counter_reset(); print(value); export { increment as inc, print };这份产物可以提炼出四个值得记住的实现事实:
- 模块边界被“拍平”:三个文件被拼进同一作用域,没有
__webpack_require__包装函数,模块间依靠提升后的函数/变量直接互相引用——这就是 scope hoisting 的直接体现。产物头部的块注释(如./example.js + 2 modules)说明这是把 1 个入口与 2 个伴随模块合并出的一个 chunk。 - 命名冲突通过改写解决:
counter.js的reset在合并作用域中被改名为counter_reset(),example.js里原属于methods.js重导出链上的resetCounter调用也被内联成了对counter_reset()的直接调用,证明重导出在合并后已被消除、链路完全扁平化。 decrement确实消失了:虽然counter.js声明并导出了decrement,但它在整个依赖图里无人引用,usedExports分析将其剔除——对应产物顶部的导出注解export decrement [not provided](未提供)。- 入口导出被原样保留:文件末尾的
export { increment as inc, print };正是output.library.type: "module"的工作成果——开发模式下命名都保持可读(inc、print、counter_reset),产物块注释中还能看到 webpack 对每个导出可读性的分析([provided] [used in main] [could be renamed])。
5. 产物逐行解读:production(压缩)输出
同一份源码在 production 下被压缩后只剩110 bytes:
let o=0;function n(){o++}const c=o=>console.log(o);c(o),n(),n(),n(),c(o),o=0,c(o);export{n as inc,c as print};对照 development 产物可以看清压缩与 tree shaking 的边界:
- 局部标识符全部被压缩为单字母(
value→o、increment→n、print→c); value的累加、打印、重置这些必须执行的顶层副作用语句被原样保留,但改成了紧凑的逗号表达式序列;export { n as inc, c as print }是唯一不能省略的语法——因为库对外契约要求保留inc、print两个具名导出,所以哪怕调用链再短,n与c这两个声明也必须留下被导出。这正是“库模式下导出成为优化边界”的典型例证:普通应用的未使用导出会被整棵删除,而库的公开导出是硬性 API,必须存活。
6. 构建统计(Info)解读
README 同时给出了两种模式下的构建统计,可以对照产物验证优化效果:
Unoptimized(development)
asset output.js 775 bytes [emitted] [javascript module] (name: main) chunk (runtime: main) output.js (main) 453 bytes [entry] [rendered] > ./example.js main ./example.js + 2 modules 453 bytes [built] [code generated] [exports: inc, print] [all exports used] entry ./example.js main used as library export webpack X.X.X compiled successfullyProduction mode
asset output.js 110 bytes [emitted] [javascript module] [minimized] (name: main) chunk (runtime: main) output.js (main) 453 bytes [entry] [rendered] > ./example.js main ./example.js + 2 modules 453 bytes [built] [code generated] [exports: inc, print] [all exports used] entry ./example.js main used as library export webpack X.X.X compiled successfully两段输出传递了同一条重要信息:chunk 的源码量都是453 bytes(./example.js + 2 modules),即无论哪种模式,webpack 都需要解析并处理这三个文件;差异全部发生在产物阶段——development 产物 775 bytes、production 经压缩后 110 bytes(相差约 86%)。此外注意两点:
[javascript module]与[minimized]:production 下产物被标记为“javascript module”且经过压缩;[exports: inc, print]与[all exports used]:说明入口模块的对外导出只有inc、print,且因library.type: "module"的存在,所有导出都被视为“已使用”(used as library export)——这正是库入口不会被 tree shaking 误删的保证,与第 5 节看到的产物中export语句必须存活互为印证。
7. 如何在本地运行与验证
仓库的 examples 通常不需要额外脚手架即可复现。在仓库根目录(即本仓库所在目录)执行:
# 使用示例自带的 webpack 配置,将 examples/module 的入口打包为 ES 模块产物 npx webpack --config examples/module/webpack.config.js构建完成后,检查生成的dist/output.js,即可看到与第 4、5 节一致的产物形态。若要观察两种模式的差异,可分别为配置注入 mode(例如--mode development与--mode production),对比产物体积与是否出现export语句、以及decrement是否被 tree-shaking 移除。若想快速理解 scope hoisting 前后差异,也可临时把配置中optimization.concatenateModules改为false重新构建,产物会退回为带__webpack_require__包装的传统形态,与合并形态形成直观对照。
关于 examples 的整体机制:仓库根目录的 examples/examples.js 维护示例清单,examples/buildAll.js 会逐个进入目录执行node build.js用于 CI/文档生成;对examples/module而言,其核心教学价值就在于第 3 节那份 18 行配置与第 4、5 节的产物对照。
8. 小结
通过 examples/module 这一最小闭环,可以完整看到“ESM 源码 → scope hoisting 合并 → usedExports 裁剪 → 库导出保留 → 压缩输出”的全过程:
| 关注点 | 结论 |
|---|---|
| 如何输出原生 ESM | 开启experiments.outputModule,并同时设置output.module: true |
| 如何把 bundle 当库用 | 设置output.library.type: "module",产物末尾保留具名export |
| 未使用导出去哪了 | usedExports分析将其标记并从产物删除(本例decrement) |
| 多模块合并成什么样 | concatenateModules拍平模块边界、改写冲突命名(本例reset→counter_reset) |
| 库导出为何能存活 | 库模式将所有公开导出视为 used,压缩阶段也无法删除硬性 API |
| 适用环境 | 原生支持 ESM 的运行时/浏览器;传统环境需换用 commonjs/umd 类库类型 |
这套“开发模式读得懂、生产模式压得狠、导出契约不丢失”的组合拳,正是 webpack 面向现代 ESM 生态输出库与应用的推荐形态。若需继续深入,可对照阅读 lib/WebpackOptionsApply.js 中相关校验逻辑、lib/config/defaults.js 中的默认值推导,以及 lib/optimize/ModuleConcatenationPlugin.js 的实现细节。
【免费下载链接】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),仅供参考