webpack 原生 `import()` 语法构建上下文模块(ContextModule):以动态模板目录为例的分包实战解析
发布时间:2026/9/8 20:50:05来源:尧图网络
webpack 原生import()语法构建上下文模块ContextModule以动态模板目录为例的分包实战解析【免费下载链接】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-splitting-native-import-context 为主体讲解如何在业务代码中借助 ECMAScript 原生import()语法构造ContextModule上下文模块让一个动态请求对应模板目录下的多个模块并把每个模块拆成独立的异步 chunk 按需加载。读完本文你将掌握动态import()在 webpack 中的解析规则、ContextModule 运行时生成形态、chunk 拆分与加载机制并能够把这一模式应用到语言包、主题模板、插件目录等需要“运行时才知道要加载谁”的真实场景。示例全貌三份模板、一个入口整个示例位于仓库根目录下的 examples/code-splitting-native-import-context文件结构如下example.js示例入口通过原生import()按运行时变量加载模板templates/三个模板模块foo.js、bar.js、baz.jswebpack.config.js示例的最小配置template.md生成示例 README 的模板源文件。模板文件的内容高度统一例如 templates/foo.jsvar foo foo; export default foo;bar.js 与 baz.js 结构一致只是把字符串换成各自的模块名。每个模板体积仅 38 字节这一数字可以从构建统计信息中直接验证却会被拆成三个互不相干的异步 chunk——这正是本例要演示的核心现象。核心入口代码动态请求如何触发上下文模块示例入口 example.js 的全部源码如下async function getTemplate(templateName) { try { let template await import(./templates/${templateName}); console.log(template); } catch(err) { console.error(template error); return new Error(err); } } getTemplate(foo); getTemplate(bar); getTemplate(baz);这段代码有两个值得注意的设计点import()的参数是运行时拼接的表达式./templates/${templateName}而非字面量路径。webpack 的静态分析在 lib/Parser.js 与 lib/javascript/JavascriptParser.js 中实现无法在编译期确定具体模块于是退而求其次把./templates/${templateName}解析为一个以./templates/为上下文目录的动态上下文请求凡是能被正则^\.\/.*$匹配到的、位于该目录下的模块全部进入“候选集合”等待运行时按名字挑选。async/await配合try/catch处理加载失败。一旦运行时传入的名字无法命中候选集合加载 Promise 会被 reject错误在catch分支捕获并打出template error。关于这一点后面生成的运行时函数里可以看到非常明确的报错路径MODULE_NOT_FOUND。编译后入口中的import()被改写为对上下文加载函数的调用template拿到的是整个模块的命名空间对象示例日志直接打印它因此能看到模块导出的内容。编译产物剖析ContextModule 的运行时形态在非优化development模式下构建后入口 chunk dist/output.js 内部首先注册了唯一的上下文模块其模块注释清晰标明了它的性质/*!***********************************************************************!*\ !*** ./templates/ lazy ^\.\/.*$ referencedExports: namespace object ***! \***********************************************************************/ /*! default exports */ /*! exports [not provided] [no usage info] */ /*! runtime requirements: __webpack_require__.e, module, __webpack_require__.o, __webpack_require__, __webpack_require__.* */注释中的lazy表示这是一个lazy 模式的 ContextModule每个候选模块都会进入独立异步 chunk^\.\/.*$是匹配“请求名”的正则。模块主体生成了一张请求名到模块的映射表map以及一个用于按需加载的webpackAsyncContext函数const map { ./bar: [2, [776]], ./bar.js:[2, [776]], ./baz: [3, [0]], ./baz.js:[3, [0]], ./foo: [4, [717]], ./foo.js:[4, [717]] }; function webpackAsyncContext(req) { try { if(!__webpack_require__.o(map, req)) { return Promise.resolve().then(() { const e new Error(Cannot find module req ); e.code MODULE_NOT_FOUND; throw e; }); } } catch(err) { return Promise.reject(err); } const ids map[req], id ids[0]; return __webpack_require__.e(ids[1][0]).then(() (__webpack_require__(id))); } webpackAsyncContext.keys () (Object.keys(map)); webpackAsyncContext.id 1; module.exports webpackAsyncContext;这段生成代码值得逐行拆解它是理解“原生import()上下文分包”的关键map 的键同时包含带扩展名与不带扩展名的请求。示例调用getTemplate(foo)webpack 会把./foo与./foo.js都登记为合法请求因此./foo、./bar、./baz三种裸名字都能命中每个键的值是[moduleId, [chunkId]]。moduleId是被加载模块在模块表中的编号chunkId是该模板专属异步 chunk 的编号。可以看到foo家族的chunkId是 717bar家族是 776baz家族是 0——三个模板被拆成了三个完全独立的 chunk__webpack_require__.e(chunkId)是“确保 chunk 已加载”的运行时入口ensure chunk它负责发起异步加载并返回 Promise加载完成后通过__webpack_require__(id)真正执行目标模块并返回其导出当req不在 map 中时例如运行时传入了不存在的模板名会以MODULE_NOT_FOUND错误 reject——这正是示例代码try/catch所要兜底的场景。入口 chunk 侧的调用webpackAsyncContext注册为模块 1入口 example.js 被改写为调用__webpack_require__(1)返回的上下文函数async function getTemplate(templateName) { try { let template await __webpack_require__(1)(./${templateName}); console.log(template); } catch(err) { console.error(template error); return new Error(err); } }注意这里传给上下文函数的请求被修正为./${templateName}与 map 中./foo、./bar、./baz的键一一对应。运行时chunk 是如何被拉取的__webpack_require__.e的实现依赖若干 webpack 运行时模块这些运行时代码在 README 中被折叠展示属于生成产物而非手写逻辑包括ensure chunk / jsonp chunk loading__webpack_require__.f.j维护installedChunks状态表——undefined表示未加载、null表示预加载/预取中、Promise 数组表示加载中、0表示已就绪首次加载时创建 Promise 并调用__webpack_require__.lload script__webpack_require__.l动态创建script标签并挂到document.head附带 120000ms 超时与脚本复用逻辑这就是浏览器端异步 chunk 的基本加载方式publicPath 与文件名本示例产物publicPath为dist/chunk 文件名格式为chunkId .output.js见运行时__webpack_require__.u因此浏览器实际请求的是dist/0.output.js、dist/717.output.js、dist/776.output.jsJSONP 回调全局self[webpackChunk]数组被替换为安装回调的 push 方法异步 chunk 加载成功后通过它把模块表合并进__webpack_require__.m并 resolve 对应 Promise。构建统计开发模式与生产模式的差异README 中Info章节同时给出了两种模式的完整构建统计这是理解产物形态的第一手数据。Unoptimized开发/默认模式asset output.js 10.7 KiB [emitted] (name: main) asset 717.output.js 846 bytes [emitted] asset 776.output.js 846 bytes [emitted] asset 0.output.js 844 bytes [emitted] chunk (runtime: main) 0.output.js 38 bytes [rendered] ./baz ./templates/ lazy ^\.\/.*$ referencedExports: namespace object ./baz ./baz.js ./templates/ lazy ^\.\/.*$ referencedExports: namespace object ./baz.js ./templates/baz.js 38 bytes [optional] [built] [code generated] chunk (runtime: main) 717.output.js 38 bytes [rendered] ./foo ... chunk (runtime: main) 776.output.js 38 bytes [rendered] ./bar ... chunk (runtime: main) output.js (main) 441 bytes (javascript) 5.34 KiB (runtime) [entry] [rendered] webpack X.X.X compiled successfullyProduction modeasset output.js 2.61 KiB [emitted] [minimized] (name: main) asset 717.output.js 117 bytes [emitted] [minimized] asset 776.output.js 117 bytes [emitted] [minimized] asset 0.output.js 114 bytes [emitted] [minimized] ... chunk (runtime: main) output.js (main) 441 bytes (javascript) 5.71 KiB (runtime) [entry] [rendered]从统计中可以读出几条关键信息每个模板都形成独立 chunk0.output.js、717.output.js、776.output.js三个异步 chunk各只包含一个 38 字节的模块模块源码体积真正做到“用哪个才加载哪个”开发模式产物体积更大主入口含完整运行时与上下文模块约 10.7 KiB每个异步 chunk 约 844~846 字节模块 大量注释/包装代码生产模式经过压缩后主入口 2.61 KiB异步 chunk 仅 114~117 字节统计中还出现 ./foo ./templates/ lazy ...与 ./foo.js ...两条“父子”请求记录再次印证 map 中同时登记了带/不带扩展名两种请求名。为什么模板数量不写死也能打包底层原理示例之所以能做到“新增一个模板文件无需改动入口”根因是 webpack 的ContextModule机制。从源码结构看相关实现分布在lib/ContextModule.jsContextModule类定义于 143 行附近它把“目录 匹配正则 加载模式”整体建模为一个模块lib/ContextModuleFactory.js根据上下文依赖去扫描目录、解析test/include/exclude等过滤选项并产出实际的ContextModulelib/ContextModuleFactory.js 依赖的 lib/dependencies 目录中的ContextDependency、ContextElementDependency等依赖类型用于把每个候选模块与上下文模块建立图连接。可以推断的整体工作流大致为JavascriptParser在遇到无法静态求值的import()表达式时把表达式拆成前缀路径 变量并注册一个上下文依赖 →ContextModuleFactory依据前缀目录与正则此例为^\.\/.*$收集候选文件 → 为每个候选创建ContextElementDependency与独立的AsyncDependenciesBlocklazy 模式因此每个候选天然拥有自己的异步 chunk →ContextModule在代码生成阶段输出前文那张 map 与webpackAsyncContext。也正因为候选与请求的映射被编码进产物webpack 才能在不支持反射的浏览器环境中实现“按字符串找模块”。关于文件匹配正则README 产物注释显示的是^\.\/.*$即该上下文目录下的任意请求都会被接受示例本身没有使用任何过滤魔法注释。过滤与变体本示例在仓库中的姊妹篇如果你希望只把满足条件的文件纳入上下文例如排除.noimport.js这类内部文件可以在import()的魔法注释中声明webpackInclude与webpackExclude两个正则。仓库中的 examples/code-splitting-native-import-context-filter 正是这一主题的进阶版其写法为let template await import( /* webpackInclude: /\.js$/ */ /* webpackExclude: /\.noimport\.js$/ */ ./templates/${templateName} );该示例的模板目录中混有foo.noimport.js等文件但构建结果里上下文模块注释会变成./templates/ lazy ^\.\/.*$ include: \.js$ exclude: \.noimport\.js$ ...map 中只保留六个合法请求名.noimport.js文件完全不会进入产物。这也揭示了上下文模块正则匹配的落地位置过滤发生在ContextModuleFactory 收集候选的阶段而不是运行时。示例配置与可复现验证示例自带的 webpack.config.js 非常克制只有一个选项/** type {import(webpack).Configuration} */ const config { optimization: { chunkIds: deterministic // To keep filename consistent between different modes (for example building only) } }; module.exports config;注释解释了它的目的chunkIds: deterministic让 chunk 编号在开发/生产等不同模式下保持一致否则相同内容的 chunk 可能被分配不同的数字 id这也是两种模式的产物能使用同一组0/717/776chunk 编号的前提。在仓库内验证本示例的方式有两个单元/集成测试体系webpack 官方仓库通过 test/Examples.test.js 遍历加载examples目录下的全部示例——该测试以示例目录为context将输出目录指向示例目录下的dist、设置publicPath为dist/、开启pathinfo然后调用 webpack 编译并断言“编译成功”。本示例对应的用例名包含code-splitting-native-import-context可通过 Jest 的用例名过滤单独执行仓库依赖安装完成后在根目录运行类似npx jest Examples --testNamePatterncode-splitting-native-import-context的方式定位它本地手动构建将 example.js 作为入口、以示例目录为上下文并加载其 webpack.config.js按默认development模式构建可复现Info/Unoptimized的统计按生产模式modeproduction构建可复现Info/Production mode的统计。README 中展示的dist/output.js正是该构建产物的快照。小结与适用场景原生import() 动态表达式把“编译期静态分析”与“运行时按名取模块”衔接起来对开发者在形式上只是普通的标准语法对 webpack 而言则会生成一个带映射表的 ContextModule并把每个候选模块独立拆包。示例中三个模板各自成 chunk、运行时只请求真正命中的文件、请求不合法时统一抛MODULE_NOT_FOUND这一整套行为都可以直接从本仓库的产物与源码中逐一验证。它非常适合这类需求候选文件在某个固定目录下、数量持续增长、但加载动作由运行时变量触发。常见落地形态包括多语言文案包、主题/皮肤模板、文档渲染器的内容片段、以及按插件名加载的模块注册表等。在使用时只需把握三点边界——请求必须能映射到某个静态目录前缀、候选文件应能通过正则表达必要时用webpackInclude/webpackExclude收窄范围、以及给运行时加载失败预留错误处理分支即可在几乎零配置的前提下获得细粒度的按需分包能力。【免费下载链接】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),仅供参考
网站建设高端定制企业官网