news 2026/9/7 8:25:44

webpack bundle-loader 实战:用 Loader 把任意模块变成按需加载的异步 Chunk

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
webpack bundle-loader 实战:用 Loader 把任意模块变成按需加载的异步 Chunk

webpack bundle-loader 实战:用 Loader 把任意模块变成按需加载的异步 Chunk

【免费下载链接】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

本篇以 webpack 官方示例code-splitting-bundle-loader为主线,讲解Code Splitting(代码分割)与 Loaders(加载器)的组合玩法:bundle-loader 会为file.js生成一个"包装模块(wrapper module)",该包装模块导出一个函数,调用它即可异步获取内层模块的 exports。读完后你将理解:如何用loader!file请求语法强制应用 loader、wrapper 内部的回调队列与__webpack_require__.e加载机制如何协作,以及最终产物中 JSONP chunk 加载运行时的完整结构。

示例定位:Code Splitting 与 Loaders 的结合体

该示例的文档开头明确指出:"This example combines Code Splitting and Loaders",并建议读者先阅读单独演示这两项功能的示例——即 code-splitting(原生动态import()切分)与 loader(自定义 loader 编写)。

与其他 code-splitting 示例不同,这里异步加载并非由源码中的动态 import 触发,而是由 loader 生成的包装代码触发。examples/README.md 中对该示例的描述是:

code-splitting-bundle-loader example demonstrating Code Splitting through the bundle loader

bundle-loader 已在仓库根目录 package.json 中声明("bundle-loader": "^0.5.6"),属于示例专用依赖,无需额外安装即可在本地构建。

示例源码:三个文件讲完核心机制

example.js:调用包装函数

example.js 全文只有三行:

require("bundle-loader!./file.js")(function(fileJsExports) { console.log(fileJsExports); });

这里有两个关键点:

  1. bundle-loader!./file.js是 loader 请求语法!前缀强制对./file.js应用 bundle-loader。打包产物中该模块的标识会变为../../node_modules/bundle-loader/index.js!./file.js,这一点可以在此示例的编译 stats 输出中直接验证(见下文 Info 小节);
  2. require(...)的返回值立即被当作函数调用:bundle-loader 的作用正是让模块的module.exports不再是目标模块本身,而是一个"加载函数"——传入回调,异步拿回真正的 exports。

file.js:被包装的目标模块

file.js 是一个最简单的 CommonJS 模块:

module.exports = "It works";

运行效果就是异步打出It works,但"取值"发生在一个独立的异步 chunk 中。

webpack.config.js:固定 chunk 文件名

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;

chunkIds: "named"使异步 chunk 以源文件名(file_js)作为 id,保证在 development / production 等不同构建模式下输出文件名保持一致,便于文档示例与调试。示例通过根目录的 examples/buildAll.js 统一执行cd <example> && node build.js完成构建,template.md(即 template.md)负责把构建产物注入生成 README.md。

产物分析:入口 chunk 中的包装模块

构建后得到两个文件:dist/output.js(main 入口 chunk)与dist/file_js.output.js(异步 chunk)。下文产物内容以仓库 README.md 中记录的实际构建结果为准。

wrapper 模块源码:回调队列 + 异步加载

dist/output.js中由 bundle-loader 生成的模块(模块 id 为 1)是理解本示例的核心:

/*!***************************!*\ !*** ../../node_modules/bundle-loader/index.js!./file.js ***! \***************************/ var cbs = [], data; module.exports = function(cb) { if(cbs) cbs.push(cb); else cb(data); } __webpack_require__.e(/*! require.ensure */ "file_js").then((function(require) { data = __webpack_require__(/*! !!./file.js */ 2); var callbacks = cbs; cbs = null; for(var i = 0, l = callbacks.length; i < l; i++) { callbacksi; } }).bind(null, __webpack_require__))'catch';

逐段拆解:

  • module.exports = function(cb) {...}实现了文档中"wrapper module returns a function that can be called to asynchronously receive the inner module"的语义:若数据未就绪(cbs存在),把回调压入队列;若 chunk 已加载完成(cbs已被置为null),立即同步回调data。这使多次调用方都能拿到结果,且天然支持"先调用后加载完成"的竞态;
  • __webpack_require__.e("file_js")是 webpack 运行时的ensure chunk(确保 chunk 加载)函数,注释require.ensure表明 bundle-loader 在早期 webpack 1.x 时代基于require.ensure实现,如今等价于动态 import 的底层加载路径。它返回 Promise,chunk 就绪后 resolve;
  • __webpack_require__(2)加载内层模块。注意其请求标注为!!./file.js——双!!是 webpack 的"清空规则"请求语法,表示加载./file.js不再应用任何 loader 规则,得到的是未加工的原始模块(对应产物中的模块 id 2);
  • .catch(__webpack_require__.oe)把加载失败(如ChunkLoadError)交给运行时统一的错误处理函数。

chunk 加载运行时:文件名规则与 JSONP 机制

dist/output.js中还内嵌了完整的 chunk 加载运行时,几个关键函数如下(完整实现见 README.md 的dist/output.js小节):

/* webpack/runtime/ensure chunk */ __webpack_require__.e = (chunkId) => { return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => { __webpack_require__.fkey; return promises; }, [])); }; /* webpack/runtime/get javascript chunk filename */ __webpack_require__.u = (chunkId) => (chunkId + ".output.js");
  • __webpack_require__.e聚合所有"加载策略钩子"(__webpack_require__.f命名空间),本示例中只有 JSONP 策略__webpack_require__.f.j
  • __webpack_require__.u定义了异步 chunk 的文件名规则:chunkId + ".output.js",这正是产物中出现file_js.output.js的原因。

JSONP 加载部分(/* webpack/runtime/jsonp chunk loading */)维护了installedChunks状态表——undefined表示未加载、null表示预加载/预取、[resolve, reject, Promise]表示加载中、0表示已加载;加载失败会构造ChunkLoadError(携带typerequestevent字段)供上层捕获。脚本注入由__webpack_require__.l完成:创建<script>标签、120 秒超时保护(setTimeout(..., 120000))、完成后从document.head移除节点。这些运行时函数由 webpack 的运行时模块体系生成(源码位于 lib/runtime/ 目录),与运行时全局标识符定义 lib/RuntimeGlobals.js 相对应。

异步 chunk:file_js.output.js

异步 chunk 的内容极为精简——仅包含目标模块本身:

(self["webpackChunk"] = self["webpackChunk"] || []).push([["file_js"],{ /***/ 2 /*!*****************!*\ !*** ./file.js ***! \*****************/ (module) { module.exports = "It works"; /***/ } }]);

它通过向全局webpackChunk数组 push[chunkIds, moreModules]数据来"回注"模块:入口运行时劫持了该数组的push方法(chunkLoadingGlobal.push = webpackJsonpCallback.bind(...)),push 即触发webpackJsonpCallback——把moreModules合入__webpack_require__.m,再把对应 chunkId 标记为0(已安装)并 resolve 等待中的 Promise,最终唤醒 wrapper 中的.then回调。

编译 Stats:Unoptimized vs Production

Unoptimized(development 默认)

asset output.js 9.34 KiB [emitted] (name: main) asset file_js.output.js 348 bytes [emitted] chunk (runtime: main) file_js.output.js 28 bytes [rendered] > ../../node_modules/bundle-loader/index.js!./file.js 7:0-14:2 ./file.js 28 bytes [built] [code generated] [used exports unknown] cjs self exports reference ./file.js 1:0-14 cjs require !!./file.js ../../node_modules/bundle-loader/index.js!./file.js 8:8-30 chunk (runtime: main) output.js (main) 375 bytes (javascript) 4.83 KiB (runtime) [entry] [rendered] > ./example.js main runtime modules 4.83 KiB 6 modules dependent modules 281 bytes [dependent] 1 module ./example.js 94 bytes [built] [code generated] [used exports unknown] entry ./example.js main webpack compiled successfully

stats 印证了前文的分析:file_js异步 chunk 的引用来源是../../node_modules/bundle-loader/index.js!./file.js(loader 包装模块)第 7–14 行,内部以!!./file.js形式依赖原始模块——loader 链与"清空规则"请求一目了然。

Production mode

asset output.js 1.88 KiB [emitted] [minimized] (name: main) asset file_js.output.js 93 bytes [emitted] [minimized] chunk (runtime: main) file_js.output.js 28 bytes [rendered] > ../../node_modules/bundle-loader/index.js!./file.js 7:0-14:2 ./file.js 28 bytes [built] [code generated] [used exports unknown] cjs self exports reference ./file.js 1:0-14 cjs require !!./file.js ../../node_modules/bundle-loader/index.js!./file.js 8:8-30 chunk (runtime: main) output.js (main) 375 bytes (javascript) 4.83 KiB (runtime) [entry] [rendered] > ./example.js main runtime modules 4.83 KiB 6 modules dependent modules 281 bytes [dependent] 1 module ./example.js 94 bytes [built] [code generated] [no exports used] entry ./example.js main webpack compiled successfully

生产模式下入口从 9.34 KiB 压缩至 1.88 KiB、异步 chunk 从 348 bytes 降至 93 bytes(均带[minimized]标记);模块标记由[used exports unknown]变为[no exports used],说明压缩阶段的副作用分析对 CommonJS 模块的 exports 使用判定更为确定。两组 stats 共同的4.83 KiB (runtime)/6 modules表明:本例体积大头是 chunk 加载运行时(6 个 runtime modules),业务代码本身极小。

原理小结与适用场景

从源码结构与产物可以归纳出本示例的完整链路:

  1. require("bundle-loader!./file.js")经 loader 请求语法生成包装模块,其 exports 是一个"带回调队列的加载函数";
  2. 包装模块顶层立即执行__webpack_require__.e("file_js"),驱动运行时把file.js单独渲染为异步 chunkfile_js.output.js(文件名由__webpack_require__.u规则决定);
  3. 运行时通过 JSONP(webpackChunk全局数组 + 脚本注入)加载该 chunk、合入模块表,resolve 后回调被依次触发;
  4. 内层模块以!!./file.js方式加载,绕过其他 loader 规则,保证拿到原始 CommonJS 模块。

需要说明的是:现代 webpack 已内置基于动态import()的原生代码分割(见 examples/code-splitting 与 examples/code-splitting-harmony),bundle-loader 这类第三方方案更多用于把无法改写源码的旧模块、或需要"按调用时机加载"的模块包装成异步单元的兼容场景。本示例的价值正在于展示了 webpack 的另一条能力面——loader 与 code splitting 机制的叠加:loader 决定"模块长什么样",而__webpack_require__.e/ JSONP 运行时决定"模块何时、如何到达",两者正交且可自由组合。

如何验证

  1. 仓库已包含示例依赖,无需安装;
  2. 进入示例目录执行构建:cd examples/code-splitting-bundle-loader && node build.js(或运行根目录 examples/buildAll.js 批量构建全部示例);
  3. 对比dist/output.js中 wrapper 模块与dist/file_js.output.js的内容,核对本篇所述模块 id(1 = wrapper、2 = 内层模块)、回调队列逻辑与 JSONP 运行时代码;
  4. 交叉参考 examples/code-splitting-bundle-loader/README.md 中记录的 Unoptimized / Production 两组 stats,确认 chunk 归属与体积符合预期。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 8:24:11

Spring Cloud微服务实战:网约车项目核心链路与高并发方案

简介&#xff1a;OnlineTaxi 是基于 Spring Cloud 的网约车全流程实战项目&#xff0c;面向具备一定 Java 基础、希望学习微服务架构的开发者或相关专业学生。项目按乘客端、司机端与能力层拆分为订单、派单、乘客用户、短信、计价、验证码、钱包、支付、地图等多个服务&#x…

作者头像 李华
网站建设 2026/9/7 8:23:58

4K视频处理技术解析:从本地部署到影视内容分析实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 8:21:28

C# Winform调用user32.dll实现自动点击工具实战

简介&#xff1a;这份C# Winform自动点击工具&#xff0c;专为需要定时或重复点击屏幕固定位置的办公自动化场景而设计&#xff1b;当用户不在工位时&#xff0c;工具可代替手工完成鼠标操作。项目基于DllImport特性引入Windows API user32.dll非托管动态库&#xff0c;通过托管…

作者头像 李华
网站建设 2026/9/7 8:18:05

Cap录屏:免费的开源录屏工具,3步录完直接拿链接

Cap录屏&#xff1a;免费的开源录屏工具&#xff0c;3步录完直接拿链接 【免费下载链接】Cap Open source Loom alternative. Beautiful, shareable screen recordings. 项目地址: https://gitcode.com/GitHub_Trending/cap1/Cap 录课程视频的时候&#xff0c;系统弹了个…

作者头像 李华