前言

官方链接

webpack中真正的”编译器执行者”,Compilation实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。 它会对应用程序的依赖图中所有模块, 进行字面上的编译(literal compilation)。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。
👇 是关于Compilation对象的组成结构:

Compilation的工作流程是怎样的呢?

👽 其实Compilation自身并没有做任何的动作,而是一堆的插件它们负责来实现的!因为Compiler创建完这个Compilation对象之后,就没有针对这个Compilation对象进行调用其相关的API了,而是将Compilation以及params作为参数,来触发Complier.hooks.thisCompilation

最简单webpack对应的Compilation的待执行插件队列

首先,针对一个上面最简单的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对象的生成!!!
source对象的生成

👉 parser解析动作的过程:
NormalModule对象中,都拥有一个parser属性,该属性代表着当前模块的一个解析器对象,关于这个NormalModule的解析过程,具体可以看 NormalModule

👽 至此,已经完成的module的加载,其中的依赖树也已经形成

模块完成–finish

此阶段实现chunk的生成动作!!!
以下是对应的module,然后由module来生成chunk的生成过程
module以及chunk的生成过程

😕 这里为什么要生成module以及chunk呢? 👉代码的生成过程 中我们可以知道,对于在模块中引用的外部模块,将会在最终的结果代码内容中被__webpack_require__()方法所引入,而这里的chunk就是存储着即将被引用的内容,比如"./src/module1.js"

模块封存–seal

此阶段实现将chunk生成在js中引用的外部依赖内容,并结合框架注释以及其他相关代码实现目标代码的生成!!!
在开始进行这个阶段的具体分析之前,先来了解一下webpack中的sources源代码机制

source体系

webpack中的源代码表现形式为一source对象,而它则是由webpack-sources库来提供的,包含代表一个源的多个类。可以向 Source 询问源代码、大小、源映射和哈希,其体系组成结构如下图所示:
Source以及其子类

👽 从上图可以看出Source作为所有source的抽象基类,其提供了对子类的source()的公共访问api(这里不同的子类对不同的api都有不同的访问机制,因此每一次的调用,都将会是比较”昂贵”的),主要 🈶 以下几个:

  1. source(): 将表示的源代码返回为字符串或缓冲区,由各个子类去实现,直接调用父类的将会报错,其返回结果定义如下:
    1
    Source.prototype.source() -> String | Buffer
  2. buffer(): 将表示的源代码返回为Buffer,字符串被转换为 utf-8;
  3. size(): 返回表示的源代码的大小(以字节为单位);
  4. map(): 将表示的源代码的 SourceMap 作为 JSON 返回,如果没有 SourceMap 可用,可能会返回 null,其返回结果定义如下:
    1
    Source.prototype.map(options?: Object) -> Object | null
  5. sourceAndMap(): 返回source code(source()方法返回的内容)以及source map(map()方法返回的内容),此方法可能比起单独调用source()以及map()来调用具有更好的性能,其返回的结果定义如下:
    1
    2
    3
    4
    Source.prototype.sourceAndMap(options?: Object) -> {
    source: String | Buffer,
    map: Object | null
    }
  6. 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
2
new CachedSource(source: Source);
new CachedSource(() => Source, cachedData? CachedData);

🌠 一般的可以通过传递一个回调函数,通过返回一个source,使得CachedSource被包裹起来,而且仅被触发一次,后续针对这个source的相关访问,都由其包裹api来提供服务!

对外暴露的api

  • getCachedData(): 返回传递给构造函数的缓存数据,缓存的数据都会被转换为缓冲区,并避免使用字符串;
  • original(): 返回源代码对象Source;
  • originalLazy(): 懒加载的方式来返回源代码对象或者函数

ReplaceSource

描述:用源代码的替换和插入装饰 Source

对外暴露的api

  • replace(): 替换源代码的从某个开始位置到结束位置为另外一个字符串;
  • insert(): 往源代码的某个位置来插入另外一个字符串;

这里的位置并不受其他的替换或者插入动作的影响!

generator关系图

😕 当程序直接操作字符串而不解析字符串的内容来进行相应的导入依赖处理等在nodejs层面才拥有的动作时,是不可能将其直接放在浏览器中就可以直接运行的,因此需要将其中对应的“特殊”操作,转换为在对应环境下(比如浏览器)才可以识别到的动作,而generator就是负责这样子的工作,它主要负责将字符串内容生成不同的Source对象!
在开始进行代码生成的时候,针对不同的文件扩展名,对应会有不同的解析协议,而generator就是作为不同解析的服务对象,对外暴露统一的方法api,实现运行时确认解析类型,以及针对类型生成不同的source源代码对象
Generator继承关系图
😕 在开始之前,先来看以下的一个例子:

1
2
import data from './data.json';
console.info(data);

这段代码在webpack执行的过程时,与”.js”的解析, 🈶 什么区别呢?
我们知道,这个”js”文件的解析,是由JavascriptModulesPlugin插件所提供的服务的,那么”
.json”是由谁来提供解析服务的呢?同样的,

1
2
// 在`WebpackOptionsApply.js`中的第280行中
new JsonModulesPlugin().apply(compiler);

JsonModulesPlugin插件来提供对json文件的代码解析生成源代码Source对象服务!
json文件代码解析器以及生成器

🌠 JavascriptGeneratorJsonGenerator都是Generator的子类,通过其公共的方法generate,来生成统一的Source子类,比如JsonGenerator生成的是RowSource,表示生成的是没有源代码的Source

1
2
3
4
5
6
7
8
9
10
11
12
13
generate(
module,
{
moduleGraph,
runtimeTemplate,
runtimeRequirements,
runtime,
concatenationScope
}
) {
// ... 省略对json内容的处理
return new RawSource(content);
}

然后将结果存储在sources这个Map对象中,而且用另外的一个CachedSource来包裹,表示避免对源码source对象的直接访问!

dependency与DenpendencyTemplate关系图

在生成*.js文件的字符串内容为Source对象时,通过从module.originalSource()来获取SourceMapSource对象(包含sourcemap以及source)的对象,然后将其用ReplaceSource对象包装起来,随后调用sourceModule()方法,针对module中的依赖模块进行sourceDependency()方法的分析
根据不同的依赖生成不同的template
关于Dependency在之前的Dependency介绍中已有提及到!
ModuleDependency则作为Dependency子类,则提供了在webpack中的所有依赖模块的超类,关于Dependency的生态如下:
Dependency生态
ModuleDependency作为webpack中的超类依赖实现对象,在webpack/lib/dependencies/目录中密密麻麻大部分都是其子类依赖实现,针对不同的业务场景进行对应实现!

👽 而这个DenpendencyTemplate则是提供了一个超类机制,通过统一提供的apply(dependency, source, templateContext)方法,来针对不同的源码对象source生成不同的依赖替换字符串源代码对象!
生成待导入的替换字符串源码对象

seal代码生成阶段

在此阶段,chunk将会被转换为”可替换的对象”,等待被替换,具体过程,可以见 👇 的关于seal阶段中的chunk转换过程:
初级代码的生成以及chunk的简单生成
而真正的代码生成阶段,则由额外的插件来提供的:
生成文件内容的时候所触发的插件

我能够做点什么?

从上述的关于compilation的工作过程的学习,可以对module生成、chunk生成、代码生成等阶段的监听/干预,追加自己的业务场景,比如有以下的几个:

  1. 干预代码生成过程,追加自定义注释,使得打包出来的代码统一在某个位置带上自己的表示;
  2. 干预js代码解析过程,监听是否在代码中使用了某个不合法的库,可以在编译打包阶段将其去掉;
  3. 干预js代码生成过程,统一隐藏项目中的console.log()等输出日志的方法;
  4. 理解同步导入以及异步导入的原理,根据实际业务场景进行合理安排依赖导入;
  5. 模仿module、generator、dependency、source等生态的设计,对外暴露统一的方法api,设计统一的接口api服务;
  6. 未完待续……