若是以为文章有用,欢迎点赞关注,但写做实属不易,未经做者赞成,禁止任何形式转载!!!
Webpack 特别难学!!!javascript
时至 5.0 版本以后,Webpack 功能集变得很是庞大,包括:模块打包、代码分割、按需加载、HMR、Tree-shaking、文件监听、sourcemap、Module Federation、devServer、DLL、多进程等等,为了实现这些功能,webpack 的代码量已经到了惊人的程度:css
在这个数量级下,源码的阅读、分析、学习成本很是高,加上 webpack 官网语焉不详的文档,致使 webpack 的学习、上手成本极其高。为此,社区围绕着 Webpack 衍生出了各类手脚架,好比 vue-cli、create-react-app,解决“用”的问题。html
但这又致使一个新的问题,大部分人在工程化方面逐渐变成一个配置工程师,停留在“会用会配”可是不知道黑盒里面究竟是怎么转的阶段,遇到具体问题就瞎了:vue
究其缘由仍是对 webpack 内部运行机制没有造成必要的总体认知,没法迅速定位问题 —— 对,连问题的本质都经常看不出,所谓的不能透过现象看本质,那本质是啥?我我的将 webpack 整个庞大的体系抽象为三方面的知识:java
三者协做构成 webpack 的主体框架:node
理解了这三块内容就算是入了个门,对 Webpack 有了一个最最基础的认知了,工做中再遇到问题也就能按图索骥了。补充一句,做为一份入门教程,本文不会展开太多 webpack 代码层面的细节 —— 个人精力也不容许,因此读者也不须要看到一堆文字就产生特别大的心理负担。react
首先,咱们要理解一个点,Webpack 最核心的功能:webpack
At its core, webpack is a static module bundler for modern JavaScript applications.
也就是将各类类型的资源,包括图片、css、js等,转译、组合、拼接、生成 JS 格式的 bundler 文件。官网首页的动画很形象地表达了这一点:git
这个过程核心完成了 内容转换 + 资源合并 两种功能,实现上包含三个阶段:github
初始化阶段:
Compiler
对象compiler
对象的 run
方法entry
找出全部的入口文件,调用 compilition.addEntry
将入口文件转换为 dependence
对象构建阶段:
entry
对应的 dependence
建立 module
对象,调用 loader
将模块转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再 递归 本步骤直到全部入口依赖的文件都通过了本步骤的处理生成阶段:
Chunk
,再把每一个 Chunk
转换成一个单独的文件加入到输出列表,这步是能够修改输出内容的最后机会单次构建过程自上而下按顺序执行,下面会展开聊聊细节,在此以前,对上述说起的各种技术名词不太熟悉的同窗,能够先看看简介:
Entry
:编译入口,webpack 编译的起点Compiler
:编译管理器,webpack 启动后会建立 compiler
对象,该对象一直存活知道结束退出Compilation
:单次编辑过程的管理器,好比 watch = true
时,运行过程当中只有一个 compiler
但每次文件变动触发从新编译时,都会建立一个新的 compilation
对象Dependence
:依赖对象,webpack 基于该类型记录模块间依赖关系Module
:webpack 内部全部资源都会以“module”对象形式存在,全部关于资源的操做、转译、合并都是以 “module” 为基本单位进行的Chunk
:编译完成准备输出时,webpack 会将 module
按特定的规则组织成一个一个的 chunk
,这些 chunk
某种程度上跟最终输出一一对应Loader
:资源内容转换器,其实就是实现从内容 A 转换 B 的转换器Plugin
:webpack构建过程当中,会在特定的时机广播对应的事件,插件监听这些事件,在特定时间点介入编译过程webpack 编译过程都是围绕着这些关键对象展开的,更详细完整的信息,能够参考 Webpack 知识图谱 。
学习一个项目的源码一般都是从入口开始看起,按图索骥慢慢摸索出套路的,因此先来看看 webpack 的初始化过程:
解释一下:
process.args + webpack.config.js
合并成用户配置validateSchema
校验配置getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults
合并出最终配置compiler
对象plugins
集合,执行插件的 apply
方法new WebpackOptionsApply().process
方法,加载各类内置插件主要逻辑集中在 WebpackOptionsApply
类,webpack 内置了数百个插件,这些插件并不须要咱们手动配置,WebpackOptionsApply
会在初始化阶段根据配置内容动态注入对应的插件,包括:
EntryOptionPlugin
插件,处理 entry
配置devtool
值判断后续用那个插件处理 sourcemap
,可选值:EvalSourceMapDevToolPlugin
、SourceMapDevToolPlugin
、EvalDevToolModulePlugin
RuntimePlugin
,用于根据代码内容动态注入 webpack 运行时到这里,compiler
实例就被建立出来了,相应的环境参数也预设好了,紧接着开始调用 compiler.compile
函数:
// 取自 webpack/lib/compiler.js compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { // ... const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, err => { // ... this.hooks.finishMake.callAsync(compilation, err => { // ... process.nextTick(() => { compilation.finish(err => { compilation.seal(err => {...}); }); }); }); }); }); }
Webpack 架构很灵活,但代价是牺牲了源码的直观性,好比说上面说的初始化流程,从建立 compiler
实例到调用 make
钩子,逻辑链路很长:
lib/webpack.js
文件中 createCompiler
方法createCompiler
方法内部调用 WebpackOptionsApply
插件WebpackOptionsApply
定义在 lib/WebpackOptionsApply.js
文件,内部根据 entry
配置决定注入 entry
相关的插件,包括:DllEntryPlugin
、DynamicEntryPlugin
、EntryPlugin
、PrefetchPlugin
、ProgressPlugin
、ContainerPlugin
Entry
相关插件,如 lib/EntryPlugin.js
的 EntryPlugin
监听 compiler.make
钩子lib/compiler.js
的 compile
函数内调用 this.hooks.make.callAsync
EntryPlugin
的 make
回调,在回调中执行 compilation.addEntry
函数compilation.addEntry
函数内部通过一坨与主流程无关的 hook
以后,再调用 handleModuleCreate
函数,正式开始构建内容这个过程须要在 webpack 初始化的时候预埋下各类插件,经历 4 个文件,7次跳转才开始进入主题,前戏太足了,若是读者对 webpack 的概念、架构、组件没有足够了解时,源码阅读过程会很痛苦。
关于这个问题,我在文章最后总结了一些技巧和建议,有兴趣的能够滑到附录阅读模块。
你有没有思考过这样的问题:
这些问题,基本上在构建阶段都能看出一些端倪。构建阶段从 entry
开始递归解析资源与资源的依赖,在 compilation
对象内逐步构建出 module
集合以及 module
之间的依赖关系,核心流程:
解释一下,构建阶段从入口文件开始:
handleModuleCreate
,根据文件类型构建 module
子类runLoaders
转译 module
内容,一般是从各种资源类型转译为 JavaScript 文本遍历 AST,触发各类钩子
HarmonyExportDependencyParserPlugin
插件监听 exportImportSpecifier
钩子,解读 JS 文本对应的资源依赖module
对象的 addDependency
将依赖对象加入到 module
依赖列表中module.handleParseResult
处理模块依赖module
新增的依赖,调用 handleModuleCreate
,控制流回到第一步这个过程当中数据流 module => ast => dependences => module
,先转 AST 再从 AST 找依赖。这就要求 loaders
处理完的最后结果必须是能够被 acorn 处理的标准 JavaScript 语法,好比说对于图片,须要从图像二进制转换成相似于 export default "data:image/png;base64,xxx"
这类 base64 格式或者 export default "http://xxx"
这类 url 格式。
compilation
按这个流程递归处理,逐步解析出每一个模块的内容以及 module
依赖关系,后续就能够根据这些内容打包输出。
假若有以下图所示的文件依赖树:
其中 index.js
为 entry
文件,依赖于 a/b 文件;a 依赖于 c/d 文件。初始化编译环境以后,EntryPlugin
根据 entry
配置找到 index.js
文件,调用 compilation.addEntry
函数触发构建流程,构建完毕后内部会生成这样的数据结构:
此时获得 module[index.js]
的内容以及对应的依赖对象 dependence[a.js]
、dependence[b.js]
。OK,这就获得下一步的线索:a.js、b.js,根据上面流程图的逻辑继续调用 module[index.js]
的 handleParseResult
函数,继续处理 a.js、b.js 文件,递归上述流程,进一步获得 a、b 模块:
从 a.js 模块中又解析到 c.js/d.js 依赖,因而再再继续调用 module[a.js]
的 handleParseResult
,再再递归上述流程:
到这里解析完全部模块后,发现没有更多新的依赖,就能够继续推动,进入下一步。
回顾章节开始时提到的问题:
Webpack 编译过程会将源码解析为 AST 吗?webpack 与 babel 分别实现了什么?
Webpack 编译过程当中,如何识别资源对其余资源的依赖?
require/ import
之类的导入语句,肯定模块对其余资源的依赖关系相对于 grant、gulp 等流式构建工具,为何 webpack 会被认为是新一代的构建工具?
构建阶段围绕 module
展开,生成阶段则围绕 chunks
展开。通过构建阶段以后,webpack 获得足够的模块内容与模块关系信息,接下来开始生成最终资源了。代码层面,就是开始执行 compilation.seal
函数:
// 取自 webpack/lib/compiler.js compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { // ... const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, err => { // ... this.hooks.finishMake.callAsync(compilation, err => { // ... process.nextTick(() => { compilation.finish(err => { **compilation.seal**(err => {...}); }); }); }); }); }); }
seal
原意密封、上锁,我我的理解在 webpack 语境下接近于 “将模块装进蜜罐” 。seal
函数主要完成从 module
到 chunks
的转化,核心流程:
简单梳理一下:
ChunkGraph
对象;compilation.modules
集合,将 module
按 entry/动态引入
的规则分配给不一样的 Chunk
对象;compilation.modules
集合遍历完毕后,获得完整的 chunks
集合对象,调用 createXxxAssets
方法createXxxAssets
遍历 module/chunk
,调用 compilation.emitAssets
方法将资 assets
信息记录到 compilation.assets
对象中seal
回调,控制流回到 compiler
对象这一步的关键逻辑是将 module
按规则组织成 chunks
,webpack 内置的 chunk
封装规则比较简单:
entry
及 entry 触达到的模块,组合成一个 chunk
chunk
chunk
是输出的基本单位,默认状况下这些 chunks
与最终输出的资源一一对应,那按上面的规则大体上能够推导出一个 entry
会对应打包出一个资源,而经过动态引入语句引入的模块,也对应会打包出相应的资源,咱们来看个示例。
假若有这样的配置:
const path = require("path"); module.exports = { mode: "development", context: path.join(__dirname), entry: { a: "./src/index-a.js", b: "./src/index-b.js", }, output: { filename: "[name].js", path: path.join(__dirname, "./dist"), }, devtool: false, target: "web", plugins: [], };
实例配置中有两个入口,对应的文件结构:
index-a
依赖于c,且动态引入了 e;index-b
依赖于 c/d 。根据上面说的规则:
entry
及entry触达到的模块,组合成一个 chunk生成的 chunks
结构为:
也就是根据依赖关系,chunk[a]
包含了 index-a/c
两个模块;chunk[b]
包含了 c/index-b/d
三个模块;chunk[e-hash]
为动态引入 e
对应的 chunk。
不知道你们注意到没有,chunk[a]
与 chunk[b]
同时包含了 c,这个问题放到具体业务场景可能就是,一个多页面应用,全部页面都依赖于相同的基础库,那么这些全部页面对应的 entry
都会包含有基础库代码,这岂不浪费?为了解决这个问题,webpack 提供了一些插件如 CommonsChunkPlugin
、SplitChunksPlugin
,在基本规则以外进一步优化 chunks
结构。
SplitChunksPlugin
的做用SplitChunksPlugin
是 webpack 架构高扩展的一个绝好的示例,咱们上面说了 webpack 主流程里面是按 entry / 动态引入
两种状况组织 chunks
的,这必然会引起一些没必要要的重复打包,webpack 经过插件的形式解决这个问题。
回顾 compilation.seal
函数的代码,大体上能够梳理成这么4个步骤:
compilation.modules
,记录下模块与 chunk
关系module
构建 chunk 集合上面 1-3 都是预处理 + chunks 默认规则的实现,不在咱们讨论范围,这里重点关注第4个步骤触发的 optimizeChunks
钩子,这个时候已经跑完主流程的逻辑,获得 chunks
集合,SplitChunksPlugin
正是使用这个钩子,分析 chunks
集合的内容,按配置规则增长一些通用的 chunk :
module.exports = class SplitChunksPlugin { constructor(options = {}) { // ... } _getCacheGroup(cacheGroupSource) { // ... } apply(compiler) { // ... compiler.hooks.thisCompilation.tap("SplitChunksPlugin", (compilation) => { // ... compilation.hooks.optimizeChunks.tap( { name: "SplitChunksPlugin", stage: STAGE_ADVANCED, }, (chunks) => { // ... } ); }); } };
理解了吗?webpack 插件架构的高扩展性,使得整个编译的主流程是能够固化下来的,分支逻辑和细节需求“外包”出去由第三方实现,这套规则架设起了庞大的 webpack 生态,关于插件架构的更多细节,下面 plugin
部分有详细介绍,这里先跳过。
通过构建阶段后,compilation
会获知资源模块的内容与依赖关系,也就知道“输入”是什么;而通过 seal
阶段处理后, compilation
则获知资源输出的图谱,也就是知道怎么“输出”:哪些模块跟那些模块“绑定”在一块儿输出到哪里。seal
后大体的数据结构:
compilation = { // ... modules: [ /* ... */ ], chunks: [ { id: "entry name", files: ["output file name"], hash: "xxx", runtime: "xxx", entryPoint: {xxx} // ... }, // ... ], };
seal
结束以后,紧接着调用 compiler.emitAssets
函数,函数内部调用 compiler.outputFileSystem.writeFile
方法将 assets
集合写入文件系统,实现逻辑比较曲折,可是与主流程没有太多关系,因此这里就不展开讲了。
OK,上面已经把逻辑层面的构造主流程梳理完了,这里结合资源形态流转的角度从新考察整个过程,加深理解:
compiler.make
阶段:
entry
文件以 dependence
对象形式加入 compilation
的依赖列表,dependence
对象记录有 entry
的类型、路径等信息dependence
调用对应的工厂函数建立 module
对象,以后读入 module
对应的文件内容,调用 loader-runner
对内容作转化,转化结果如有其它依赖则继续读入依赖资源,重复此过程直到全部依赖均被转化为 module
compilation.seal
阶段:
module
集合,根据 entry
配置及引入资源的方式,将 module
分配到不一样的 chunk
chunk
集合,调用 compilation.emitAsset
方法标记 chunk
的输出规则,即转化为 assets
集合compiler.emitAssets
阶段:
assets
写入文件系统网上很多资料将 webpack 的插件架构归类为“事件/订阅”模式,我认为这种概括有失偏颇。订阅模式是一种松耦合架构,发布器只是在特定时机发布事件消息,订阅者并不或者不多与事件直接发生交互,举例来讲,咱们日常在使用 HTML 事件的时候不少时候只是在这个时机触发业务逻辑,不多调用上下文操做。而 webpack 的钩子体系是一种强耦合架构,它在特定时机触发钩子时会附带上足够的上下文信息,插件定义的钩子回调中,能也只能与这些上下文背后的数据结构、接口交互产生 side effect,进而影响到编译状态和后续流程。
学习插件架构,须要理解三个关键问题:
从形态上看,插件一般是一个带有 apply
函数的类:
class SomePlugin { apply(compiler) { } }
apply
函数运行时会获得参数 compiler
,以此为起点能够调用 hook
对象注册各类钩子回调,例如: compiler.hooks.make.tapAsync
,这里面 make
是钩子名称,tapAsync
定义了钩子的调用方式,webpack 的插件架构基于这种模式构建而成,插件开发者可使用这种模式在钩子回调中,插入特定代码。webpack 各类内置对象都带有 hooks
属性,好比 compilation
对象:
class SomePlugin { apply(compiler) { compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => { compilation.hooks.optimizeChunkAssets.tapAsync('SomePlugin', ()=>{}); }) } }
钩子的核心逻辑定义在 Tapable 仓库,内部定义了以下类型的钩子:
const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable");
不一样类型的钩子根据其并行度、熔断方式、同步异步,调用方式会略有不一样,插件开发者须要根据这些的特性,编写不一样的交互逻辑,这部份内容也特别多,回头展开聊聊。
了解 webpack 插件的基本形态以后,接下来须要弄清楚一个问题:webpack 会在什么时间节点触发什么钩子?这一块我认为是知识量最大的一部分,毕竟源码里面有237个钩子,但官网只介绍了不到100个,且官网对每一个钩子的说明都太简短,就我我的而言看完并无太大收获,因此有必要展开聊一下这个话题。先看几个例子:
compiler.hooks.compilation
:
compiler.hooks.make
:
compilation
对象EntryPlugin
基于此钩子实现 entry
模块的初始化compilation.hooks.optimizeChunks
:
seal
函数中,chunk
集合构建完毕后触发chunks
集合与 chunkGroups
集合SplitChunksPlugin
插件基于此钩子实现 chunk
拆分优化compiler.hooks.done
:
stats
对象,包含编译过程当中的各种统计信息webpack-bundle-analyzer
插件基于此钩子实现打包分析这是我总结的钩子的三个学习要素:触发时机、传递参数、示例代码。
触发时机与 webpack 工做过程紧密相关,大致上从启动到结束,compiler
对象逐次触发以下钩子:
而 compilation
对象逐次触发:
因此,理解清楚前面说的 webpack 工做的主流程,基本上就能够捋清楚“何时会触发什么钩子”。
传递参数与具体的钩子强相关,官网对这方面没有作出进一步解释,个人作法是直接在源码里面搜索调用语句,例如对于 compilation.hooks.optimizeTree
,能够在 webpack 源码中搜索 hooks.optimizeTree.call
关键字,就能够找到调用代码:
// lib/compilation.js#2297 this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => { });
结合代码所在的上下文,能够判断出此时传递的是通过优化的 chunks
及 modules
集合。
Webpack 的钩子复杂程度不一,我认为最好的学习方法仍是带着目的去查询其余插件中如何使用这些钩子。例如,在 compilation.seal
函数内部有 optimizeModules
和 afterOptimizeModules
这一对看起来很对偶的钩子,optimizeModules
从字面上能够理解为用于优化已经编译出的 modules
,那 afterOptimizeModules
呢?
从 webpack 源码中惟一搜索到的用途是 ProgressPlugin
,大致上逻辑以下:
compilation.hooks.afterOptimizeModules.intercept({ name: "ProgressPlugin", call() { handler(percentage, "sealing", title); }, done() { progressReporters.set(compiler, undefined); handler(percentage, "sealing", title); }, result() { handler(percentage, "sealing", title); }, error() { handler(percentage, "sealing", title); }, tap(tap) { // p is percentage from 0 to 1 // args is any number of messages in a hierarchical matter progressReporters.set(compilation.compiler, (p, ...args) => { handler(percentage, "sealing", title, tap.name, ...args); }); handler(percentage, "sealing", title, tap.name); } });
基本上能够猜想出,afterOptimizeModules
的设计初衷就是用于通知优化行为的结束。
apply
虽然是一个函数,可是从设计上就只有输入,webpack 不 care 输出,因此在插件中只能经过调用类型实体的各类方法来或者更改实体的配置信息,变动编译行为。例如:
到这里,插件的工做机理和写法已经有一个很粗浅的介绍了,回头单拎出来细讲吧。
解决上述两个问题以后,咱们就能理解“如何将特定逻辑插入 webpack 编译过程”,接下来才是重点 —— 如何影响编译状态?强调一下,webpack 的插件体系与日常所见的 订阅/发布 模式差异很大,是一种很是强耦合的设计,hooks 回调由 webpack 决定什么时候,以何种方式执行;而在 hooks 回调内部能够经过修改状态、调用上下文 api 等方式对 webpack 产生 side effect。
好比,EntryPlugin
插件:
class EntryPlugin { apply(compiler) { compiler.hooks.compilation.tap( "EntryPlugin", (compilation, { normalModuleFactory }) => { compilation.dependencyFactories.set( EntryDependency, normalModuleFactory ); } ); compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => { const { entry, options, context } = this; const dep = EntryPlugin.createDependency(entry, options); compilation.addEntry(context, dep, options, (err) => { callback(err); }); }); } }
上述代码片断调用了两个影响 compilation
对象状态的接口:
compilation.dependencyFactories.set
compilation.addEntry
操做的具体含义能够先忽略,这里要理解的重点是,webpack 会将上下文信息以参数或 this
(compiler 对象) 形式传递给钩子回调,在回调中能够调用上下文对象的方法或者直接修改上下文对象属性的方式,对原定的流程产生 side effect。因此想纯熟地编写插件,除了要理解调用时机,还须要了解咱们能够用哪一些api,例如:
compilation.addModule
:添加模块,能够在原有的 module
构建规则以外,添加自定义模块compilation.emitAsset
:直译是“提交资产”,功能能够理解将内容写入到特定路径compilation.addEntry
:添加入口,功能上与直接定义 entry
配置相同module.addError
:添加编译错误信息Loader 的做用和实现比较简单,容易理解,因此简单介绍一下就好了。回顾 loader 在编译流程中的生效的位置:
流程图中, runLoaders
会调用用户所配置的 loader 集合读取、转译资源,此前的内容能够千奇百怪,但转译以后理论上应该输出标准 JavaScript 文本或者 AST 对象,webpack 才能继续处理模块依赖。
理解了这个基本逻辑以后,loader 的职责就比较清晰了,不外乎是将内容 A 转化为内容 B,可是在具体用法层面还挺多讲究的,有 pitch、pre、post、inline 等概念用于应对各类场景。
为了帮助理解,这里补充一个示例: Webpack 案例 -- vue-loader 原理分析。
ndb
单点调试功能追踪程序的运行,虽然 node 的调试有不少种方法,可是我我的更推荐 ndb
,灵活、简单,配合 debugger
语句是大杀器compiler + compilation + plugins
,webpack 运行过程当中只会有一个 compiler
;而每次编译 —— 包括调用 compiler.run
函数或者 watch = true
时文件发生变动,都会建立一个 compilation
对象。理解这三个核心对象的设计、职责、协做,差很少就能理解 webpack 的核心逻辑了抓大放小: plugin 的关键是“钩子”,我建议战略上重视,战术上忽视!钩子毕竟是 webpack 的关键概念,是整个插件机制的根基,学习 webpack 根本不可能绕过钩子,可是相应的逻辑跳转实在太绕太不直观了,看代码的时候一直揪着这个点的话,复杂性会剧增,个人经验是:
tapable
的源码,理解同步钩子、异步钩子、promise 钩子、串行钩子、并行钩子等概念,对 tapable
提供的事件模型有一个较为精细的认知,这叫战略上重视debugger
语句单点调试,等你缕清后续逻辑的时候,大几率你也知道钩子的含义了,这叫战术上忽视保持好奇心:学习过程保持旺盛的好奇心和韧性,善于 \& 勇于提出问题,而后基于源码和社区资料去总结出本身的答案,问题可能会不少,好比:
compilation.seal
函数内部设计了不少优化型的钩子,为何须要区分的这么细?webpack 设计者对不一样钩子有什么预期?module
子类?这些子类分别在何时被使用?Module
与 Module
子类从上文能够看出,webpack 构建阶段的核心流程基本上都围绕着 module
展开,相信接触过、用过 Webpack 的读者对 module
应该已经有一个感性认知,可是实现上 module
的逻辑是很是复杂繁重的。
以 webpack\@5.26.3 为例,直接或间接继承自 Module
(webpack/lib/Module.js
文件) 的子类有54个:
没法复制加载中的内容
要一个一个捋清楚这些类的做用实在太累了,咱们须要抓住本质:module
的做用是什么?
module
是 webpack 资源处理的基本单位,能够认为 webpack 对资源的路径解析、读入、转译、分析、打包输出,全部操做都是围绕着 module 展开的。有不少文章会说 module = 文件, 其实这种说法并不许确,好比子类 AsyncModuleRuntimeModule
就只是一段内置的代码,是一种资源而不能简单等价于实际文件。
Webpack 扩展性很强,包括模块的处理逻辑上,好比说入口文件是一个普通的 js,此时首先建立 NormalModule 对象,在解析 AST 时发现这个文件里还包含了异步加载语句,例如 requere.ensure
,那么相应地会建立 AsyncModuleRuntimeModule
模块,注入异步加载的模板代码。上面类图的 54 个 module 子类都是为适配各类场景设计的。