webpck深入浅出教程(三)webpack源码分析

webpack入口文件:

执行npm命令后,查看node_modules\.bin目录下是否存在webpack.sh或者webpack.cmd文件,如果存在就执行,如果不存在就抛出错误。

webpack实际入口文件是:

node_modules\webpack\bin\webpack.js

一、分析入口文件webpack.js

#!/usr/bin/env node

// @ts-ignore
process.exitCode = 0;//1、状态码

const runCommand = (command, args) => {};//2、运行某个命令

const isInstalled = packageName => {};//3、判断是否安装某个包


const CLIs = ["webpack-cli","webpack-command"]//webpack的cli工具webpack-cli或者webpack-command
	
const installedClis = CLIs.filter(cli => cli.installed);//过滤两个cli工具是否安装

if (installedClis.length === 0) {//根据安装的cli数量处理
	
} else if (installedClis.length === 1) {
	
} else {
	process.exitCode = 1;
}

二、进入webpack-cli(或者webpack-command处理)

webpack-cli/bin/cli.js文件分析,其实就是执行webpack-cli

1、解析命令,看命令是否需要经过编译,如果是NON_COMPILATION_CMD数组中的以下命令

const NON_COMPILATION_ARGS = ["init", "migrate", "serve", "generate-loader", "generate-plugin", "info"];

判断是否有serve,并做处理后return返回

const NON_COMPILATION_CMD = process.argv.find(arg => {
		if (arg === "serve") {
			global.process.argv = global.process.argv.filter(a => a !== "serve");
			process.argv = global.process.argv;
		}
		return NON_COMPILATION_ARGS.find(a => a === arg);
	});

如果不是NON_COMPILATION_CMD数组中的这些命令则引入yargs,对命令行定制处理

2、分析命令行参数及webpack配置文件中的参数,对各个参数做转换,组成webpack编译需要的配置项

3、进入webpack,根据options配置项编译和构建

processOptions的过程中,判断命令行中的参数和配置文件的参数,对outputOptions增加对应的option。

处理完options后,将参数传给webpack对象,并实例化生成全局的编译对象compiler = webpack(options);

三、进入webpack/lib/webpack.js

1、如果传入options是个数组,递归执行,否则就调用WebpackOptionsDefaulter.js中的process对options处理

2、接着实例化compiler对象,并执行环境相关的hooks,最后WebpackOptionsApply调用根据options.target类型设置webpack对应的默认插件。

3、最后执行compiler的run方法

四、进入compiler.js中run方法

1、compiler中的run方法核心就这句this.compile(onCompiled),由于compiler类中没有hooks,而且由于compiler和compilation类都继承了Tapable类,并且compiler和compilation类中this.hooks中定义了很多个Hook,所以按照逻辑推测(很早就知道。。。)是Tapable中的hooks方法。所以我们待会再来看下面这几句代码,先看Tapable实现。

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);
				});
			});
		});

2、Tapable是一个类似与Nodejs的EventEmitter的库,主要是控制钩子函数的发布订阅,控制着webpack的plugin系统。

Tapable中暴漏了很多Hook类,主要是为插件提供挂载的钩子,入口文件中暴漏了以下Hooks:

exports.SyncHook = require("./SyncHook");//同步钩子
exports.SyncBailHook = require("./SyncBailHook");//同步熔断钩子
exports.SyncWaterfallHook = require("./SyncWaterfallHook");//同步流水钩子
exports.SyncLoopHook = require("./SyncLoopHook");//同步循环钩子
exports.AsyncParallelHook = require("./AsyncParallelHook");//异步并发钩子
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");//异步并发熔断钩子
exports.AsyncSeriesHook = require("./AsyncSeriesHook");//异步串行钩子
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");//异步串行熔断钩子
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");//异步串行流水钩子

webapck中所有插件都监听Hooks,一旦Hooks触发后,对应插件也执行,并且每个插件都会实现一个apply方法,传参为compiler对象。

序号 钩子名称 执行方式 使用要点
1 SyncHook 同步串行 不关心监听函数的返回值
2 SyncBailHook 同步串行 只要监听函数中有一个函数的返回值不为 null,则跳过剩下所有的逻辑
3 SyncWaterfallHook 同步串行 上一个监听函数的返回值可以传给下一个监听函数
4 SyncLoopHook 同步循环 当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环
5 AsyncParallelHook 异步并发 不关心监听函数的返回值
6 AsyncParallelBailHook 异步并发 只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,然后执行这个被绑定的回调函数
7 AsyncSeriesHook 异步串行 不关系callback()的参数
8 AsyncSeriesBailHook 异步串行 callback()的参数不为null,就会直接执行callAsync等触发函数绑定的回调函数
9 AsyncSeriesWaterfallHook 异步串行 上一个监听函数的中的callback(err, data)的第二个参数,可以作为下一个监听函数的参数

Tapable的使用-钩子的绑定与执行,类似于发布订阅的on和emit

Async Sync
tapAsync/tapPromise/tap

tap

callAsync/promise call

3、这句代码是关键:

this.compile(onCompiled)

先看compile实现:

webpack编译的执行顺序为以下这些hooks的执行顺序:

beforerun->beforeCompile->compile->aftercompile->make(从entry开始递归分析依赖)->compile调用compilation中的finish(上报模块错误)->compile调用compilation中的seal(优化)->afterCompile

以上这些hooks为一些关键流程的hooks,实际compiler和compilation上各自挂载了很多hooks,有入口相关,模块构建相关,优化相关,输出相关,watch相关,环境相关等大约一百左右。

compile(callback) {//编译
		const params = this.newCompilationParams();
		this.hooks.beforeCompile.callAsync(params, err => {//编译前的准备工作
			if (err) return callback(err);

			this.hooks.compile.call(params);
                      //生成编译实例compilation对象
			const compilation = this.newCompilation(params);
                      //对entry开始递归分析依赖,对每个依赖模块build
			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);
                                              //执行回调onCompiled
							return callback(null, compilation);
						});
					});
				});
			});
		});
	}

4、期间生成了compilation对象,挂载了一堆属性,并执行了两个hooks

createCompilation() {
		return new Compilation(this);
	}

	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;
	}

五、输出到文件

执行完compile函数就执行回调-onCompiled函数

const onCompiled = (err, compilation) => {
			if (err) return finalCallback(err);

			if (this.hooks.shouldEmit.call(compilation) === false) {
				const stats = new Stats(compilation);
				stats.startTime = startTime;
				stats.endTime = Date.now();
				this.hooks.done.callAsync(stats, err => {
					if (err) return finalCallback(err);
					return finalCallback(null, stats);
				});
				return;
			}

			this.emitAssets(compilation, err => {
				if (err) return finalCallback(err);

				if (compilation.hooks.needAdditionalPass.call()) {
					compilation.needAdditionalPass = true;

					const stats = new Stats(compilation);
					stats.startTime = startTime;
					stats.endTime = Date.now();
					this.hooks.done.callAsync(stats, err => {
						if (err) return finalCallback(err);

						this.hooks.additionalPass.callAsync(err => {
							if (err) return finalCallback(err);
							this.compile(onCompiled);
						});
					});
					return;
				}

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

					const stats = new Stats(compilation);
					stats.startTime = startTime;
					stats.endTime = Date.now();
					this.hooks.done.callAsync(stats, err => {
						if (err) return finalCallback(err);
						return finalCallback(null, stats);
					});
				});
			});
		};

从中我们看到执行的hooks为输出文件到磁盘的任务

shouldEmit(判断是否应该输出文件)-> emit -> needAdditionalPass->done

六,其他一些核心概念

ModuleFactory

模块工厂就是负责构造模块的实例,介绍两种NormalModuleFactoryContextModuleFactory。两者不同的地方在于后者用于解析动态import(). 模块工厂主要是用于将Resolver解析成功的请求里的源码从文件中拿出,在内存中创建一个模块对象(NormalModule)

Resolver

请求一个模块的时,将模块名或者相对地址发给模块解析器,它会去解析出绝对地址去寻找那个模块,看是否存在,如果存在则返回相应的模块信息,包括上下文等。这里的请求可以类似网络请求一样携带上查询参数之类的,Resolver将会返回额外信息。webpack4里将Resolver这个实例抽出来单独发了一个包enhanced-resolve, 抽象出来可以便于用户实现自己的Resolver

七、webpack流程总结

借用腾讯@程柳锋老师的webpack中hooks的编译流程图:

webpck深入浅出教程(三)webpack源码分析_第1张图片

上面是hooks调用顺序,下面我们总结webpack总的工作流程,包含加载 - 编译 - 输出三个步骤:

1.初始化。 从webpck的配置文件(webpack.config.js或其它)中读取配置信息,或者从shell脚本的输入参数中读取配置信息,初始化本次的执行环节。

2.加载插件,准备编译。 根据配置信息,加载本次执行所需要的所有相关插件。

3.读取入口文件。 根据配置信息的entry属性依次读取要编译入的文件。

4.编译。 对第3步中读取到的入口文件内容进行编译,根据配置信息匹配相对于的Loader进行编译,同时递归地对该文件所依赖的的文件/资源匹配相对于的Loader进行编译。

5.完成编译。 第四步中,得到每个模块被编译后的内容,以及模块之间的依赖关系。

6.准备输出。 根据第5步中的编译内容和模块的依赖关系,将每一个主入口文件和其所依赖的所有模块组成一个chunk,根据配置的entry得到一个chunk列表。

7.输出到文件。 根据第6步的结果结合webpack配置信息中的output参数按照指定的格式,对每一个输出chunk进行命名,chunk内容转换(主要是指输出的模块类型,比如指定输出amd,umd等)并输出到指定的路径中。

八、参考资料:

1、https://medium.com/webpack/the-contributors-guide-to-webpack-part-2-9fd5e658e08c

2、https://juejin.im/post/5abf33f16fb9a028e46ec352

3、https://frontendmasters.com/courses/webpack-plugins/ 强烈推荐,webpack核心开发者Sean Larkin视频讲解webpack

4、极客时间《玩转webpack4》课程

你可能感兴趣的:(webpack)