webpack源码 最全概括和调试

150 阅读22分钟

webpack源码笔记

写笔记的目的是 能快速记忆出相应的内容,对webpack有一个系统性的了解,然后哪里不懂了,再专门针对性的学习。知识留存,效率变高。

系统分割webpack流程,一定要自己调试源码,不懂的就查。借鉴了很多的技术文章,最后有链接会一一列出来

1.webpack是什么?

webpack 是一个现代JavaScript应用程序的静态模块打包器,将我们程序代码的各类资源经过webpack编译打包成bundler文件。供浏览器使用。

设计原则

webpack 只处理和支持JS模块,所有其他类型的模块,比如图片,css等,都需要通过对应的loader转成JS模块。所以在webpack中无论任何类型的资源,本质上都被当成JS模块处理。

接下来的深入webpack,探究webpack源码的奥秘。参考了很多文章,形成自己的记忆点去理解webpack。文章链接在最后。

2.webpack核心流程

提前了解

先整体认识一下webpack,走一下webpack的打包流程,然后再从整体到细节阶段,每个阶段主要要做什么,每个阶段做什么事情,有什么重点函数 ,对象。慢慢的展开 参照对应。

webpack本质上是一种事件流的机制,运行流程是一个串行的过程,从启动初始化阶段到构建阶段再到生成bundle文件阶段一系列的流程。

webpack分为三个阶段:

  1. 初始化阶段
  2. 构建阶段
  3. 生成阶段

首先会从配置文件(通常就是我们的webpack.config.js)和 Shell 语句(命令行执行)中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的参数;初始化完成后会调用Compilerrun来真正启动webpack编译构建过程,webpack的构建流程包括compilemakebuildsealemit阶段,执行完这些阶段就完成了构建过程。

更具体一点:

1.png

2.1初始化阶段 (图中流程的1、 2、 3)

1 初始化参数:合并配置文件和shell语句配置参数

2 .实例化compiler对象及处理其他参数: 实例化compiler对象并处理配置中的插件(webpack.config.js中,我们自己引入的)和webpack自带的内置插件以及执行环境需要的参数(options,就是这个webpack.config.js)以及注册各种模块工厂。

3.调用run方法编译:调用compiler.run方法,启动webpack编译构建,会构建compilation对象,存储编译这一次过程的所有数据

2.2构建阶段 (图中流程的4、 5 )

4.make方法编译:make方法编译从入口文件开始,构建模块,直到所有模块编译完成(配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都编译完成)

5.build Module :完成模块编译, 得到翻译模块的所有内容,以及他们之间的依赖关系

2.3生成阶段 (图中流程的6、 7、8 )

6.调用seal方法,生成chunks,进行优化,生成输出代码。seal结束,compliation实例构建结束,这一次的构建过程结束

7.seal结束后 emit触发,webpack遍历所有compliation。assets文件,生成所有文件bundle,

8.done;将文件内容写入文件系统触发done方法。


ps:

文件名称代表的含义: module、chunk、bundle

这个过程中 我们从侧面文件名称的改变去加深印象:

初始化阶段,即我们自己写的源代码文件大致称之为module

构建阶段:将构建时的文件称之为chunk

生成阶段,即代码文件构建完成,seal方法调用后生成的文件,供浏览器使用。我们叫bundle

不是很准确,但是大致意思 文件在webpack中的过程流转。


2事件节点.png

大致的webpack的重要事件节点是这样子,接下来每个阶段断点调试分析细节过程。

3.源码分析

前言

webpack采用版本:

we_02.png

    "webpack": "^4.44.2",
    "webpack-cli": "^3.3.12"

下面我们来打断点调试webpack源码流程:(要记忆深刻的话,最好要自己打断点进去看一下)

准备工作

01.新建项目 并初始化,然后新建webpack.config.js run.js 文件

we_01.png

webpack.config.js

we_03.png

run.js

we_04.png

//run.js 
let webpack = require('webpack')
let options = require('./webpack.config.js')
​
let complier = webpack(options)
​
complier.run(function (err, stats) {
  console.log(err)
  console.log(stats.toJson())
})
​

准备工作已经准备好。 run.js 是模拟webpack打包时,webpack-cli的操作 其实在shell命令行输入webpack 也是会执行compiler.run 函数

stats.toJson()是它里面自带的方法 我们使用 node run.js 可以看到 正常运行

we_05.png

打断点调试 webpack初始化阶段

使用vscode 对 run.js的第四行进行断点调试

we_06.png

可以看到:左边是断点调试的变量 我们点击第三个' 单步调试'

给webpack一个参数options ,调用webpack 赋值给compiler

点击第三个' 单步调试' 进入 webpack .js

we_07.png

进入webpack.js 中

we_08.png

分析:webpack函数 接受两个参数:options 和callback ;从左边变量那里可以看到 options就是我们自己的配置webpack.config.js 文件,目前没有callback

let complier = webpack(options)  //就是这里的options

下面整体分析一下:

webpack函数要做的事情:

1.调用 validateSchema 校验配置 像这种定义类的猜一下是校验 当然如果要一步一步细致的走 就进入查看我们现在关心一下 主要的流程

    const webpackOptionsValidationErrors = validateSchema(
        webpackOptionsSchema,
        options
    );
    
    if (webpackOptionsValidationErrors.length) {
        throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
    }

2.创建compiler;

3,判断options,将webpack默认配置给到options上,这也是为什么我们也可以对webpack0配置的原因,因为webpack有默认配置.

我们使用第二个按钮 单步跳过 走到38行 未执行下面函数之前options的值

we_09.png

再单步跳过到41行 执行之后options的值

we_10.png

默认配置已经给到了。 走到41行之后 先将webpack分析完成 ,之后处理 compiler = new Compiler(options.context) 这行代码 我们单步跳过 走到42行 ,不进入41行(之后重点讲解compiler对象的实例化)

4.得到compiler 对象

        compiler = new Compiler(options.context);

we_11.png

可以查看 compiler对象的值

5.可以看到是一个插件的使用 函数调用

new NodeEnvironmentPlugin({
            infrastructureLogging: options.infrastructureLogging
        }).apply(compiler)

这个现在也不深入进去,这行代码的意思是 设置node文件读写能力挂载到compiler身上。

6.这个是 将我们自定义的插件挂载到compiler身上,如果是对象就挂载,如果是函数就调用

    if (options.plugins && Array.isArray(options.plugins)) {
            for (const plugin of options.plugins) {
                if (typeof plugin === "function") {
                    plugin.call(compiler, compiler);
                } else {
                    plugin.apply(compiler);
                }
            }
        }

7.继续往下走

这是compiler身上的两个钩子触发了监听,在之前肯定有tap方法添加了事件监听

(这里就要讲一下,这个call方法 , compiler是继承了tapable而来,tapable是一种事件流机制,简单来讲核心就是实例化钩子hook、tap方法添加事件监听、call方法触发事件监听) 这里就是call 触发了事件监听

当然 tapable的钩子很多 我上述的是tap call 是基础钩子的同步用法 还有 tapAsync tapPromise callAsync 几种可以添加事件监听 和触发事件监听的异步 这里的 tapable 要了解机制 才能看懂源码 大家不懂得可以搜索一下tapable 了解了之后再看源码 因为之后的compiler就涉及到了tapable

we_12.png

这两个钩子 目前步骤不重要 继续向下 57行代码 就是加载webpack的各种内置插件

compiler.options = new WebpackOptionsApply().process(options, compiler);

WebpackOptionsApply 类,webpack 内置了数百个插件,这些插件并不需要我们手动配置,WebpackOptionsApply 会在初始化阶段根据配置内容动态注入对应的插件

  1. 判断是否有回调函数

we_13.png

如果有回调函数, 就走里面函数 判断是否有监听 有就执行监听然后再执行run

我们没有用到 最后 就是返回了compiler对象。

继续点 就回到了run.js 里 第四行代码就算执行完毕了.

we_14.png

小总结: 在compiler.run之前我们的流程有:

1.开始 合并配置---->实例化 Compiler---->设置Node文件读写的能力---->通过循环挂载plugins---->处理webpack内部默认的插件(入口文件)

我们这里看一下

2.1.1WebpackOptionsApply
compiler.options = new WebpackOptionsApply().process(options, compiler);

我们进入函数调用的process方法里面调试一下,看发生了什么:

断点进去到了 WebpackOptionsApply.js

看里面的process 有很多插件 是内置plugin的引入 new JavascriptModulesPlugin().apply(compiler); 290行 EntryOptionPlugin

new JavascriptModulesPlugin().apply(compiler);
new JsonModulesPlugin().apply(compiler);
new WebAssemblyModulesPlugin({
	mangleImports: options.optimization.mangleWasmImports
}).apply(compiler);

new EntryOptionPlugin().apply(compiler);

听函数名称 是处理入口文件的插件 打个断点 进去看看这个内置插件

we_15.png

插件内部函数

we_16.png

compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
	if (typeof entry === "string" || Array.isArray(entry)) {
		itemToPlugin(context, entry, "main").apply(compiler);
	} else if (typeof entry === "object") {
		for (const name of Object.keys(entry)) {
			itemToPlugin(context, entry[name], name).apply(compiler);
		}
	} else if (typeof entry === "function") {
		new DynamicEntryPlugin(context, entry).apply(compiler);
	}
	return true;
});
		
		
		
	//在代码20行
const itemToPlugin = (context, item, name) => {
	if (Array.isArray(item)) {
		return new MultiEntryPlugin(context, item, name);
	}
	return new SingleEntryPlugin(context, item, name);
};

apply方法里 添加了entryOption钩子 这个肯定在compiler对象上已经注册了 这里直接调用 里面又有个itemToPlugin插件调用 往上看 代码20行 又有两个插件

一个是多入口插件MultiEntryPlugin 一个是单入口插件 SingleEntryPlugin

我们这个案例是单入口 所以进入里面看一看 在24行打个断点进去看看

we_161.png

可以看到 SingleEntryPlugin 里面的apply方法里又注册了两个监听事件 (第二个没截图下来)

compiler.hooks.compilation.tap(
	"SingleEntryPlugin",
	(compilation, { normalModuleFactory }) => {
		compilation.dependencyFactories.set(
			SingleEntryDependency,
			normalModuleFactory
		);
	}
);

			//这个tapAsync 是异步注册事件
		compiler.hooks.make.tapAsync(
			"SingleEntryPlugin",
			(compilation, callback) => {
				const { entry, name, context } = this;

				const dep = SingleEntryPlugin.createDependency(entry, name);
				compilation.addEntry(context, dep, name, callback);
			}
		);

compiler.hooks.compilation compiler.hooks.make

之后就再WebpackOptionsApply.js文件里 触发监听事件compiler.hooks.entryOption

we_17.png

所以 这个入口文件插件就是提前埋点 注册事件 等待之后需要的时候触发

2.2compiler.run()

在run.js 文件里这个run方法调用的地方打断点 进去看看实现

complier.run(function (err, stats) {
  console.log(err)
  console.log(stats.toJson())
})

we_18.png

我们看到 进入了Compiler.js 里 run方法如下

一个finalCallback 函数 一个onCompiled函数 有个beforeRun的触发监听事件 这个里面有个回调函数 看这个回调函数里面有个判断 然后又有一个run的监听事件 里面又有一个回调函数 回调函数里面有个readRecords函数 里面还有一个回调函数 回调函数里面有个compile方法

这个就是webpack的run方法,里面的回调超级多 :

this.hooks.beforeRun.callAsync(this, err => {
	if (err) return finalCallback(err);

	this.hooks.run.callAsync(this, err => {
		if (err) return finalCallback(err);

		this.readRecords(err => {
			if (err) return finalCallback(err);

			this.compile(onCompiled);
		});
	});
}
		

webpack的编译都按照下面的钩子调用顺序执行。

  • before-run 清除缓存
  • run 注册缓存数据钩子
  • before-compile
  • compile 开始编译
  • make 从入口分析依赖以及间接依赖模块,创建模块对象
  • build-module 模块构建
  • seal 构建结果封装, 不可再更改
  • after-compile 完成构建,缓存数据
  • emit 输出到dist目录

run函数中出现的钩子有:beforeRun --> run --> done --> afterDone。第三方插件可以钩住不同的生命周期,接收compiler对象,处理不同逻辑。

run函数钩住了webpack编译的前期和后期的阶段,那么中期最为关键的代码编译过程就交给了this.compile()来完成了。在this.comille()中,另一个主角compilation

这个run方法相当于已经走完流程了。

然后再研究 compiler的 compile的方法

在调用compile方法的地方打断点

321行

we_19.png

进入方法里面 看看

const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
	if (err) return callback(err);

	this.hooks.compile.call(params);

	const compilation = this.newCompilation(params);

	this.hooks.make.callAsync(compilation, err => {
		if (err) return callback(err);

		compilation.finish(err => {
			if (err) return callback(err);

			compilation.seal(err => {
				if (err) return callback(err);

				this.hooks.afterCompile.callAsync(compilation, err => {
					if (err) return callback(err);

					return callback(null, compilation);
				});
			});
		});
	});
});

compile函数中出现的钩子有:beforeCompile --> compile --> make --> afterCompile

we_20.png

compile 方法执行 1 准备参数(其中 normalModuleFactory 是我们后续用于创建模块的):执行了函数newCompilationParams,里面调用了createNormalModuleFactory工厂函数 2 触发beforeCompile 3 将第一步的参数传给一个函数,开始创建一个 compilation (newCompilation)(断点进入)

	newCompilation(params) {
		const compilation = this.createCompilation();
		compilation.fileTimestamps = this.fileTimestamps;
		compilation.contextTimestamps = this.contextTimestamps;
		compilation.name = this.name;
		compilation.records = this.records;
		compilation.compilationDependencies = params.compilationDependencies;
		this.hooks.thisCompilation.call(compilation, params);
		this.hooks.compilation.call(compilation, params);
		return compilation;
	}

4 在调用 newCompilation 的内部

  • 调用了 createCompilation
createCompilation() {
	return new Compilation(this);
}
  • 触发了 this.compilation 钩子 和 compilation 钩子的监听 03 当创建了 compilation 对象之后就触发了 make 钩子

04 当我们触发 make 钩子监听的时候,将 compilation 对象传递了过去

我们看一下make钩子在哪里埋的

we_21.png

在SingleEntryPlugin .js apply方法里 ,也就是在入口模块插件的时候就埋进去了

之后 make 就带着compilation 执行了

compiler.hooks.compilation.tap(
	"SingleEntryPlugin",
	(compilation, { normalModuleFactory }) => {
		compilation.dependencyFactories.set(
			SingleEntryDependency,
			normalModuleFactory
		);
	}
);

compiler.hooks.make.tapAsync(
	"SingleEntryPlugin",
	(compilation, callback) => {
		const { entry, name, context } = this;

		const dep = SingleEntryPlugin.createDependency(entry, name);
		compilation.addEntry(context, dep, name, callback);
	}
);
compiler

Compiler类(./lib/Compiler.js):webpack的主要引擎,在compiler对象记录了完整的webpack环境信息,在webpack从启动到结束,compiler只会生成一次。你可以在compiler对象上读取到webpack config信息,outputPath

可以理解为 webpack 编译的调度中心,是一个编译器实例,在 compiler 对象中记录了完整的 webpack 环境信息,在 webpack 的每个进程中,compiler 只会生成一次。(举例,npm run dev 一次只有一个 compiler)

compile.js主要做了几个事情:

  • 接收webpack.config.js配置参数,并初始化entryoutput
  • 开启编译run方法。处理构建模块、收集依赖、输出文件等。
  • buildModule方法。主要用于构建模块(被run方法调用)
  • emitFiles方法。输出文件(同样被run方法调用)

Compilation类(./lib/Compilation.js):代表了一次单一的版本构建和生成资源。compilation编译作业可以多次执行,比如webpack工作在watch模式下,每次监测到源文件发生变化时,都会重新实例化一个compilation对象。一个compilation对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息

在Compilation对象中:

  • modules 记录了所有解析后的模块
  • chunks 记录了所有chunk
  • assets记录了所有要生成的文件

Compilation是 compiler 的生命周期内一个核心对象,它包含了一次构建过程中所有的数据(modules、chunks、assets)。也就是说一次构建过程对应一个 compliation 实例。(举例,比如热更新的时候,webpack 会监听本地文件改变然后重新生成一个 compliation)

总结一下 初始化阶段的重要步骤

一、步骤 01 实例化 compiler 对象( 它会贯穿整个webpack工作的过程 ) 02 由 compiler 调用 run 方法

二、compiler 实例化操作 01 compiler 继承 tapable,因此它具备钩子的操作能力(监听事件,触发事件,webpack是一个事件流)

02 在实例化了 compiler 对象之后就往它的身上挂载很多属性,其中 NodeEnvironmentPlugin 这个操作就让它具备了 文件读写的能力

03 具备了 fs 操作能力之后又将 plugins 中的插件都挂载到了 compiler 对象身上

04 将内部默认的插件与 compiler 建立关系,其中 EntryOptionPlugin 处理了入口模块的 id

05 在实例化 compiler 的时候只是监听了 make 钩子(SingleEntryPlugin) 5-1 在 SingleEntryPlugin 模块的 apply 方法中有二个钩子监听 5-2 其中 compilation 钩子就是让 compilation 具备了利用 normalModuleFactory 工厂创建一个普通模块的能力 5-3 因为它就是利用一个自己创建的模块来加载需要被打包的模块 5-4 其中 make 钩子 在 compiler.run 的时候会被触发,走到这里就意味着某个模块执行打包之前的所有准备工作就完成了 5-5 addEntry 方法调用()

三、run 方法执行( 当前想看的是什么时候触发了 make 钩子 )

01 run 方法里就是一堆钩子按着顺序触发(beforeRun run compile)

02 compile 方法执行 1 准备参数(其中 normalModuleFactory 是我们后续用于创建模块的) 2 触发beforeCompile 3 将第一步的参数传给一个函数,开始创建一个 compilation (newCompilation) 4 在调用 newCompilation 的内部 - 调用了 createCompilation - 触发了 this.compilation 钩子 和 compilation 钩子的监听 03 当创建了 compilation 对象之后就触发了 make 钩子

04 当我们触发 make 钩子监听的时候,将 compilation 对象传递了过去

四、总结

1 实例化 compiler 2 调用 compile 方法 3 newCompilation 4 实例化了一个compilation 对象(它和 compiler 是有关系) 5 触发 make 监听 6 addEntry 方法(这个时候就带着 context name entry 一堆的东西) 就奔着编译去了.....

接下来就是 构建阶段了

打断点调试构建阶段

调试阶段webpack要做什么?

4.make方法编译:make方法编译从入口文件开始,构建模块,直到所有模块编译完成(配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都编译完成)

5.build Module :完成模块编译, 得到翻译模块的所有内容,以及他们之间的依赖关系

稍微总结一下: 在构建阶段做的重要的事情;

1 .addEntry(): compile中触发make时间并调用addEntry 找到入口js文件 进行下一步的模块绑定

2 _addModuleChain()---->6.2addModuleDependencise() ;.buildModule()

2.1解析入口文件,通过对应的工厂方法创建模块,保存到compilation对象上(通过单例模式保证同样的模块只有一个实例

2.2对module进行build了,包括调用loader处理源文件,使用acorn生成AST并且遍历AST,遇到require等依赖时,创建依赖Dependency加入依赖数组

2.3module已经处理完毕,此时开始处理依赖的module

2.4异步的对依赖的module进行build 如果依赖中仍然有依赖,就循环处理其依赖

addEntry addEntry 方法会开始第一批 module 的解析,即我们配置的入口文件(例如 main.js)。addEntry 主要执行addModuleChain(), addModuleChain 中调用addModule

addModule 使用对应的工厂 NormalModuleFactory (具体方法是 create)生成一个空的 webpack 模块化 对象。把它存入 compliation.modules 中,由于当前是入口文件,所以也会存入 compliation.entries 中。随后执行 buildModule->doBuild

在创建 compliation 的时候 compiler 对象也会实例化两个核心的工厂对象,分别是 NormalModuleFactory 和 ContextModuleFactory。

doBuild 读取 module 中内容并进行处理。用对应的 loaders ,把我们的源码 module 转成标准的 JS 模块对象。然后执行 Parse.parse()

parse 将 JS 解析为 AST。然后遍历 AST

parse的最大作用就是收集模块依赖关系

parser.js中主要就三个方法:

  • getAST: 将获取到的模块内容 解析成AST语法树
  • getDependencies:遍历AST,将用到的依赖收集起来
  • transform:把获得的ES6AST转化成ES5

遍历 AST 找到入口中的所有依赖 module,加入 dependencies 数组。再依次对 dependencies 中的依赖 module 进行processModuleDependencies-,这是一个递归遍历过程,会把所有的原来的 module 源码都聚合到一个对象(后面称为“chunk 初始态对象” )中。 如果发现动态引入例如 import(),那么就开启新的一轮addModuleChain。直到每一轮的递归结束,执行 seal() 。 此时调用多少次addModuleChain,就形成了多少个依赖树,就有多少个 chunk 初始态对象

we_35.png

开始调试:

我们依旧在run.js中打断点 走make之后的 构建阶段源码 直接第六行

we_22.png

1.点击单步调试进入Compiler.js 我们之前走到了make方法那里 直接给个断点 669行

we_26.png

make 钩子监听触发,传compilation对象进去。

  1. 点击单步调试 进去,会进到Hooks.js 看它的返回值

we_23.png

3.先点击单步跳过到154 再点击单步调试进去 找到f0函数(callback),进去。

we_24.png

4.点击单步跳出到第七行 点击单步调试 进去这个函数 会跳转到SingleEntryPlugin.js 我们就发现了这段代码

we_25.png

compiler.hooks.make的添加监听事件 ,可以看到 它的第二个参数函数里面有这几行

compiler.hooks.make.tapAsync(
	"SingleEntryPlugin",
	(compilation, callback) => {
		const { entry, name, context } = this;

		const dep = SingleEntryPlugin.createDependency(entry, name);
		compilation.addEntry(context, dep, name, callback);
	}
);

1.这里的this 是我们上文传过来的compilation

2.entry(被打包模块相对路径), name(main), context(当前项目根路径)

3.dep 对当前的入口模块中的依赖关系进行处理

4.调用了addEntry方法

我们直接进入addEntry方法内进入 compilation.js 里的addEntry方法

addEntry

we_27.png

这个compilation 我们上面也总结了一下 但是先看流程

我们往下走看到一个_addModuleChain函数 根据名字 添加模块链,

we_28.png

5、addEntry方法 内部调用了_addModuleChain方法 处理依赖 点进去

we_29.png

这里有个Dep 还有一个moduleFactory 一个是依赖类,一个是模块工厂

const moduleFactory = this.dependencyFactories.get(Dep);

这个意思就是:在compilation当中,我们可以通过NormalModuleFactory 工厂创建一个普通的模块对象

we_30.png

走到1056行之后 可以看到 moduleFactory的 值 是一个类

我们继续往下走 这个this.semaphore.acquire 函数 是webpack默认启用了并发100的打包操作 这个方法 :moduleFactory.create 创建模块 进入这个方法去看一看:

we_31.png

发现是在NomalModuleFactory.js里面

we_32.png

里面有个触发监听事件 this.hooks.beforeResolve 点进去 再进去函数里的回调函数里 会进到下面的factory函数里

we_33.png

在这里面会触发一个factory钩子监听(这是处理loader的 再点进去 )上述操作完成,获取到一个函数被存在factory里,然后对它进行调用了

we_34.png

这个函数调用里又触发了一个叫resolver的钩子(处理loader,拿到了reesolve方法 就意味着所有的loader处理完成) 完成之后 就调用了resolver ,调用之后就会进入afterResolve钩子,就会触发 new NormalModule, 为了创建模块

we_36.png

在这里 129行里面的回调函数是进不去的 所有就打个130行的断点进去

we_37.png

然后进到138行 afterResolve 就会触发 new NormalModule,

we_38.png

走进去调用当中 继续

createdModule = this.hooks.module.call(createdModule, result);

we_41.png

在完成上述操作之后就将module 进行了保存和一些其它属性的添加

Compilation.js _addModuleChain方法里面

_addModuleChain

we_42.png

we_43.png

当normalModule 成功之后就调用buildModule

buildModule

we_40.png

调用 buildModule 方法开始编译 会调用 build ,build会调用doBuild

build

build之前 要doBuild一下

we_39.png

doBuild 读取 module 中内容并进行处理。用对应的 loaders ,把我们的源码 module 转成标准的 JS 模块对象。然后执行 Parse.parse() module处理完成 就开始处理依赖的module

在buildModule之后 调用afterBuild 来检查是否有依赖进行处理 没有就结束有就继续处理 用dependence数组的长度, 因为dependence是存储依赖关系的

we_45.png

we_44.png

这是递归加载依赖函数 // 当前逻辑就表示module 有需要依赖加载的模块,因此我们可以再单独定义一个方法来实现

we_46.png

parse

parse主要功能 acron 将 JS 解析为 AST。然后遍历 AST

parse的最大作用就是收集模块依赖关系

parser.js中主要就三个方法:

  • getAST: 将获取到的模块内容 解析成AST语法树
  • getDependencies:遍历AST,将用到的依赖收集起来
  • transform:把获得的ES6AST转化成ES5

遍历 AST 找到入口中的所有依赖 module,加入 dependencies 数组。再依次对 dependencies 中的依赖 module 进行processModuleDependencies,这是一个递归遍历过程,会把所有的原来的 module 源码都聚合到一个对象(后面称为“chunk 初始态对象” )中。 如果发现动态引入例如 import(),那么就开启新的一轮addModuleChain。直到每一轮的递归结束,执行 seal() 。 此时调用多少次addModuleChain,就形成了多少个依赖树,就有多少个 chunk 初始态对象

生成阶段

6.调用seal方法,生成chunks,进行优化,生成输出代码。seal结束,compliation实例构建结束,这一次的构建过程结束

7.seal结束后 emit触发,webpack遍历所有compliation。assets文件,生成所有文件bundle,

8.done;将文件内容写入文件系统触发done方法。

chunk流程分析:

构建完成之后 开始执行 seal函数 处理chunk compiler.js中

const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
	if (err) return callback(err);
	this.hooks.compile.call(params);

	const compilation = this.newCompilation(params);

	this.hooks.make.callAsync(compilation, err => {
		if (err) return callback(err);

		compilation.finish(err => {
			if (err) return callback(err);

			compilation.seal(err => {
				if (err) return callback(err);

				this.hooks.afterCompile.callAsync(compilation, err => {
					if (err) return callback(err);

					return callback(null, compilation);
				});
			});
		});
	});
});

chunk生成过程

  • 先把 entry 中对应的每个 module 都生成一个新的 chunk
  • 遍历module.dependencies,将其依赖的模块也加入到上一步生成的chunk中
  • 若某个module是动态引入的,为其创建一个新的chunk,接着遍历依赖

1.先封闭模块,生成资源,这些资源保存在compilation.assets, compilation.chunks

2.调用compilation.createChunkAssets方法把所有依赖项通过对应的模板 render 出一个拼接好的字符串:

2.1 createChunkAssets执行过程中,会优先读取cache中是否已经有了相同hash的资源,如果有,则直接返回内容,否则才会继续执行模块生成的逻辑,并存入cache中

3.生成了字符串之后 就到了生成bundle文件阶段 遍历compilation.assets , 再写入文件系统

4.触发 compiler 的 emit 钩子

emit 钩子,主要做两件事。 一是 compiler.hooks.emit.callAsync() 接入各个 plugin(执行订阅了 emit 钩子的 plugin 的回调函数,这是我们修改最终文件的最后一个机会。) 二是 遍历 compliation.assets 生成 bundle 们写入文件系统,写入后触发 compiler 的 done 钩子

到了 done 钩子,就代表结束构建流程了。

[webpack官方文档]  www.webpackjs.com/concepts/ 

[Webpack源码解读:理清编译主流程]  juejin.cn/post/684490… 

[[万字总结 一文吃透 Webpack 核心原理]  juejin.cn/post/694904…

[面试官:webpack原理都不会?]  juejin.cn/post/685953… 

[webpack 工作流程]  juejin.cn/post/698889… 

[webpack构建流程分析]  juejin.cn/post/684490…