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。产物一主 chunkdist/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_)向运行时申请加载指定的异步 chunkJSONPscript注入返回 Promise。AMD require 的回调形式天然就是异步语义因此上下文模块被整体放入异步 chunk 而不是主 chunk__webpack_require__(1)(./templateName)chunk 就绪后同步 require 模块 id 为1的上下文模块并以./templateName为 key 查表__WEBPACK_AMD_REQUIRE_ARRAY__模拟 AMD 回调参数注入保持原有function(tmpl) {...}的调用形态catchchunk 加载失败如网络错误时统一抛出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。产物二异步 chunkdist/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两种写法都映射到真实模块 id2/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 无法知道运行时只会用到哪几个。构建统计输出StatsUnoptimized 模式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 modulesa/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)); }processCallRequireL293 起先对第一个参数做parser.evaluateExpression静态求值然后创建AMDRequireDependenciesBlock与AMDRequireDependency。当只有 1 个参数模块请求数组时进入processArrayL112-L120processArray(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||bprocessItem走静态路径生成AMDRequireItemDependency或ConstDependencyrequire/module/exports被替换为运行时全局请求项无法静态求值如本例的../require.context/templates/templateNameprocessItem返回undefined转而调用processContextL235-L253processContext(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 表。依赖类型AMDRequireContextDependencyAMDRequireContextDependency 继承自通用ContextDependency类型与类别定义如下get type() { return amd require context; // 即 stats 中看到的 amd require context } get category() { return amd; }文件末尾还指定了它的模板L66AMDRequireContextDependency.Template require(./ContextDependencyTemplateAsRequireCall);该模板负责在生成阶段把require([...变量], cb)改写为产物中看到的__webpack_require__.e(chunkId).then(...)形态——即“异步确保 chunk 同步 require 上下文模块”的组合这也是本例产物中出现/* AMD require */注释标记的来源。为什么上下文模块进了独立 chunkCommonJS 的require(./dir/name)生成上下文模块后模块本身通常留在所在 chunk而AMD 带回调的require是异步语义回调可能在下一 tick 才执行webpack 因此把整个上下文模块划入独立的异步 chunk运行时用__webpack_require__.e保证“回调触发前 chunk 已就绪”。这与 stats 中 chunk 归属 ./example.js 2:1-4:3及产物文件名一致。与同类示例的关系仓库中围绕动态模块加载提供了一组可对照的示例便于横向理解examples/require.contextCommonJS 同步require(./templates/templateName)生成的上下文模块随所在 chunk 同步加载不产生额外的 JSONP 请求examples/code-splitting-native-import-context 与 examples/code-splitting-native-import-context-filterESM 原生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),仅供参考