webpack 按名动态加载模块并自动代码拆分:AMD 风格 require.context 示例的完整解析
【免费下载链接】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 官方仓库中的示例 examples/code-splitted-require.context-amd,讲解一个经典场景:当 AMD 风格的require(["目录/"+变量], callback)携带动态路径时,webpack 如何将其识别为上下文模块(Context Module),自动把整个目录打进一个独立的异步 chunk,并通过运行时 JSONP 机制按需加载。读完后,你将掌握动态 require 触发的代码拆分原理、产物结构(主 chunk + 异步 chunk)的逐段解读,以及背后 AMDRequireDependenciesBlockParserPlugin 与 AMDRequireContextDependency 的源码级实现链路。
示例场景:按名称加载模板文件
示例入口 example.js 用 AMD 异步require按名称加载模板模块:
function getTemplate(templateName, callback) { require(["../require.context/templates/"+templateName], function(tmpl) { callback(tmpl()); }); } getTemplate("a", function(a) { console.log(a); }); getTemplate("b", function(b) { console.log(b); });关键点在于请求字符串"../require.context/templates/"+templateName是运行时才能确定的拼接表达式。webpack 在构建期无法静态解析出确切模块,于是退而求其次:把"../require.context/templates/"目录下的所有匹配模块全部纳入构建,生成一个“上下文模块”,运行时再按传入名称查找。
构建配置
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;optimization.chunkIds: "named"的作用是使用可读的命名 chunk id,保证在不同模式(开发/生产)下异步 chunk 文件名一致,方便演示产物分析。示例的统计输出里可以看到异步 chunk 被命名为require_context_templates_sync_recursive_.output.js——这正是以该上下文模块命名的 chunk id。
产物一:主 chunk(dist/output.js)
构建后主 chunk 由三部分组成:webpackBootstrap 运行时、被转换的入口模块代码。
1. 被转换的入口模块
原始的require(["..."], callback)被重写为“先请求异步 chunk 加载,再同步 require 上下文模块并调用”:
/*!********************!*\ !*** ./example.js ***! \********************/ /*! unknown exports (runtime-defined) */ /*! runtime requirements: __webpack_require__.e, __webpack_require__.oe, __webpack_require__, __webpack_require__.* */ function getTemplate(templateName, callback) { __webpack_require__.e(/*! AMD require */ "require_context_templates_sync_recursive_").then(function() { var __WEBPACK_AMD_REQUIRE_ARRAY__ = [__webpack_require__(1)("./"+templateName)]; (function(tmpl) { callback(tmpl()); }).apply(null, __WEBPACK_AMD_REQUIRE_ARRAY__);})'catch'; } getTemplate("a", function(a) { console.log(a); }); getTemplate("b", function(b) { console.log(b); });可以逐段拆解这段重写结果:
__webpack_require__.e("require_context_templates_sync_recursive_"):向运行时申请加载指定的异步 chunk(JSONP<script>注入),返回 Promise。AMD require 的回调形式天然就是异步语义,因此上下文模块被整体放入异步 chunk 而不是主 chunk;__webpack_require__(1)("./"+templateName):chunk 就绪后同步 require 模块 id 为1的上下文模块,并以"./"+templateName为 key 查表;__WEBPACK_AMD_REQUIRE_ARRAY__:模拟 AMD 回调参数注入,保持原有function(tmpl) {...}的调用形态;'catch':chunk 加载失败(如网络错误)时统一抛出ChunkLoadError。
2. webpack 运行时(节选自完整输出)
主 chunk 中还内嵌了 chunk 加载相关的运行时模块(完整内容见 examples/code-splitted-require.context-amd/README.md 中dist/output.js一节),核心结构如下:
/******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({}); /******/ const __webpack_module_cache__ = {}; /******/ function __webpack_require__(moduleId) { /******/ const cachedModule = __webpack_module_cache__[moduleId]; /******/ if (cachedModule !== undefined) { /******/ return cachedModule.exports; /******/ } /******/ const module = __webpack_module_cache__[moduleId] = { exports: {} }; /******/ __webpack_modules__moduleId; /******/ return module.exports; /******/ } /******/ __webpack_require__.m = __webpack_modules__;随后是驱动异步 chunk 加载的关键运行时片段:
/******/ /* webpack/runtime/ensure chunk */ /******/ __webpack_require__.f = {}; /******/ // This file contains only the entry chunk. /******/ // The chunk loading function for additional chunks /******/ __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/runtime/load script */ /******/ // __webpack_require__.l:通过 script 标签注入加载 chunk(含 inProgress 去重、120s 超时) /******/ /* webpack/runtime/publicPath */ /******/ __webpack_require__.p = "dist/";以及 JSONP 回调核心——异步 chunk 通过self["webpackChunk"]全局数组向主 chunk 交付模块与模块 id:
/******/ /* webpack/runtime/jsonp chunk loading */ /******/ (() => { /******/ const installedChunks = { "main": 0 }; /******/ __webpack_require__.f.j = (chunkId, promises) => { /******/ let installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined; /******/ if(installedChunkData !== 0) { // 0 means "already installed". /******/ if(installedChunkData) { /******/ promises.push(installedChunkData[2]); /******/ } else { /******/ // setup Promise in chunk cache /******/ const promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject])); /******/ promises.push(installedChunkData[2] = promise); /******/ // create error before stack unwound to get useful stacktrace later /******/ const error = new Error(); /******/ const loadingEnded = (event) => { /* 失败时构造 ChunkLoadError */ }; /******/ __webpack_require__.l(__webpack_require__.p + __webpack_require__.u(chunkId), loadingEnded, "chunk-" + chunkId, chunkId); /******/ } /******/ } /******/ }; /******/ // install a JSONP callback for chunk loading /******/ const webpackJsonpCallback = (parentChunkLoadingFunction, data) => { /******/ let [chunkIds, moreModules, runtime] = data; /******/ // add "moreModules" to the modules object, /******/ // then flag all "chunkIds" as loaded and fire callback /******/ var moduleId, chunkId, i = 0; /******/ if(chunkIds.some((id) => (installedChunks[id] !== 0))) { /******/ for(moduleId in moreModules) { /******/ if(__webpack_require__.o(moreModules, moduleId)) { /******/ __webpack_require__.m[moduleId] = moreModules[moduleId]; /******/ } /******/ } /******/ if(runtime) var result = runtime(__webpack_require__); /******/ } /******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data); /******/ for(;i < chunkIds.length; i++) { /******/ chunkId = chunkIds[i]; /******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) { /******/ installedChunks[chunkId][0](); /******/ } /******/ installedChunks[chunkId] = 0; /******/ } /******/ } /******/ const chunkLoadingGlobal = self["webpackChunk"] = self["webpackChunk"] || []; /******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0)); /******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal)); /******/ })();这套机制与浏览器端常规动态import()的 chunk 加载完全同源:__webpack_require__.e统一入口、.f.j注册 JSONP 策略、webpackJsonpCallback负责把moreModules合并进__webpack_require__.m并 resolve 等待 Promise。
产物二:异步 chunk(dist/require_context_templates_sync_recursive_.output.js)
这是本示例真正的主角——按目录整体打包出来的异步 chunk:
(self["webpackChunk"] = self["webpackChunk"] || []).push([["require_context_templates_sync_recursive_"],[ /* 0 */, /* 1 */ /*!***************************************************!*\ !*** ../require.context/templates/ sync ^\.\/.*$ ***! \***************************************************/ /*! default exports */ /*! exports [not provided] [no usage info] */ /*! runtime requirements: module, __webpack_require__.o, __webpack_require__ */ /***/ ((module, __unused_webpack_exports, __webpack_require__) => { const map = { "./a": 2, "./a.js": 2, "./b": 3, "./b.js": 3, "./c": 4, "./c.js": 4 }; function webpackContext(req) { const id = webpackContextResolve(req); return __webpack_require__(id); } function webpackContextResolve(req) { if(!__webpack_require__.o(map, req)) { const e = new Error("Cannot find module '" + req + "'"); e.code = 'MODULE_NOT_FOUND'; throw e; } return map[req]; } webpackContext.keys = function webpackContextKeys() { return Object.keys(map); }; webpackContext.resolve = webpackContextResolve; module.exports = webpackContext; webpackContext.id = 1; /***/ }), /* 2 */ /*!*****************************************!*\ !*** ../require.context/templates/a.js ***! \*****************************************/ /*! unknown exports (runtime-defined) */ /*! runtime requirements: module */ /*! CommonJS bailout: module.exports is used directly at 1:0-14 */ /***/ ((module) => { module.exports = function() { return "This text was generated by template A"; } /***/ }), /* 3 */ /*!*****************************************!*\ !*** ../require.context/templates/b.js ***! \*****************************************/ /***/ ((module) => { module.exports = function() { return "This text was generated by template B"; } /***/ }), /* 4 */ /*!*****************************************!*\ !*** ../require.context/templates/c.js ***! \*****************************************/ /***/ ((module) => { module.exports = function() { return "This text was generated by template C"; } /***/ }) ]]);这段产物揭示了上下文模块的完整形态:
- 模块
1是上下文模块本体。注释***! ../require.context/templates/ sync ^\.\/.*$ ***!表明它是对templates/目录做“同步、递归正则^\.\/.*$”扫描生成的——即匹配目录下所有直接子模块; map是运行时的路由表,把"./a"、"./a.js"两种写法都映射到真实模块 id(2/3/4)。这解释了为什么getTemplate("a")与getTemplate("a.js")都能命中;webpackContext(req)就是运行时被注入到__webpack_require__(1)位置的函数:先用webpackContextResolve查表(未命中则抛出带MODULE_NOT_FOUNDcode 的Error),再__webpack_require__(id)加载具体模板;- 模块 2/3/4是目录下 a/b/c 三个模板文件本身,被原样打包(CommonJS 风格,注释标明
module.exports is used directly因而跳过了 ESM 转换优化); - 整个 chunk 通过
(self["webpackChunk"] = ...).push(...)交付给主 chunk 的webpackJsonpCallback——这正是上一节中installedChunks状态机等待的那个 JSONP 回调。
注意一个细节:getTemplate只用了 "a" 和 "b",但 "c" 同样被打包进来。这是上下文模块的固有成本——目录内所有匹配文件都会进包,webpack 无法知道运行时只会用到哪几个。
构建统计输出(Stats)
Unoptimized 模式
asset output.js 8.72 KiB [emitted] (name: main) asset require_context_templates_sync_recursive_.output.js 2.28 KiB [emitted] chunk (runtime: main) output.js (main) 251 bytes (javascript) 4.83 KiB (runtime) [entry] [rendered] > ./example.js main runtime modules 4.83 KiB 6 modules ./example.js 251 bytes [built] [code generated] [used exports unknown] entry ./example.js main chunk (runtime: main) require_context_templates_sync_recursive_.output.js 457 bytes [rendered] > ./example.js 2:1-4:3 dependent modules 240 bytes [dependent] 3 modules ../require.context/templates/ sync ^\.\/.*$ 217 bytes [built] [code generated] [no exports] [used exports unknown] amd require context ./example.js 2:1-4:3 webpack X.X.X compiled successfullyProduction 模式
asset output.js 1.88 KiB [emitted] [minimized] (name: main) asset require_context_templates_sync_recursive_.output.js 652 bytes [emitted] [minimized] chunk (runtime: main) output.js (main) 251 bytes (javascript) 4.83 KiB (runtime) [entry] [rendered] > ./example.js main runtime modules 4.83 KiB 6 modules ./example.js 251 bytes [built] [code generated] [no exports used] entry ./example.js main chunk (runtime: main) require_context_templates_sync_recursive_.output.js 457 bytes [rendered] > ./example.js 2:1-4:3 dependent modules 240 bytes [dependent] 3 modules ../require.context/templates/ sync ^\.\/.*$ 217 bytes [built] [code generated] [no exports] amd require context ./example.js 2:1-4:3 webpack X.X.X compiled successfullyStats 中有几个值得关注的字段:
amd require context ./example.js 2:1-4:3:上下文依赖挂在入口模块第 2 行 1 列至第 4 行 3 列(正是require(["..."], function(tmpl) {...})的源码范围),类别为 AMD——这直接对应下面源码中的依赖类型;dependent modules 240 bytes 3 modules:a/b/c 三个模板文件作为上下文模块的“从属模块”列在该 chunk 下;- production 模式下主 chunk 从 8.72 KiB 压到 1.88 KiB(运行时模块压缩显著),异步 chunk 从 2.28 KiB 压到 652 bytes,且均标记
[minimized]。
源码级原理:动态 AMD require 如何变成上下文模块
解析阶段:processCallRequire 的分流逻辑
AMD 风格require(...)的解析入口是 AMDRequireDependenciesBlockParserPlugin。它在apply中挂到 JS 解析器的call for "require"钩子上:
apply(parser) { parser.hooks.call .for("require") .tap(PLUGIN_NAME, this.processCallRequire.bind(this, parser)); }processCallRequire(L293 起)先对第一个参数做parser.evaluateExpression静态求值,然后创建AMDRequireDependenciesBlock与AMDRequireDependency。当只有 1 个参数(模块请求数组)时进入processArray(L112-L120):
processArray(parser, expr, param) { if (param.isArray()) { for (const p of param.items) { const result = this.processItem(parser, expr, p); if (result === undefined) { this.processContext(parser, expr, p); } } return true; } else if (param.isConstArray()) { // 纯字面量数组:逐项生成静态 AMDRequireItemDependency ... } }分流的判断标准一目了然:
- 请求项是字符串字面量(或能静态求值为字符串,如条件表达式
"a"||"b"):processItem走静态路径,生成AMDRequireItemDependency或ConstDependency(require/module/exports被替换为运行时全局); - 请求项无法静态求值(如本例的
"../require.context/templates/"+templateName):processItem返回undefined,转而调用processContext(L235-L253):
processContext(parser, expr, param) { const dep = ContextDependencyHelpers.create( AMDRequireContextDependency, param.range, param, expr, this.options, { category: "amd" }, parser ); if (!dep) return; dep.loc = parser.getLocation(expr); dep.optional = Boolean(parser.scope.inTry); parser.state.current.addDependency(dep); return true; }它借助ContextDependencyHelpers.create把“基目录 + 正则”从求值结果中提取出来(本例得到../require.context/templates/与^\.\/.*$),创建AMDRequireContextDependency挂到AMDRequireDependenciesBlock上。后续由ContextModuleFactory构建上下文模块,resolve阶段扫描目录得到 map 表。
依赖类型:AMDRequireContextDependency
AMDRequireContextDependency 继承自通用ContextDependency,类型与类别定义如下:
get type() { return "amd require context"; // 即 stats 中看到的 "amd require context" } get category() { return "amd"; }文件末尾还指定了它的模板(L66):
AMDRequireContextDependency.Template = require("./ContextDependencyTemplateAsRequireCall");该模板负责在生成阶段把require(["..."+变量], cb)改写为产物中看到的__webpack_require__.e(chunkId).then(...)形态——即“异步确保 chunk + 同步 require 上下文模块”的组合,这也是本例产物中出现/* AMD require */注释标记的来源。
为什么上下文模块进了独立 chunk
CommonJS 的require("./dir/"+name)生成上下文模块后,模块本身通常留在所在 chunk;而AMD 带回调的require是异步语义(回调可能在下一 tick 才执行),webpack 因此把整个上下文模块划入独立的异步 chunk,运行时用__webpack_require__.e保证“回调触发前 chunk 已就绪”。这与 stats 中 chunk 归属(> ./example.js 2:1-4:3)及产物文件名一致。
与同类示例的关系
仓库中围绕动态模块加载提供了一组可对照的示例,便于横向理解:
- examples/require.context:CommonJS 同步
require("./templates/"+templateName),生成的上下文模块随所在 chunk 同步加载,不产生额外的 JSONP 请求; - examples/code-splitting-native-import-context 与 examples/code-splitting-native-import-context-filter:ESM 原生
import("./templates/"+name)形式,同样触发上下文模块,并可配合import.meta.glob式的过滤器缩小范围。
本例(AMD 形式)的独特之处在于:动态请求 + 异步回调二者叠加,演示了上下文模块与 JSONP chunk 加载的完整闭环;而optimization.chunkIds: "named"让异步 chunk 文件名(require_context_templates_sync_recursive_)在开发/生产构建间保持稳定,方便像本 README 这样直接引用产物文件名。
实践要点小结
- 任何“字符串拼接目录/文件名”的动态请求(AMD require、CommonJS require、
require.ensure、ESM 动态 import)都会被 webpack 收敛为上下文模块:整个匹配目录进包,运行时查map路由; - 上下文模块的目录与正则可以进一步约束(如加正则只匹配
^\.\/.*\.js$),以控制入包范围——本例默认正则^\.\/.*$匹配了目录下全部文件,因此未被使用的c.js也在产物中; - AMD 回调式 require 使上下文模块落入独立异步 chunk,加载走
__webpack_require__.e+ JSONP 回调(self["webpackChunk"].push),失败时抛ChunkLoadError; - 演示/测试场景下建议
chunkIds: "named",使异步 chunk 文件名稳定可预测,便于产物分析与回归对比。
进一步阅读建议:上下文依赖的通用基类 ContextDependency、请求字符串到“目录+正则”的提取逻辑 ContextDependencyHelpers、上下文模块构建 ContextModule 与 ContextModuleFactory,以及同步形式的对照示例 examples/require.context/README.md。
【免费下载链接】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),仅供参考