webpack 按需加载入门:用 `require.ensure` 实现 Code Splitting 的完整示例剖析
本指南围绕 webpack 官方示例中的最小化代码分割场景展开:仅用不到 20 行业务代码,演示如何通过 require.ensure 把一个 CommonJS 应用拆分为“入口 chunk + 按需加载 chunk”,并逐一拆解两个产物的内容构成、JSONP 运行时加载机制与打包器对模块的去重优化。读完你将掌握 require.ensure 的调用语法与三种参数形态、如何解读 webpack 的拆包产物与 Stats 输出,以及该 API 在当前源码中的解析实现与其与现代 import() 的演进关系。
示例概览:一个最小的 Code Splitting 场景
示例位于仓库的 examples/code-splitting 目录,由四个文件构成:
| 文件 | 作用 |
|---|---|
| example.js | 入口业务代码,演示 require.ensure 的用法 |
| webpack.config.js | 仅指定 chunkIds: "named" 的极简配置 |
| template.md | 示例的“写作模板”,定义待填充的产物与输出结构 |
| README.md | 构建后生成的完整文档,含真实的产物代码与构建统计 |
示例中的模块依赖关系非常简单:a、b 通过 CommonJS 被正常引用;c 只出现在 require.ensure 的依赖数组里(被“预先暴露”但不立即执行);d 只在异步回调内部被引用。构建后会得到两个文件——入口 chunk output.js 与按需加载的 node_modules_c_js-node_modules_d_js.output.js,这一文件名恰恰由 chunkIds: "named"(webpack.config.js)派生,目的是让不同模式(普通 / production)下生成的文件名保持一致。
核心语法:逐行解读 require.ensure 调用
示例的业务代码全部位于 example.js,原文如下:
var a = require("a");
var b = require("b");
require.ensure(["c"], function(require) {
require("b").xyz();
var d = require("d");
});
这四行代码对应文档 template.md 归纳的四条行为规则,逐条说明如下:
a、b以普通 CommonJS 方式被引用。由于它们出现在顶层同步代码中,webpack 会把它们编入入口 chunk(即主 chunk),随首屏一起加载。c通过require.ensure的依赖数组“暴露”给运行时。require.ensure(["c"], ...)的第一个参数是模块列表,语义是“把c预先加载好并放进模块表,但不执行它”。webpack 据此知道c属于按需 chunk,会为其生成一次独立的网络请求。b、d在require.ensure的回调函数内部被 CommonJS 引用。回调体形如function(require) { ... },webpack 会分析这个回调的整个函数体,凡是在其中被引用的模块都被标记为“按需依赖”,最终一起进入异步 chunk。- 优化器可以把回调里的
b从异步 chunk 中剔除。因为b已经存在于父级(入口)chunk 中,异步 chunk 不需要再携带一份副本;运行时在回调中require("b")时命中入口 chunk 中已缓存的模块即可。
需要特别说明的一点是:c 只被“保证可用”,并不会因为出现在依赖数组中就被执行——真正触发执行的只有回调函数体内的 require 语句。这正是“Code Splitting”(把加载时机与执行时机分离)的精髓。
完整的调用形态与参数可选性
require.ensure 实际支持 2~4 个参数。从其解析器插件 lib/dependencies/RequireEnsureDependenciesBlockParserPlugin.js 中对 expr.arguments.length 的 switch 分支可以推断出完整签名:
require.ensure(dependencies, callback);
require.ensure(dependencies, callback, errorCallback);
require.ensure(dependencies, callback, errorCallback, chunkName);
// 也支持将第三个字符串参数当作 chunkName 的形态:
require.ensure(dependencies, callback, "my-chunk-name");
- 依赖数组(必填):支持字符串组成的数组;从源码看,若传入单个表达式且不是数组,会被包装成单元素数组处理(
isArray() ? items : [expr])。 - 成功回调(必填):回调体内所有
require(...)会被递归遍历,全部进入异步块。 - 错误回调 / chunk 名(可选):当第 3 个参数是函数时视为错误回调;当它不是函数而是一个可求值为字符串的表达式时,则按 chunk 名处理;第 4 个参数则为显式 chunk 名。
在该插件中,webpack 每遇到一次 require.ensure 调用,就会在语法树上构造一个 RequireEnsureDependenciesBlock(异步依赖块),把依赖数组里的每个模块字符串转成 RequireEnsureItemDependency,并把回调函数体内的 require(...) 依依赖关系解析到对应的模块上——这正是“回调中的模块被识别为按需加载”这一行为得以成立的根本原因。
构建产物拆解:入口 chunk 与异步 chunk 各装了什么
根据 template.md(构建后的完整版见 README.md),这次构建产出两个 chunk:
dist/output.js —— 入口 chunk(main)
包含:
- 模块系统运行时:模块缓存表、
__webpack_require__模块加载函数,以及对模块表的引用__webpack_require__.m; - chunk 加载逻辑:
__webpack_require__.e(异步加载入口)、__webpack_require__.u(由 chunk id 计算文件名)、__webpack_require__.l(动态创建<script>标签)、JSONP 回调注册等运行时片段; - 入口点
example.js以及模块a、b。
入口代码在编译后从 var a = require("a") 变为对模块表下标的调用,而 require.ensure 调用则被替换为一次运行时异步加载(节选自 README.md 中 dist/output.js 的入口包装部分):
var a = __webpack_require__(/*! a */ 1);
var b = __webpack_require__(/*! b */ 2);
__webpack_require__.e(/*! require.ensure */ "node_modules_c_js-node_modules_d_js").then((function(require) {
(__webpack_require__(/*! b */ 2).xyz)();
var d = __webpack_require__(/*! d */ 4);
}).bind(null, __webpack_require__))'catch';
注意三点变化:
b的二次引用被编译为__webpack_require__(2),仍指向入口 chunk 中已有的模块 2;c与d所在的模块 3、4 并没有出现在这段入口代码里,而是由__webpack_require__.e(...)触发按需请求;- 原先的回调被改造成
.then(...),并以'catch'兜底未捕获的错误,即“错误回调缺省时统一收口到__webpack_require__.oe”。
dist/node_modules_c_js-node_modules_d_js.output.js —— 异步 chunk
这个按需文件只包含模块 c 与 d,完整内容如下(节选自 README.md):
(self["webpackChunk"] = self["webpackChunk"] || []).push([["node_modules_c_js-node_modules_d_js"],[
/* 0 */,
/* 1 */,
/* 2 */,
/* 3 */
/*!***************************!*\
!*** ./node_modules/c.js ***!
\***************************/
/***/ (() => {
// module c
/***/ }),
/* 4 */
/*!***************************!*\
!*** ./node_modules/d.js ***!
\***************************/
/***/ (() => {
// module d
/***/ })
]]);
可以看到异步 chunk 的模块表与入口 chunk 的模块表共享同一份“下标空间”,因此模块 3、4 的编号是入口模块 1、2 的顺延。优化证据也直接写在这份产物里:异步 chunk 的下标 0、1、2 全部留空——尤其是下标 2(即模块 b),说明打包器确实没有把回调中引用到的 b 复制进异步 chunk。
压缩后的体积对比
该异步文件本身极小,压缩后只有一行:
(self.webpackChunk=self.webpackChunk||[]).push([["node_modules_c_js-node_modules_d_js"],{605(){},576(){}}]);
文档 template.md 对此给出的结论是:按需 chunk 通常很小,压缩收益明显——这正是“把不常用的代码拆出去、让主包更轻”这一实践价值的直接体现。
JSONP 式 chunk 加载与运行时协作
文档明确说明“chunks are loaded via JSONP”(chunk 通过 JSONP 方式加载)。结合 README.md 中完整展示的 dist/output.js 运行时代码,可以看清整套协作链条:
- 入口 chunk 的运行时维护
installedChunks状态表:undefined表示未加载,0表示已加载完成,数组中存放[resolve, reject, Promise]表示加载中。 - 异步加载函数
__webpack_require__.e(chunkId)汇总所有 chunk 加载策略(此处为 JSONP 策略__webpack_require__.f.j),首次加载时创建一个 Promise 记录到状态表,随后通过__webpack_require__.l(...)动态插入<script src="dist/" + chunkId + ".output.js">。 - 全局挂载
self["webpackChunk"]数组作为 JSONP 回调池:异步 chunk 文件头部执行(self["webpackChunk"] = self["webpackChunk"] || []).push([...]),被 push 的数据形如[chunkIds, moreModules, runtime];入口运行时用自定义的push包装(webpackJsonpCallback)接管该数组,把moreModules合并进模块表、把对应 chunk 标记为已加载,并 resolve 之前创建的 Promise。 - 加载失败(含脚本 404、超时等)时,运行时构造带
name: "ChunkLoadError"、request、event等字段的错误对象,最终流转到入口代码里的'catch'。
这套机制即“把异步脚本的加载结果以 JSONP 回调形式回灌给模块系统”的完整闭环,它与浏览器端 chunk 加载在 webpack 源码中的实现对应关系为:运行时源码位于 lib/runtime(含 jsonp chunk loading、ensure chunk、load script 等运行时模块),而本示例产物中“仅含入口 chunk、运行时模块 6 个、共 4.83 KiB”的描述可直接从构建统计中验证。
用 Stats 验证两种模式的差异
示例构建会分别以普通模式与 production 模式各跑一次,把 webpack X.X.X compiled successfully 的统计输出写入文档。以 README.md 的 “Info” 章节为据:
Unoptimized(普通模式)
asset output.js 9.16 KiB [emitted] (name: main)
asset node_modules_c_js-node_modules_d_js.output.js 562 bytes [emitted]
chunk (runtime: main) output.js (main) 161 bytes (javascript) 4.83 KiB (runtime) [entry] [rendered]
runtime modules 4.83 KiB 6 modules
dependent modules 22 bytes [dependent] 2 modules
./example.js 139 bytes [built] [code generated]
chunk (runtime: main) node_modules_c_js-node_modules_d_js.output.js 22 bytes [rendered]
./node_modules/c.js 11 bytes [built] [code generated]
require.ensure item c ./example.js 3:0-6:2
./node_modules/d.js 11 bytes [built] [code generated]
cjs require d ./example.js 5:12-24
Production mode(压缩模式)
asset output.js 1.8 KiB [emitted] [minimized] (name: main)
asset node_modules_c_js-node_modules_d_js.output.js 108 bytes [emitted] [minimized]
两段输出值得关注的信息点:
- 模块归属一目了然:
require.ensure item c ./example.js 3:0-6:2标注了c来自 example.js 第 3 行的require.ensure依赖数组;cjs require d ./example.js 5:12-24标注了d来自回调体内的第 5 行 CommonJS require。这正是前述“依赖数组中的模块”与“回调中的模块”两种来源在 Stats 中的可读化呈现。 - 体积优化显著:入口 chunk 从 9.16 KiB 压缩到 1.8 KiB;异步 chunk 从 562 字节压到 108 字节,而其中的业务模块本体仅 22 字节(
c、d各 11 字节),几乎可以认为压缩后就是最优规模。 - 统计中不会出现模块
b的重复条目:异步 chunk 的 22 字节只由c、d构成,印证了优化器对父 chunk 已存在模块的去重。
从 require.ensure 到现代 import() 的演进与实现细节
require.ensure 是 webpack 早期(webpack 1 时代、ES 模块尚未普及)提供的第一代按需加载 API,配合 AMD/CommonJS 生态而生。在源码中,它的解析实现集中在 lib/dependencies/RequireEnsurePlugin.js 与 lib/dependencies/RequireEnsureDependenciesBlockParserPlugin.js:插件会为 javascript/auto 与 javascript/dynamic 两类 JavaScript 模块的 parser 挂上 require.ensure 的调用钩子,并额外把 typeof require.ensure 编译期求值为字符串 "function",保证“特性探测”类代码也能正常通过解析。同时,若配置中显式声明 module.parser.javascript.requireEnsure: false,parser 将跳过注册,从而禁用该 API。
需要强调的是,这一示例属于“最基础的历史形态”。webpack 的 Code Splitting 能力在今天已全面转向标准化的动态 import() 语法,仓库中与之配套的演进示例还包括:
- examples/code-splitting-harmony —— 面向 ES module 的按需加载写法;
- examples/code-splitting-specify-chunk-name —— 用“魔法注释”指定 chunk 名;
- examples/extra-async-chunk 与 examples/extra-async-chunk-advanced —— 展示异步 chunk 的进一步细粒化。
若在 import() 动态导入之外探索更多“代码切分”手段,还可参考 examples/common-chunk-and-vendor-chunk(公共 chunk / 第三方 vendor chunk)以及 examples/reexport-components(按需 re-export 组件)等目录。
在本地复现该示例
若要亲手跑出上述两份产物与统计,可按 examples/README.md 末尾 “Building an Example” 一节的官方流程执行:
- 在仓库根目录运行
yarn安装依赖; - 运行
yarn setup完成示例环境初始化; - 在仓库根目录运行
yarn add --dev webpack-cli,确保 CLI 可用; - 进入具体示例目录后执行示例自身的构建脚本(构建完成后,产物会落在该目录的
dist/下,并把 template.md 中的_{{...}}_占位符替换为真实产物与 Stats,生成 README.md)。
如需一次性构建全部示例,可在根目录执行 npm run build:examples。构建入口与占位符替换逻辑分别由 examples/buildAll.js(遍历所有含 template.md 的目录、逐个执行构建)与 examples/template-common.js(实现 _{{stdout}}_、_{{production:stdout}}_、_{{dist/xxx}}_ 等占位符的文件读取与正则替换)承载。
小结:从本示例能带走什么
通过这份最小示例,可以串联起一条完整的知识链:
- API 语义:
require.ensure(deps, cb)中,依赖数组负责“预加载”,回调体内的require负责“执行时引用”,两者共同决定异步 chunk 的内容边界; - 产物形态:入口 chunk 携带模块系统与运行时,异步 chunk 只携带业务模块,二者共享模块编号空间,并通过 JSONP 数组完成“加载即回灌”;
- 优化行为:已被父 chunk 持有的模块不会被重复打包进异步 chunk,示例中的
b即是最直观的验证; - 测量方法:普通与 production 两套 Stats 给出了量化的体积对照,可作为评估“拆包收益”的标准参照。
对现代工程而言,推荐直接使用 import() 动态导入享受同样的能力(且获得浏览器原生语义支持),但理解 require.ensure 背后的 chunk 划分与 JSONP 加载模型,仍然是读懂 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 StartedRust0626
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