webpack 混合模块系统实战:在同一个打包中协同使用 CommonJS、AMD 与 ES6(Harmony)模块
webpack 的历史使命之一就是充当不同模块规范之间的“粘合剂”。本指南以仓库中的 examples/mixed 官方示例(template.md,其渲染产物为 README.md)为骨架,完整演示如何在同一个项目、甚至同一个文件中混用 CommonJS、AMD 与 ES6 Modules(webpack 内部称为 Harmony),并解析混用动态拼接的 AMD 依赖数组时 webpack 如何自动生成 Context Module 与异步代码分割 chunk。读完你将理解三种模块格式的解析路径、混合 require 的产物形态,以及如何亲手构建并解读该示例。
1. 示例要解决什么问题
在传统前端工程中,你经常会遇到“历史包袱与现代化并存”的代码库:老代码用 require 与 module.exports,第三方库以 AMD define 暴露接口,新写的模块则是标准 ESM 的 import/export。examples/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) 的数组语法声明异步依赖,而数组中同时出现了三类元素:
- 普通的静态模块路径:
"./commonjs"、"./amd"; - 带变量的动态路径:
"../require.context/templates/" + amd1 + ".js",其中amd1来自顶部同步 require 到的 AMD 模块导出; - 条件表达式:
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)发挥作用的地方:./commonjs 以 module.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.context 中 require("./templates/" + templateName) 的原理同源。
产物里,这个 context 被单独拆成了一个异步 chunk:dist/require_context_templates_sync_recursive_js_.output.js。其名字一眼即可读解:
- 目录范围:
../require.context/templates/; - 匹配规则:
sync ^\.\/.*\.js$,即以./开头、以.js结尾的同步 context; - 内容:
templates/a.js、templates/b.js、templates/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 的解析分别由 CommonJsImportsParserPlugin、HarmonyImportDependencyParserPlugin 等负责,它们统一产出 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.path、chunkFilename 等也均由 webpack 默认值与示例构建脚本注入。
构建该示例可参考 examples/README.md 中 "Building an Example" 的通用流程:
- 在仓库根目录执行
yarn安装依赖; - 执行
yarn setup完成初始化; - 执行
yarn add --dev webpack-cli; - 进入示例目录执行
node build.js(例如cd examples/mixed && node build.js)。
若想一次性构建全部示例,可运行 npm run build:examples。示例模板 template.md 中形如 _{{example.js}}_、_{{stdout}}_ 的占位符,正是由这套构建脚本(examples 目录下的 buildAll.js 与 template-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、AMDrequire([...])/define与 ESMimport可在同一模块图中共存互引,webpack 在解析期用各自的 parser 插件识别,在生成期统一转换为模块数组里的标准包装函数; - AMD 数组 require = 异步块:凡写成
require([...], cb)的依赖都会被当作可异步加载的块处理,回调体会延迟到所有依赖就绪后执行; - 含变量的依赖会触发 Context:
"目录/" + 变量 + ".js"这类不可静态解析的请求会生成带映射表与匹配正则的 Context Module,并把匹配到的模块全部纳入打包,因此要谨慎控制 context 目录范围,避免把无关文件误打进 bundle; - 静态路径的“假动态”可被解析:
cond ? "./a" : "./b"这类条件表达式不产生 context,编译期即登记两条依赖; - 观察产物是理解 webpack 最快的方式:
dist/output.js中的CommonJS bailout、AMD bailout注解与 runtime 的按需注入,都直接反映了本示例各模块的静态分析边界。
如果希望继续深入,同一仓库还提供了大量可对照的姊妹示例:想看纯 CommonJS 场景可参考 examples/commonjs,想进一步研究 ESM 的互操作细节可阅读 harmony 系列示例,而 Context Module 的独立讲解与更多动态 require 场景见 examples/require.context。把 examples/mixed 的产物与源码对照阅读,是理解 webpack “模块格式无关”设计哲学的一条捷径。
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 StartedRust0627
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