源码分析之路——webpack

1,931 阅读18分钟

工作流程简述

Webpack 的运行流程是 一个串行的过程,会经过大致以下流程 。

  1. 初始化参数 : 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译: 初始 化 Compiler 对象,加载所有配置的插件,执行编译
  3. 确定入口:通过entry配置找出所有入口文件
  4. 编译模块:调用对应的loader编译所有的模块
  5. 组装输出:模块编译完成后,根据入口和模块的依赖关系,组装成一个个包含多个模块的 Chunk,将Chunk代码块放到输出列表
  6. 输出完成: 根据输出文件的配置,将文件写入系统

分阶段概述webpack的工作流程

1. 初始化

初始化阶段大致经过下面这些步骤:

  • 初始化参数: 读取并合并参数,并执行配置文件中的插件实例化语句 new Plugin()
  • 实例化Compiler: 初始化 Compiler 实例,负责文件监听和启动编译
  • 加载插件: 依次调用插件的 apply 方法,并给插件传入compiler实例的引用
  • environment: 应用 Node.js 风格的文件系统到 compiler 对象,以方便后续的文件寻找和读取
  • entry-option: option 读取配置的 Entrys,为每个 Entry 实例化一个对应的 EntryPlugin,为后面该 Entry 的递归解析工作做准备
  • after-plugins: 调用完所有内置的和配置的插件的 apply 方法
  • after-resolvers: 根据配置初始化完 resolver,resolver 负责在文件系统中寻找指定路径的文件

2. 编译

  • run: 启动一次新的编译。
  • watch-run: 和 run 类似,区别在于它是在监听模式下启动的编译,在这个事件中可以获取到是哪些文件发生了变化导致重新启动一次新的编译。
  • compile: 该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上 compiler 对象。
  • compilation: 当 Webpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被创建。一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象也提供了很多事件回调供插件做扩展。
    • build-module: 使用对应的 Loader 去转换一个模块
    • normal-module-loader: 在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 Webpack 后面对代码的分析。
    • program: 从配置的入口模块开始,分析其 AST,当遇到 require 等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系。
    • seal: 所有模块及其依赖的模块都通过 Loader 转换完成后,根据依赖关系开始生成 Chunk。
  • make: 一个新的 Compilation 创建完毕,即将从 Entry 开始读取文件,根据文件类型和配置的 Loader 对文件进行编译,编译完后再找出该文件依赖的文件,递归的编译和解析。
  • after-compile: 一次 Compilation 执行完成。
  • invalid: 当遇到文件不存在、文件编译错误等异常时会触发该事件,该事件不会导致 Webpack 退出

3. 输出

  • should-emit: 所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。
  • emit: 确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。
  • after-emit: 文件输出完毕。
  • done: 成功完成一次完成的编译和输出流程。
  • failed: 如果在编译和输出流程中遇到异常导致 Webpack 退出时,就会直接跳转到本步骤,插件可以在本事件中获取到具体的错误原因。

源码分析

从使用开始:

  1. 最简单的使用方式,使用命令行打包,可以看webpack文档的第一个例子: webpack中文文档指南

具体代码就不跟着描述了,具体步骤可以看上面文档的链接, 写完最简单的demo之后(需要包括package.json文件,index.html等一个完整的项目文件夹),执行 npx webpack命令,这个命令会将我们的脚本作为入口起点,然后 输出 为 main.js。Node 8.2+ 版本提供的 npx 命令,可以运行在初始安装的 webpack 包(package)的 webpack 二进制文件

这时候就能在我们的dist目录下看到main.js这个文件了,打开这个文件会发现你的代码已经经过了压缩。

  1. 通过配置文件自由配置打包
    • 这个方式通过自定义配置webpack.config.js文件,可以灵活的按照你配置的方式去打包,只需要通过命令行npx webpack --config webpack.config.js执行就可以了,具体配置方式指路: 配置
  2. 最常见的方式:npm脚本
    • 在 package.json 添加一个 npm 脚本(npm script),即build": "webpack",可以使用 npm run build 命令,来替代我们之前使用的 npx 命令。
    • 不过当然了,现在的vue-cli3已经不需要进行webpack的配置了,vue-cli 3使用了零配置思路,所以项目初始化后,没有了以前熟悉的 build 目录,也就没有了 webpack.base.config.js、webpack.dev.config.js 、webpack.prod.config.js 等配置文件,npm run build也使用的是vue-cli-service build命令了。

webpack的定义

从最简单的命令行方式来看,webpack这个命令做的事情最简化来说,其实就是读取源码通过一系列的解析和组装最后输出打包文件的过程。

下面来看一下webpack定义的地方,在lib文件夹下面的webpack.js文件里,定义的主要代码如下:

const webpack = (options, callback) => {
	// 1. 判断options的合法性
	...
	// 2. 定义编译对象compiler,整合options
	// 如果配置项是数组,则调用MultiCompiler并循环遍历使用webpack函数
	let compiler;
	if (Array.isArray(options)) {
		compiler = new MultiCompiler(
			Array.from(options).map(options => webpack(options))
		);
	} else if (typeof options === "object") {
	
	    // 当参数项是对象时,整合参数,实例化compiler
		options = new WebpackOptionsDefaulter().process(options);

		compiler = new Compiler(options.context);
		compiler.options = options;
		new NodeEnvironmentPlugin({
			infrastructureLogging: options.infrastructureLogging
		}).apply(compiler);
		
		// 3. 调用插件plugin的apply方法,方便插件使用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);
				}
			}
		}
		compiler.hooks.environment.call();
		compiler.hooks.afterEnvironment.call();
		compiler.options = new WebpackOptionsApply().process(options, compiler);
	} else {
		throw new Error("Invalid argument: options");
	}
	// 4. 如果有回调,执行compiler.run(callback);
	if (callback) {
		if (typeof callback !== "function") {
			throw new Error("Invalid argument: callback");
		}
		// 如果配置了监听项,处于监听模式,compiler 会触发诸如 watchRun, watchClose 和 invalid 等额外的事件
		// 通常用于开发环境中使用
		if (
			options.watch === true ||
			(Array.isArray(options) && options.some(o => o.watch))
		) {
			const watchOptions = Array.isArray(options)
				? options.map(o => o.watchOptions || {})
				: options.watchOptions || {};
			return compiler.watch(watchOptions, callback);
		}
		compiler.run(callback);
	}
	// 5. 返回compiler对象
	return compiler;
};

compiler实例

在文档介绍中是这么描述compiler的:

Compiler 模块是 webpack 的支柱引擎,它通过 CLI 或 Node API 传递的所有选项,创建出一个 compilation 实例。它扩展(extend)自 Tapable 类,以便注册和调用插件。大多数面向用户的插件首,会先在 Compiler 上注册。

通过前一段webpack函数定义的过程来看,最后返回的是一个compiler对象,而且当使用webpack传入的有callback回调时,这个compiler还调用了run方法,接下来我们就来看compiler中的逻辑:

Compiler.js文件同样位于lib文件夹下,这个文件主要定义了Compiler类,源代码略长,我就贴一下主要代码结构,

class Compiler extends Tapable {
    constructor(context) {
        this.hooks = {
            ...
            beforeRun,
            run,
            ...
        }
    }
    ...
}
module.exports = Compiler;

钩子函数

其实观察一下不难发现,Compiler中在constructor里定义了hooks,其实就一堆各个阶段的钩子函数:

钩子函数名称 功能
beforeRun compiler.run() 执行之前,添加一个钩子
run 开始读取 records 之前,钩入(hook into) compiler
beforeCompile 编译(compilation)参数创建之后,执行插件
compile 一个新的编译(compilation)创建之后,钩入(hook into) compiler
make 从Compilation的addEntry函数,开始构建模块
afterCompile 编译结束
thisCompilation compilation 事件之前执行
compilation 编译(compilation)创建之后,执行插件
shouldEmit 是否应该生成资源到 output 目录之前
emit 生成资源到 output 目录之前
afterEmit 生成资源到 output 目录之后
watchRun 监听模式下,一个新的编译(compilation)触发之后,执行一个插件,但是是在实际编译开始之前
failed 编译(compilation)失败
invalid 监听模式下,编译无效时
watchClose 监听模式停止
environment environment 准备好之后,执行插件
afterEnvironment environment 安装完成之后,执行插件
afterPlugins 设置完初始插件之后,执行插件
afterResolvers resolver 安装完成之后,执行插件
entryOption 在 entry 配置项处理过之后,执行插件
done 编译(compilation)完成

run()函数

主要看一下run函数:

run(callback) {
		// ...
		const onCompiled = (err, compilation) => {
			// ...
			if (this.hooks.shouldEmit.call(compilation) === false) {
				// ...
				this.hooks.done.callAsync(stats, err => {
					return finalCallback(null, stats);
				});
				return;
			}
			this.emitAssets(compilation, err => {
                // ...
			});
		};
        // ...
		this.hooks.beforeRun.callAsync(this, err => {
			this.hooks.run.callAsync(this, err => {
				this.readRecords(err => {
					this.compile(onCompiled);
				});
			});
		});
	}
  • 首先执行beforeRun这个钩子,绑定了读取文件的对象;
  • 接着执行run这个async钩子,在这个钩子中主要是处理缓存的模块,减少编译的模块,加速编译速度;
  • 接着调用了compile钩子,这个钩子的参数就是run里面的onCompiled函数,这个函数的作用就是将编译后的内容生成文件
    • onCompiled中首先使用shouldEmit判断编译是否成功,未成功则直接结束done
    • 否则的话,调用emitAssets打包文件

compile()函数

上面执行run的时候,最后一步调用了compile,这也是一个钩子函数

compile(callback) {
		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);
						});
					});
				});
			});
		});
	}

这一段的核心逻辑流程如下:

  • 调用beforeCompile钩子,
  • 调用compile钩子,
  • 实例化compilation对象,实例化过程中又调用了thisCompilation和compilation钩子函数,一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等 。 Compilation 对象也提供了很多事件回调给插件进行扩展
  • 接着调用make钩子函数
  • make调用完成后,开始用seal封装,
  • 封装完成后,调用了afterCompile钩子,表示一次 Compilation执行完成

这段代码不长,基本就是回调套回调,通过层层回调的关系实现了compile整个过程的生命周期。

compile()执行结束后,我们回看run函数中调用的地方,也就是compile(callback)中的这个参数回调,onCompiled方法,这是最后一步,调用emitAssets方法,emitAssets内部调用emit钩子,执行文件输出,最后调用done,到此成功完成一次输出。

Compilation对象

前面compile函数中实例化了Compilation对象,下面可以来梳理一下这个对象。

const compilation = this.newCompilation(params);

run函数里是这么实例化Compilation的,那就顺着这个newCompilation函数是怎么完成实例化这个过程的

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

这两段函数也很容易理解,也就是新建了一个Compilation类的实例对象,给这个实例添加上各种需要的属性,最后返回这个实例。

Compilation类的定义是在lib文件夹下的Compilation.js中;

class Compilation extends Tapable {
	constructor(compiler) {
		super();
		this.hooks = {
		// ...定义生命周期钩子
		};
	}
}

Compilation这个对象的理解比较费劲儿,我也是通过网上的各种分析和源码文件翻来覆去的找,才顺着代码梳理了一遍,也跟本人才疏学浅以及对webpack没那么熟悉可能有关系。

跟Compiler类一样,Compilation也在constructor中定义了各种生命周期钩子,不过由于定义的钩子太多,这里就不一一贴出来了,各个钩子的含义指路文档-> compilation 钩子

不过可以看到compile函数中实例化Compilation对象后并没有对它做什么操作,而是直接调用了make钩子函数,从这个类里面也看到代码只是做了个定义,还没有调用到它,那么来看看this.newCompilation(params);中的这个params,这个参数在调用beforeCompile之前进行了定义,它其实是个模块工厂,传递给compilation,方便之后的插件或钩子操作模块。后面执行make的时候调用了这个compilation对象。我们现在来看一下这个params参数:

const params = this.newCompilationParams();
newCompilationParams() {
	const params = {
		normalModuleFactory: this.createNormalModuleFactory(),
		contextModuleFactory: this.createContextModuleFactory(),
		compilationDependencies: new Set()
	};
	return params;
}

可以发现,这个参数里面主要包含了normalModuleFactory工厂和contextModuleFactory工厂,以及compilationDependencies。其中最常用的是normalModuleFactory,

createNormalModuleFactory() {
	const normalModuleFactory = new NormalModuleFactory(
		this.options.context,
		this.resolverFactory,
		this.options.module || {}
	);
	this.hooks.normalModuleFactory.call(normalModuleFactory);
	return normalModuleFactory;
}

在这个工厂的创建里主要传进了作用域context、resolverFactory工厂,以及模块module,那么这个options是怎么来的呢?这就需要回到webpack.js文件中了,

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

这里将传入的配置项和compiler对象整合处理了之后挂载到了compiler的options上,接下来就看看WebpackOptionsApply这个类的process方法是怎么处理的。WebpackOptionsApply类定义在lib文件夹下的WebpackOptionsApply.js文件里。

主要是看一下process这个方法,这里面其实为compiler集成了很多的插件,我们要看的是EntryOptionPlugin这个插件的调用:

new EntryOptionPlugin().apply(compiler);

追根溯源,这个EntryOptionPlugin插件中,使用entries参数创建一个单入口(SingleEntryDependency)或者多入口(MultiEntryDependency)依赖,多个入口时在make事件上注册多个相同的监听,并行执行多个入口。我们先选一个单入口SingleEntryDependency.js文件: 终于可以看见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);
	}
);

这里的tapAsync可以以异步的方式触及make钩子,SingleEntryPlugin插件在这个时候运行。然后调用compilation.addEntry(context, dep, name, callback)正式进入make阶段。

好了,绕了一大圈,现在让我们再回到compilation定义的地方,看看这个addEntry方法。

addEntry(context, entry, name, callback) {
		// ...
		this._addModuleChain(
			context,
			entry,
			module => {
				this.entries.push(module);
			},
			(err, module) => {
				if (err) {
					this.hooks.failedEntry.call(entry, name, err);
					return callback(err);
				}

				if (module) {
					slot.module = module;
				} else {
					const idx = this._preparedEntrypoints.indexOf(slot);
					if (idx >= 0) {
						this._preparedEntrypoints.splice(idx, 1);
					}
				}
				this.hooks.succeedEntry.call(entry, name, module);
				return callback(null, module);
			}
		);
	}

addEntry中其实没有做很多事,主要就是调用了this._addModuleChain方法,根据依赖查找对应的工厂函数,并调用工厂函数的create来创建模块的依赖,创建模块调用了this.hooks.buildModule钩子,开始执行module.build,在build成功触发succeedModule钩子,失败触发failedModule钩子。

不过build这个过程里并没有处理什么事,直接调用了afterBuild这个方法。

const afterBuild = () => {
	if (addModuleResult.dependencies) {
		this.processModuleDependencies(module, err => {
	        if (err) return callback(err);
			callback(null, module);
		});
	} else {
	    return callback(null, module);
	}
};

判断是否有依赖,如果有,需要调用processModuleDependencies来查找依赖,没有则直接结束。我们这里是从EntryOptionPlugin插件中调用的addEntry方法,所以传入了EntryOptionPlugin插件。

那么processModuleDependencies这个方法究竟做了什么呢?

processModuleDependencies(module, callback) {
		const dependencies = new Map();

		const addDependency = dep => {
			//...
		};

		const addDependenciesBlock = block => {
			if (block.dependencies) {
				iterationOfArrayCallback(block.dependencies, addDependency);
			}
			//...
		};

		try {
			addDependenciesBlock(module);
		} catch (e) {
			callback(e);
		}

		const sortedDependencies = [];

		for (const pair1 of dependencies) {
			for (const pair2 of pair1[1]) {
				sortedDependencies.push({
					factory: pair1[0],
					dependencies: pair2[1]
				});
			}
		}

		this.addModuleDependencies(
			module,
			sortedDependencies,
			this.bail,
			null,
			true,
			callback
		);
	}

这一段比较好解读,processModuleDependencies方法根据dependencies对象查找该module依赖中所有需要加载的资源和对应的工厂类,并把它们都传入到addModuleDependencies方法中,在addModuleDependencies方法中,异步执行工厂的create方法,在create的回调中执行buildModule来创建模块,这个方法其实之前在addEntry中也调用过,其实就是一个对依赖遍历递归的过程,最终等到这部分所有模块都build完成后,才走到addEntry的afterBuild钩子,这时整个make阶段才结束。

这段make阶段的梳理比较不好理解,下面用一个图来小结一下这个流程:

Compiler对象和Compilation对象

前面说了两个主要的对象,Compiler和Compilation,现小结一下这两者的含义和区别:

  • Compiler对象包含了 Webpack环境的所有配置信息,包含 options、loaders、plugins 等信息。这个对象在 Webpack 启动时被实例化,它是全局唯一的,可以简单地将 它理解为 Webpack 实例

  • Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件发生变化,便有一次新的 Compilation 被 创建 。 Compilation 对象也提供了很多事件回调供插件进行扩展。通过 Compilation也能读取到 Compiler对象

  • 它俩主要的区别:

    • Compiler代表了整个 Webpack从启动到关闭的生命周期,
    • 而 Compilation 只代表一次新的编译。

seal过程

上面总算顺了一下Compilation对象,回到comile的make钩子,当make这一步骤完成后,调用了compilation.finish,finish回调中又调用了seal函数。

seal,顾名思义,就是封装、打包的意思,webpack的核心功能就是编译和打包,下面来具体看看这个打包过程:

seal(callback) {
		this.hooks.seal.call();

		while (
			this.hooks.optimizeDependenciesBasic.call(this.modules) ||
			this.hooks.optimizeDependencies.call(this.modules) ||
			this.hooks.optimizeDependenciesAdvanced.call(this.modules)
		) {
			/* empty */
		}
		this.hooks.afterOptimizeDependencies.call(this.modules);

		this.hooks.beforeChunks.call();
		for (const preparedEntrypoint of this._preparedEntrypoints) {
			const module = preparedEntrypoint.module;
			const name = preparedEntrypoint.name;
			const chunk = this.addChunk(name);
			const entrypoint = new Entrypoint(name);
			entrypoint.setRuntimeChunk(chunk);
			entrypoint.addOrigin(null, name, preparedEntrypoint.request);
			this.namedChunkGroups.set(name, entrypoint);
			this.entrypoints.set(name, entrypoint);
			this.chunkGroups.push(entrypoint);

			GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
			GraphHelpers.connectChunkAndModule(chunk, module);

			chunk.entryModule = module;
			chunk.name = name;

			this.assignDepth(module);
		}
		buildChunkGraph(
			this,
			/** @type {Entrypoint[]} */ (this.chunkGroups.slice())
		);
		this.sortModules(this.modules);
		this.hooks.afterChunks.call(this.chunks);

		this.hooks.optimize.call();

		while (
			this.hooks.optimizeModulesBasic.call(this.modules) ||
			this.hooks.optimizeModules.call(this.modules) ||
			this.hooks.optimizeModulesAdvanced.call(this.modules)
		) {
			/* empty */
		}
		this.hooks.afterOptimizeModules.call(this.modules);

		while (
			this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups) ||
			this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups) ||
			this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups)
		) {
			/* empty */
		}
		this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups);

		this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
			if (err) {
				return callback(err);
			}

			this.hooks.afterOptimizeTree.call(this.chunks, this.modules);

			while (
				this.hooks.optimizeChunkModulesBasic.call(this.chunks, this.modules) ||
				this.hooks.optimizeChunkModules.call(this.chunks, this.modules) ||
				this.hooks.optimizeChunkModulesAdvanced.call(this.chunks, this.modules)
			) {
				/* empty */
			}
			this.hooks.afterOptimizeChunkModules.call(this.chunks, this.modules);

			const shouldRecord = this.hooks.shouldRecord.call() !== false;

			this.hooks.reviveModules.call(this.modules, this.records);
			this.hooks.optimizeModuleOrder.call(this.modules);
			this.hooks.advancedOptimizeModuleOrder.call(this.modules);
			this.hooks.beforeModuleIds.call(this.modules);
			this.hooks.moduleIds.call(this.modules);
			this.applyModuleIds();
			this.hooks.optimizeModuleIds.call(this.modules);
			this.hooks.afterOptimizeModuleIds.call(this.modules);

			this.sortItemsWithModuleIds();

			this.hooks.reviveChunks.call(this.chunks, this.records);
			this.hooks.optimizeChunkOrder.call(this.chunks);
			this.hooks.beforeChunkIds.call(this.chunks);
			this.applyChunkIds();
			this.hooks.optimizeChunkIds.call(this.chunks);
			this.hooks.afterOptimizeChunkIds.call(this.chunks);

			this.sortItemsWithChunkIds();

			if (shouldRecord) {
				this.hooks.recordModules.call(this.modules, this.records);
				this.hooks.recordChunks.call(this.chunks, this.records);
			}

			this.hooks.beforeHash.call();
			this.createHash();
			this.hooks.afterHash.call();

			if (shouldRecord) {
				this.hooks.recordHash.call(this.records);
			}

			this.hooks.beforeModuleAssets.call();
			this.createModuleAssets();
			if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
				this.hooks.beforeChunkAssets.call();
				this.createChunkAssets();
			}
			this.hooks.additionalChunkAssets.call(this.chunks);
			this.summarizeDependencies();
			if (shouldRecord) {
				this.hooks.record.call(this, this.records);
			}

			this.hooks.additionalAssets.callAsync(err => {
				if (err) {
					return callback(err);
				}
				this.hooks.optimizeChunkAssets.callAsync(this.chunks, err => {
					if (err) {
						return callback(err);
					}
					this.hooks.afterOptimizeChunkAssets.call(this.chunks);
					this.hooks.optimizeAssets.callAsync(this.assets, err => {
						if (err) {
							return callback(err);
						}
						this.hooks.afterOptimizeAssets.call(this.assets);
						if (this.hooks.needAdditionalSeal.call()) {
							this.unseal();
							return this.seal(callback);
						}
						return this.hooks.afterSeal.callAsync(callback);
					});
				});
			});
		});
	}
  1. 咋一看看过去,seal中几乎都是调用了各种钩子函数,真正的构建和优化工作,其实都是插件做的
  2. 循环调用optimizeDependenciesBasic、optimizeDependencies、optimizeDependenciesAdvanced钩子;
  3. afterOptimizeDependencies钩子
  4. beforeChunks钩子
  5. sortModules
  6. afterChunks钩子
  7. optimize钩子,优化阶段开始时触发
  8. 循环调用optimizeModulesBasic、optimizeModules、optimizeModulesAdvanced钩子,跟第2步的钩子不同
  9. afterOptimizeModules钩子
  10. 循环调用optimizeChunksBasic、optimizeChunks、optimizeChunksAdvanced钩子,优化 chunk
  11. afterOptimizeChunks钩子
  12. optimizeTree钩子,异步优化依赖树,这里又接着在依赖树里继续调用各生命周期钩子,调用完成后,seal结束

seal结束之后,触发了compiler的afterCompile钩子函数,这个时候compilation总算完成了它的工作,此时compiler的compile方法也就执行完毕了。

onCompiled

其实到这里,其实还在最开始提到的那个run函数里,在compiler的run钩子中,使用了onCompiled作为compile的参数,开始进行编译

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

先看一下onCompiled这个参数,上文有提到过,onCompiled中首先使用shouldEmit判断编译是否成功,未成功则直接调用done钩子,否则的话,调用emitAssets打包文件。 在compile这个方法的最后一步,即make完成、seal也完成之后,调用afterCompile钩子并执行onCompiled。

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

到这里,也就完成了compiler.run这个流程。

webpack事件流机制

经过上面对webpack流程的分析,我终于大致了解了webpack的运行方式,希望自己之后使用的时候可以知其然,知其所以然。

上面已经借《深入浅出Webpack》这一书第五章的部分归纳了整个webpack的流程,但是还可以再简化为如下流程:

初始化配置参数 -> 绑定钩子函数 -> 通过Entry逐一遍历 -> 编译文件 -> 打包输出

webpack的事件流机制使得webpack可以借助各种插件实现你想配置的一些功能。

Webpack 就像 一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果 。这条生产线上的每个处理流程的职责都是单一的,多个流程之间存在依赖关系,只有在 完成当 前处理后才能提交给下一个流程去处理。插件就像插入生产线中的某个功能,在特定的时机 对生产线上的资源进行处理。 Webpack通过 Tapable (github.com/webpack/tap…)来组织这条复杂的生产线。 Webpack 在运行的过程中会广播事件,插件只需要监听它所 关心的事件,就能加入这条生产 线中,去改变生产线的运作。 Webpack的事件流机制保证了插件的有序性,使得整个系统的 扩展性良好。
——《深入浅出Webpack》

Webpack 的事件流机制应用了观察者模式,Compiler 和 Compilation都继承自 Tapable,Tapable的实现原理如下:

class SyncHook{
    constructor(){
        this.hooks = [];
    }

    // 订阅事件
    tap(name, fn){
        this.hooks.push(fn);
    }

    // 发布
    call(){
        this.hooks.forEach(hook => hook(...arguments));
    }
}

所以只要我们能拿到 Compiler 或 Compilation 对象,就能使用tap来订阅事件,使用 call 来广播新的事件,所以在新开发的插件中也能广播事件,为其他插件监听使用。

总结

源码到这里其实只是梳理了整个大致流程,更具体的实现就没有细看了,感觉最核心的就是compiler和compilation两个对象了。webpack封装的很好,只暴露出来compiler和compilation这两个对象,我们可以调用合适的钩子,开发出新的插件,而这种设计使得扩展性很强大,通过各种插件的补充, Webpack几乎能胜任任何场景。

这篇源码读的确实有点心肌梗塞,各种各样的生命周期钩子,调不完的函数,还有看不完的插件,菜鸟看这种体积的代码,想哭。

本文整理主要来自《深入浅出Webpack》的第五章

参考文章: