首页
/ webpack 混合模块系统实战:在同一个打包中协同使用 CommonJS、AMD 与 ES6(Harmony)模块

webpack 混合模块系统实战:在同一个打包中协同使用 CommonJS、AMD 与 ES6(Harmony)模块

2026-09-07 12:18:57作者:何举烈Damon

webpack 的历史使命之一就是充当不同模块规范之间的“粘合剂”。本指南以仓库中的 examples/mixed 官方示例(template.md,其渲染产物为 README.md)为骨架,完整演示如何在同一个项目、甚至同一个文件中混用 CommonJS、AMD 与 ES6 Modules(webpack 内部称为 Harmony),并解析混用动态拼接的 AMD 依赖数组时 webpack 如何自动生成 Context Module 与异步代码分割 chunk。读完你将理解三种模块格式的解析路径、混合 require 的产物形态,以及如何亲手构建并解读该示例。

1. 示例要解决什么问题

在传统前端工程中,你经常会遇到“历史包袱与现代化并存”的代码库:老代码用 requiremodule.exports,第三方库以 AMD define 暴露接口,新写的模块则是标准 ESM 的 import/exportexamples/mixed 示例就是为了说明:webpack 允许你在任意模块文件里混用任意(受支持的)模块格式,且它们之间可以互相依赖

该示例的目录结构非常精简:

  • example.js:入口,先做同步静态 require,再发起 AMD 风格的动态 require;
  • amd.js:以 AMD define 编写的模块;
  • commonjs.js:以 CommonJS module.exports 编写的模块;
  • harmony.js:以 ESM import/export default 编写的模块;
  • webpack.config.js:最小配置;
  • dist/:构建产物(示例模板注释中列出的 output.js 与异步 chunk 文件)。

正如 examples/README.md 的目录表中对该示例的注记——"demonstrating mixing CommonJs and AMD"——它的教学重点是跨格式互相调用动态依赖的分割

2. 先读入口:三种模块风格的同屏混用

入口文件 example.js 一共只做了两件事:

// CommonJs-style requires
var commonjs1 = require("./commonjs");
var amd1 = require("./amd");
var harmony1 = require("./harmony");

// AMD-style requires (with all webpack features)
require([
	"./commonjs", "./amd",
	"../require.context/templates/"+amd1+".js",
	Math.random() < 0.5 ? "./commonjs" : "./amd"],
	function(commonjs2, amd2, template, randModule) {
		// Do something with it...
	}
);

前半段是“CommonJS 风格的静态 require”:它用 require() 依次加载 CommonJS、AMD、Harmony 三种格式的模块,演示了同步 require 可以命中任意模块格式——webpack 编译时静态分析 require("./amd"),将该依赖登记进模块图,运行时通过内部的 __webpack_require__ 统一取出模块导出。

后半段是“AMD 风格的 require”,也是本例最精华的部分。它使用 require([...], callback) 的数组语法声明异步依赖,而数组中同时出现了三类元素:

  1. 普通的静态模块路径:"./commonjs""./amd"
  2. 带变量的动态路径"../require.context/templates/" + amd1 + ".js",其中 amd1 来自顶部同步 require 到的 AMD 模块导出;
  3. 条件表达式Math.random() < 0.5 ? "./commonjs" : "./amd",运行期才可能确定的分支。

对 webpack 而言,require([...]) 语法意味着“这一段依赖不阻塞首屏,可以按需异步加载”,其回调函数要等到依赖全部就绪后才执行——这正是现代 Code Splitting 的雏形表达。而第 2、3 项的存在,会迫使 webpack 在编译期做“猜路径”的额外工作,见第 4 节。

3. 三种模块格式的写法与互相调用

3.1 CommonJS 模块:内部还能嵌 AMD require

commonjs.js 用最标准的 CommonJS 方式导出:

// CommonJs Module Format
module.exports = 123;

// but you can use amd style requires
require(
	["./amd", "./harmony"],
	function(amd1, harmony) {
		var amd2 = require("./amd");
		var harmony2 = require("./harmony");
	}
);

值得注意两点:其一,顶部直接对 module.exports 赋值导出数字 123,因此构建产物里会出现一行 CommonJS bailout: module.exports is used directly 的注解(见 dist/output.js 中 commonjs 模块的注释),表示这种写法会让静态导出分析在此“退出”,模块导出只能由运行时决定;其二,在 CommonJS 文件内部又发起了一次 AMD 风格的异步 require,且回调里还嵌套着 CommonJS 同步 require——说明混合并不限于“入口文件”,而是发生在任意模块内部。

3.2 AMD 模块:具名 define,依赖可以来自任意格式

amd.js 采用带模块名的 AMD 写法:

// AMD Module Format
define(
	"app/amd", // anonym is also supported
	["./commonjs", "./harmony"],
	function(commonjs1, harmony1) {
		// but you can use CommonJs-style requires:
		var commonjs2 = require("./commonjs");
		var harmony2 = require("./harmony");
		// Do something...
		return 456;
	}
);

它声明依赖 ["./commonjs", "./harmony"]——即一个 AMD 模块可以依赖 CommonJS 模块和 Harmony 模块。define 的模块名 "app/amd" 是可选的,注释说明匿名(anonymous)define 同样受支持。工厂函数返回值 456 会成为模块的导出。

3.3 Harmony(ES6)模块:语义最清晰的默认导出

harmony.js 是标准 ESM:

// ES6 Modules
import commonjs from "./commonjs";
import amd from "./amd";

export default 456;

它用 import 引入两个不同格式的依赖。这里正是 webpack 互操作(interop)发挥作用的地方:./commonjsmodule.exports = 123 形式导出,而 ./amd 的导出是 AMD 工厂函数的返回值。Harmony 模块的默认导入如何与这两种非 ESM 导出对齐?从产物中可以看到,webpack 为这类兼容场景引入了运行时助手 __webpack_require__.n(即 “compat get default export”,位于 lib/runtime 目录下的运行时模块):若目标模块带 __esModule 标记则取其 default,否则把整个 module 当作默认导出,再以 __webpack_require__.d 挂上 getter。

4. 动态 require 的编译魔法:Context Module 的诞生

回到入口那行最容易让人困惑的依赖:

"../require.context/templates/"+amd1+".js"

amd1 是运行时才能确定的值,webpack 在编译期无法静态解析出具体文件。此时 webpack 的应对策略是生成一个 Context Module(上下文模块):把该目录下所有满足匹配规则的文件全部纳入打包,并维护一张“请求路径 → 模块 id”的映射表,运行时再按实际拼接出的路径去查表加载。这与 examples/require.contextrequire("./templates/" + templateName) 的原理同源。

产物里,这个 context 被单独拆成了一个异步 chunk:dist/require_context_templates_sync_recursive_js_.output.js。其名字一眼即可读解:

  • 目录范围:../require.context/templates/
  • 匹配规则:sync ^\.\/.*\.js$,即以 ./ 开头、以 .js 结尾的同步 context;
  • 内容:templates/a.jstemplates/b.jstemplates/c.js 三个模板(见 examples/require.context/templates)。

生成的 Context Module 代码结构是固定的——先定义一张 map,把每个可能的请求路径映射到模块 id:

const map = {
	"./a.js": 5,
	"./b.js": 6,
	"./c.js": 7
};

再提供 webpackContext(真正加载模块)、webpackContextResolve(路径解析并抛出 MODULE_NOT_FOUND)、webpackContext.keys(枚举所有可加载路径)等辅助函数,最终 module.exports = webpackContext。有了这张表,"../require.context/templates/"+amd1+".js" 在执行期查到 ./a.js./b.js./c.js,就能立刻定位并加载对应模块。模板模块自身同样是普通 CommonJS(module.exports = function() { ... }),再次印证“context 内的模块也遵循普通模块规则”。

顺带一提,数组里的 Math.random() < 0.5 ? "./commonjs" : "./amd" 是一个“二选一”的条件依赖。从产物的统计信息看(amd require context ./example.js 7:0-14:1),webpack 只对无法枚举出有限集合的动态部分(模板目录)建了 context;而对于条件分支中的两个静态可解析路径,webpack 在编译期就已将两者都登记为依赖——运行时不管随机数落到哪个分支,所需模块都已在 bundle 中就绪,只是回调会拿到不同的模块导出。

5. 产物解剖:三类模块如何被“归一化”

构建生成的 dist/output.js 最能说明 webpack 的模块归一化能力:所有模块被塞进同一个 __webpack_modules__ 数组,每个模块统一被包装成形如 (module, exports, __webpack_require__) => { ... } 的函数,差异只在包装头。

  • **CommonJS 模块(id 1)**几乎原样保留 module.exports = 123;内部的 AMD 风格 require([...], callback) 被改写为 Promise.resolve(...).then(...)'catch',即异步依赖变成 Promise 链,出错时走 __webpack_require__.oe(unhandled error 通道)。

  • **AMD 模块(id 2)**的 define("app/amd", [...], factory) 被重写为一个 IIFE 形态:

    !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! ./commonjs */ 1), __webpack_require__(/*! ./harmony */ 3)],
      __WEBPACK_AMD_DEFINE_RESULT__ = (function(commonjs1, harmony1) { ... }).apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__),
      __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
    

    依赖数组被替换成同步的 __webpack_require__ 调用序列,工厂函数返回值若不为 undefined 便回写 module.exports——AMD 就此被“降维”成普通 CommonJS 语义。模块注释中的 AMD bailout: define() prevents static exports analysis 也说明:define 的包裹方式会阻止静态导出分析,其导出被标记为“运行时定义”。

  • **Harmony 模块(id 3)**被打上 "use strict",先以 __webpack_require__.r 标记 __esModule,用 __webpack_require__.d 定义 default 导出属性,对 CommonJS 依赖则套上 __webpack_require__.n 包装(default interop),再原样保留 export default 456

产物末尾还完整保留了 webpack runtime(含 module cache、__webpack_require____webpack_require__.e 异步 chunk 加载、__webpack_require__.l 动态创建 <script>、JSONP 回调等)。__webpack_require__.e 的实现说明异步依赖最终会走 JSONP chunk 加载:output.publicPath 在此被推导为 "dist/",配合 chunkFilename 拼出 require_context_templates_sync_recursive_js_.output.js 的 URL。运行时模块本身来自 lib/runtime 目录下 38 个可独立组合的运行时模块(目录 lib/runtime),webpack 按产物实际需要按需注入,这也是为什么该示例 runtime 只有 5.61 KiB / 9 modules

6. 驱动 AMD require 解析的底层插件

跨格式混用的能力并非巧合,而是 webpack 解析管线中若干 parser 插件协作的结果。以 AMD require 为例,其编译期行为由 lib/dependencies/AMDRequireDependenciesBlockParserPlugin.js 承担:它识别 require([...], callback) / define(...) 调用,将依赖数组逐项转换为 AMDRequireItemDependency(静态项)或 AMDRequireContextDependency(含变量的动态项),并把整段依赖挂成一个 AMDRequireDependenciesBlock(异步依赖块,即后来拆出独立 chunk 的最小单位)。回调函数的参数会被解析器走 inFunctionScope 收进依赖块的作用域,从而保证回调内再出现的同步 require 也能被正确归属。产物统计中 > ./example.js 7:0-14:1 这一行,正是该插件登记异步块的证据:它精确指向 example.js 中 AMD require 调用的行号区间。

同理,CommonJS、Harmony 的解析分别由 CommonJsImportsParserPluginHarmonyImportDependencyParserPlugin 等负责,它们统一产出 Dependency 对象进入 ModuleGraph,最终由 JavascriptModulesPlugin 等生成代码。不同格式在“依赖发现”阶段各有各的插件,但在“模块执行”阶段全部收敛到同一条 __webpack_require__ 通道上。

7. 最小配置与运行方式

本示例的 webpack.config.js 简洁到只有一条优化配置:

"use strict";

/** @type {import("webpack").Configuration} */
const config = {
	optimization: {
		chunkIds: "named" // To keep filename consistent between different modes (for example building only)
	}
};

module.exports = config;

注释解释了唯一的目的:固定 chunk 的命名方式,使不同模式(如只做 development 构建)下异步 chunk 的文件名保持一致,方便示例文档展示与对比。这里没有写 entry,因为默认约定即为当前目录下的 example.js(webpack 的默认入口约定);output.pathchunkFilename 等也均由 webpack 默认值与示例构建脚本注入。

构建该示例可参考 examples/README.md 中 "Building an Example" 的通用流程:

  1. 在仓库根目录执行 yarn 安装依赖;
  2. 执行 yarn setup 完成初始化;
  3. 执行 yarn add --dev webpack-cli
  4. 进入示例目录执行 node build.js(例如 cd examples/mixed && node build.js)。

若想一次性构建全部示例,可运行 npm run build:examples。示例模板 template.md 中形如 _{{example.js}}__{{stdout}}_ 的占位符,正是由这套构建脚本(examples 目录下的 buildAll.jstemplate-common.js 负责替换/渲染)在构建时用真实文件内容与运行输出填充,最终生成你看到的 README.md——这也是阅读 examples/mixed 目录时,README 内容远比 template 丰满的原因。

8. 两种模式的产物对比

dist/output.js 之下,模板保留了两种模式的实际输出统计(版本号以 X.X.X 占位,代表构建时刻的 webpack 版本)。

Unoptimized(开发模式)

asset output.js 13.4 KiB [emitted] (name: main)
asset require_context_templates_sync_recursive_js_.output.js 2.29 KiB [emitted]
chunk (runtime: main) output.js (main) 1010 bytes (javascript) 5.61 KiB (runtime) [entry] [rendered]
  > ./example.js main
  runtime modules 5.61 KiB 9 modules
  dependent modules 617 bytes [dependent] 3 modules
  ./example.js 396 bytes [built] [code generated]
    [used exports unknown]
    entry ./example.js main
chunk (runtime: main) require_context_templates_sync_recursive_js_.output.js 433 bytes [rendered]
  > ./example.js 7:0-14:1
  dependent modules 240 bytes [dependent] 3 modules
  ../require.context/templates/ sync ^\.\/.*\.js$ 193 bytes [built] [code generated]
    [no exports]
    amd require context ./example.js 7:0-14:1

可以读出几个关键事实:主 chunk output.js 中除 3 个业务模块外还内联了约 5.61 KiB 的 runtime(9 个运行时模块,含 JSONP chunk 加载能力,因为产物确实需要异步加载);而模板目录的 context 与其 3 个模板模块被完整剥离进第二个 chunk,注释 [no exports] 表明 context 作为整体没有静态导出。amd require context ./example.js 7:0-14:1 则是“谁引发了这次分割”的可追溯来源。

Production mode

asset output.js 2.5 KiB [emitted] [minimized] (name: main)
asset require_context_templates_sync_recursive_js_.output.js 625 bytes [emitted] [minimized]

主 chunk 从 13.4 KiB 压到 2.5 KiB(约 81% 缩减),异步 chunk 从 2.29 KiB 压到 625 bytes;依赖统计中的 [used exports unknown] 也变为 [no exports used],说明 minifier 在确认导出未使用后可以进一步压缩。这组数字直观展示了同一份混合格式代码在开发(可读性优先)与生产(体积优先)两种模式下的体积差异。

9. 小结与实战要点

回看整个示例,把关键结论梳理如下:

  • 三种格式天然互通:CommonJS require、AMD require([...])/define 与 ESM import 可在同一模块图中共存互引,webpack 在解析期用各自的 parser 插件识别,在生成期统一转换为模块数组里的标准包装函数;
  • AMD 数组 require = 异步块:凡写成 require([...], cb) 的依赖都会被当作可异步加载的块处理,回调体会延迟到所有依赖就绪后执行;
  • 含变量的依赖会触发 Context"目录/" + 变量 + ".js" 这类不可静态解析的请求会生成带映射表与匹配正则的 Context Module,并把匹配到的模块全部纳入打包,因此要谨慎控制 context 目录范围,避免把无关文件误打进 bundle;
  • 静态路径的“假动态”可被解析cond ? "./a" : "./b" 这类条件表达式不产生 context,编译期即登记两条依赖;
  • 观察产物是理解 webpack 最快的方式dist/output.js 中的 CommonJS bailoutAMD bailout 注解与 runtime 的按需注入,都直接反映了本示例各模块的静态分析边界。

如果希望继续深入,同一仓库还提供了大量可对照的姊妹示例:想看纯 CommonJS 场景可参考 examples/commonjs,想进一步研究 ESM 的互操作细节可阅读 harmony 系列示例,而 Context Module 的独立讲解与更多动态 require 场景见 examples/require.context。把 examples/mixed 的产物与源码对照阅读,是理解 webpack “模块格式无关”设计哲学的一条捷径。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388