webpack中的Compilation
前言
官方链接
webpack
中真正的”编译器执行者”,Compilation
实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。 它会对应用程序的依赖图中所有模块, 进行字面上的编译(literal compilation)。 在编译阶段,模块会被加载(load
)、封存(seal
)、优化(optimize
)、 分块(chunk
)、哈希(hash
)和重新创建(restore
)。
👇 是关于Compilation
对象的组成结构:
Compilation的工作流程是怎样的呢?
👽 其实
Compilation
自身并没有做任何的动作,而是一堆的插件它们负责来实现的!因为Compiler
创建完这个Compilation
对象之后,就没有针对这个Compilation
对象进行调用其相关的API了,而是将Compilation
以及params
作为参数,来触发Complier.hooks.thisCompilation
首先,针对一个上面最简单的webpack.config.js
进行一个针对thisCompilation
事件的一个插件执行队列分析,从上图中我们可以对应整理对应的执行队列:
👇 表中的”是否webpack内置集成”,指的是在WebpackOptionsApply.js
中默认集成的!
插件名称 | 是否webpack内置集成 | 描述 |
---|---|---|
ArrayPushCallbackChunkFormatPlugin | ✅ | 格式化输出*.js 中的内容执行者 |
JsonpChunkLoadingPlugin | ❌ | |
StartupChunkDependenciesPlugin | ❌ | |
FetchCompileWasmPlugin | ❌ | |
FetchCompileAsyncWasmPlugin | ❌ | |
WorkerPlugin | ✅ | |
SplitChunksPlugin | ✅ | 根据“条件”自动拆分chunks |
ResolverCachePlugin | ✅ |
编译动作触发的开始:加载模块–load并生成module
一切从
compiler.hooks.make
–>EntryPlugin中触发
–>Compilation.addEntry
操作!
而在这个addEntry
方法中,最终进入到了addModuleTree
以及handleModuleCreation
阶段,然后到了addModule
,该方法则是实际的module-build
阶段!
🌠 ==> 最终在这个handleModuleCreation
方法中,调用的_handleModuleBuildAndDependencies()
方法中,通过AsyncQueue的processor,来触发到了_buildModule(module, callback)
。
真正通过每个NormalModule
对象自身的build
方法,通过传递的参数,来进行每个module自身的build动作,而且在此动作中将促成Source
对象的生成!!!
👉 parser解析动作的过程:
在NormalModule
对象中,都拥有一个parser
属性,该属性代表着当前模块的一个解析器对象,关于这个NormalModule
的解析过程,具体可以看 NormalModule
👽 至此,已经完成的module的加载,其中的依赖树也已经形成
模块完成–finish
此阶段实现chunk的生成动作!!!
以下是对应的module,然后由module来生成chunk的生成过程
😕 这里为什么要生成module
以及chunk
呢? 👉 从 代码的生成过程 中我们可以知道,对于在模块中引用的外部模块,将会在最终的结果代码内容中被__webpack_require__()
方法所引入,而这里的chunk
就是存储着即将被引用的内容,比如"./src/module1.js"
模块封存–seal
此阶段实现将chunk生成在js中引用的外部依赖内容,并结合框架注释以及其他相关代码实现目标代码的生成!!!
在开始进行这个阶段的具体分析之前,先来了解一下webpack
中的sources
源代码机制
source体系
webpack
中的源代码表现形式为一source
对象,而它则是由webpack-sources库来提供的,包含代表一个源的多个类。可以向Source
询问源代码、大小、源映射和哈希,其体系组成结构如下图所示:
👽 从上图可以看出Source
作为所有source的抽象基类,其提供了对子类的source()
的公共访问api(这里不同的子类对不同的api都有不同的访问机制,因此每一次的调用,都将会是比较”昂贵”的),主要 🈶 以下几个:
- source(): 将表示的源代码返回为字符串或缓冲区,由各个子类去实现,直接调用父类的将会报错,其返回结果定义如下:
1
Source.prototype.source() -> String | Buffer
- buffer(): 将表示的源代码返回为
Buffer
,字符串被转换为utf-8
; - size(): 返回表示的源代码的大小(以字节为单位);
- map(): 将表示的源代码的
SourceMap
作为JSON
返回,如果没有SourceMap
可用,可能会返回 null,其返回结果定义如下:1
Source.prototype.map(options?: Object) -> Object | null
- sourceAndMap(): 返回source code(source()方法返回的内容)以及source map(map()方法返回的内容),此方法可能比起单独调用source()以及map()来调用具有更好的性能,其返回的结果定义如下:
1
2
3
4Source.prototype.sourceAndMap(options?: Object) -> {
source: String | Buffer,
map: Object | null
} - updateHash(): 使用表示的源代码的内容更新提供的
Hash
对象。 (Hash 是一个有更新方法的对象,用字符串值调用);
👇 是子类的相关介绍:
子类 | 描述 | 使用方式 |
---|---|---|
RawSource | 表示没有 SourceMap 的源代码 |
new RawSource(sourceCode: String | Buffer) |
OriginalSource | 表示源代码,它是原始文件的副本 | new OriginalSource(sourceCode: String | Buffer, name: String) |
SourceMapSource | 用 SourceMap 表示源代码,可以选择为原始源添加一个额外的 SourceMap |
new SourceMapSource(sourceCode: String | Buffer,name: String,sourceMap: Object | String | Buffer,originalSource?: String | Buffer,innerSourceMap?: Object | String | Buffer,removeOriginalSource?: boolean) |
CachedSource | 包装一个Source,将map、source、buffer、size、sourceAndMap的返回结果缓存到内存中,而updateHash 未缓存。它尝试重用来自其他方法的缓存结果以避免计算,比如当 source 已经被缓存时,调用 size 将从缓存的 source 中获取大小,调用 sourceAndMap 只会在包装的 Source 上调用 map。 |
new CachedSource(source: Source | () => Source, cachedData?: CachedData) |
PrefixSource | 使用提供的字符串为装饰源的每一行添加前缀 | new PrefixSource(prefix: String,source: Source | String | Buffer) |
ConcatSource | 将多个源或字符串连接到一个源 | new ConcatSource(...items?: Source | String) |
ReplaceSource | 用源代码的替换和插入装饰 Source | - |
CompatSource | 将类似于Source对象转换为真正的 Source 对象 |
CompatSource.from(sourceLike: any | Source) |
SizeOnlySource | 表示只有尺寸大小的源代码 | new SizeOnlySource(size: Number) |
这里着重介绍一下CachedSource以及ReplaceSource两个对象
CachedSource
⭐ 描述:包装一个Source,将map、source、buffer、size、sourceAndMap的返回结果缓存到内存中,尝试重用来自其他方法的缓存结果以避免计算!
⭐ 构造方法:
1 | new CachedSource(source: Source); |
🌠 一般的可以通过传递一个回调函数,通过返回一个source,使得CachedSource
被包裹起来,而且仅被触发一次,后续针对这个source的相关访问,都由其包裹api来提供服务!
⭐ 对外暴露的api
- getCachedData(): 返回传递给构造函数的缓存数据,缓存的数据都会被转换为缓冲区,并避免使用字符串;
- original(): 返回源代码对象
Source
; - originalLazy(): 懒加载的方式来返回源代码对象或者函数
ReplaceSource
⭐ 描述:用源代码的替换和插入装饰 Source
⭐ 对外暴露的api
- replace(): 替换源代码的从某个开始位置到结束位置为另外一个字符串;
- insert(): 往源代码的某个位置来插入另外一个字符串;
⚠ 这里的位置并不受其他的替换或者插入动作的影响!
generator关系图
😕 当程序直接操作字符串而不解析字符串的内容来进行相应的
导入
、依赖处理
等在nodejs层面才拥有的动作时,是不可能将其直接放在浏览器中就可以直接运行的,因此需要将其中对应的“特殊”操作,转换为在对应环境下(比如浏览器)才可以识别到的动作,而generator
就是负责这样子的工作,它主要负责将字符串内容生成不同的Source
对象!
在开始进行代码生成的时候,针对不同的文件扩展名,对应会有不同的解析协议,而generator
就是作为不同解析的服务对象,对外暴露统一的方法api,实现运行时确认解析类型,以及针对类型生成不同的source
源代码对象
😕 在开始之前,先来看以下的一个例子:
1 | import data from './data.json'; |
这段代码在webpack执行的过程时,与”.js”的解析, 🈶 什么区别呢?
我们知道,这个”js”文件的解析,是由JavascriptModulesPlugin
插件所提供的服务的,那么”.json”是由谁来提供解析服务的呢?同样的,
1 | // 在`WebpackOptionsApply.js`中的第280行中 |
由JsonModulesPlugin
插件来提供对json文件的代码解析生成源代码Source
对象服务!
🌠 JavascriptGenerator
与JsonGenerator
都是Generator
的子类,通过其公共的方法generate
,来生成统一的Source
子类,比如JsonGenerator
生成的是RowSource
,表示生成的是没有源代码的Source
1 | generate( |
然后将结果存储在sources
这个Map对象中,而且用另外的一个CachedSource
来包裹,表示避免对源码source对象的直接访问!
dependency与DenpendencyTemplate关系图
在生成*.js文件的字符串内容为
Source
对象时,通过从module.originalSource()
来获取SourceMapSource
对象(包含sourcemap以及source)的对象,然后将其用ReplaceSource
对象包装起来,随后调用sourceModule()
方法,针对module
中的依赖模块进行sourceDependency()
方法的分析
关于Dependency
在之前的Dependency介绍中已有提及到!
而ModuleDependency
则作为Dependency
子类,则提供了在webpack
中的所有依赖模块的超类,关于Dependency
的生态如下:
⭐ModuleDependency
作为webpack
中的超类依赖实现对象,在webpack/lib/dependencies/
目录中密密麻麻大部分都是其子类依赖实现,针对不同的业务场景进行对应实现!
👽 而这个DenpendencyTemplate
则是提供了一个超类机制,通过统一提供的apply(dependency, source, templateContext)
方法,来针对不同的源码对象source生成不同的依赖替换字符串源代码对象!
seal代码生成阶段
在此阶段,
chunk
将会被转换为”可替换的对象”,等待被替换,具体过程,可以见 👇 的关于seal阶段中的chunk转换过程:
而真正的代码生成阶段,则由额外的插件来提供的:
我能够做点什么?
从上述的关于
compilation
的工作过程的学习,可以对module生成、chunk生成、代码生成等阶段的监听/干预,追加自己的业务场景,比如有以下的几个:
- 干预代码生成过程,追加自定义注释,使得打包出来的代码统一在某个位置带上自己的表示;
- 干预js代码解析过程,监听是否在代码中使用了某个不合法的库,可以在编译打包阶段将其去掉;
- 干预js代码生成过程,统一隐藏项目中的console.log()等输出日志的方法;
- 理解同步导入以及异步导入的原理,根据实际业务场景进行合理安排依赖导入;
- 模仿module、generator、dependency、source等生态的设计,对外暴露统一的方法api,设计统一的接口api服务;
- 未完待续……