webpack源码执行过程分析,loaders+plugin

930 阅读7分钟

webpack运行于node js之上,了解源码的执行,不仅可以让我们对webpack的使用更为熟悉,更会增强我们对应用代码的组织能力,

本篇文章重点从webpack核心的两个特性loader,plugin,进行深入分析,

我们从一个例子出发来分析webpack执行过程,地址

我们使用 vscode 调试工具来对webpack进行调试,

首先我们从入口出发

"build":"webpack --config entry.js"

示例项目通过npm run build 进行启动,npm run 会新建一个shell,并将 node_modules/.bin 下的所有内容加入环境变量,我们查看下.bin 文件夹下内容

webpack
webpack-cli
webpack-dev-server

可以看到webpack便在其中, 打开文件,可以看到文件头部

#!/usr/bin/env node

使用node执行此文件内容,webpack 文件的主要内容是判断webpack-cli或者webpack-command有没有安装,如果有安装则执行对应文件内容,本例安装了webpack-cli,所以通过对目标cli的require,进入到对应cli的执行,

webpack-cli webpack-cli是一个自执行函数,对我们在命令行传入的一些参数进行了解析判断,核心内容是把webpack入口文件作为参数,执行webpack,生成compiler

    try {
		compiler = webpack(options);
	} catch (err) {
		if (err.name === "WebpackOptionsValidationError") {
		if (argv.color) console.error(`\u001b[1m\u001b[31m${err.message}\u001b[39m\u001b[22m`);
		else console.error(err.message);
			// eslint-disable-next-line no-process-exit
			process.exit(1);
		}

		throw err;
	}

生成compiler后,执行compiler.run()或者compiler.watch(), 本例未启动热更新所以执行的是 compiler.run()

	if (firstOptions.watch || options.watch) {
		const watchOptions = firstOptions.watchOptions || firstOptions.watch || options.watch || {};
		if (watchOptions.stdin) {
			process.stdin.on("end", function(_) {
				process.exit(); // eslint-disable-line
			});
			process.stdin.resume();
		}
		compiler.watch(watchOptions, compilerCallback);
		if (outputOptions.infoVerbosity !== "none") console.error("\nwebpack is watching the files…\n");
		if (compiler.close) compiler.close(compilerCallback);
	} else {
		compiler.run(compilerCallback);
		if (compiler.close) compiler.close(compilerCallback);
	}

既然已经知道核心是这两个参数的执行,我们即可模拟一个webpack的执行过程,本例中,我们创建一个debug.js

const webpack = require('webpack');
const options = require('./entry.js');

const compiler = webpack(options);

我们在webpack()函数前面加上断点,即可通过vscode开始debug 我们先对生成compiler过程进行分析,

webpack函数

const webpack = (options, callback) => {
	const webpackOptionsValidationErrors = validateSchema(
		webpackOptionsSchema,
		options
	);
	if (webpackOptionsValidationErrors.length) {
		throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
	}
	let compiler;
	if (Array.isArray(options)) {
		compiler = new MultiCompiler(options.map(options => webpack(options)));
	} else if (typeof options === "object") {
		options = new WebpackOptionsDefaulter().process(options);

		compiler = new Compiler(options.context);
		compiler.options = options;
		new NodeEnvironmentPlugin().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");
	}
	if (callback) {
		if (typeof callback !== "function") {
			throw new Error("Invalid argument: callback");
		}
		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);
	}
	return compiler;
};

我们可以看到,有对options参数的验证validateSchema(webpackOptionsSchema,options); 有对默认配置的合并 options = new WebpackOptionsDefaulter().process(options); 合并内容 然后对所有的plugins配置进行注册操作

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

关于这里的注册,我们可以通过写一个plugin来描述执行过程, 本例中我们新建一个testplugin文件,

testplugin

module.exports = class testPlugin{
    apply(compiler){
        console.log('注册')
        compiler.hooks.run.tapAsync("testPlugin",(compilation,callback)=>{
            console.log("test plugin")
            callback()
        })
    }
}

关于插件的编写,我们只需要提供一个类,prototype上含有apply函数,同时拥有一个compiler参数,之后通过tap注册compiler上的hook,使得webpack执行到指定时机执行回调函数,具体编写方法参考写一个插件 本示例插件中,我们在compiler的run hook上注册了testplugin插件,回调的内容为打印 “test plugin”,并且,在注册的时候我们会打印 ”注册“,来跟踪plugin的注册执行流程,

回到webpack 函数,可以看到,进行完插件的注册,就会执行两个hook的回调,

compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();

这时,就会执行我们注册在environment,afterEnvironment上的plugin的回调,其他插件的回调执行也是通过call或者callAsync 来触发执行,webpack整个源码执行过程中会在不同的阶段执行不同的hook的call函数,所以,在我们编写插件的过程中要对流程有些了解,从而将插件注册在合适的hook上,

webpack函数的最后,就是执行compiler.run函数,我们在这里加上断点,进入compiler.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);
				});
			});
		});

compiler.run 函数中也是执行了一系列的hook,我们编写的testplugin就会在this.hooks.run.callAsync处执行,关于plugin的注册和运行具体细节,本篇先不讲,只需知道注册通过tap,运行通过call即可,, 到了这里,基本的plugin的运行过程我们已经了解,接下来我们通过几个目标来对loader的执行过程进行分析,

  1. 模块如何匹配到相对应loader
  2. 模块是如何递归的解析当前模块引用模块的
  3. loader是在哪里执行的

回到源代码,执行完一些hooks后,进入到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();

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

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

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

依旧是一些hooks的执行,重点是make 的hook,我们进入,make hook通过htmlWebpackPlugin注册了一个回调,回调中又注册了一个SingleEntryPlugin,然后又重新执行了make.callAsync,进入了SingleEntryPlugin的回调

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

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

可以看到,主要执行了addEntry方法,addEntry中执行addEntry hook,然后调用_addModuleChain,

addEntry(context, entry, name, callback) {
		this.hooks.addEntry.call(entry, name);

		const slot = {
			name: name,
			// TODO webpack 5 remove `request`
			request: null,
			module: null
		};

		if (entry instanceof ModuleDependency) {
			slot.request = entry.request;
		}

		// TODO webpack 5: merge modules instead when multiple entry modules are supported
		const idx = this._preparedEntrypoints.findIndex(slot => slot.name === name);
		if (idx >= 0) {
			// Overwrite existing entrypoint
			this._preparedEntrypoints[idx] = slot;
		} else {
			this._preparedEntrypoints.push(slot);
		}
		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);
			}
		);
	}

然后_addModuleChain中通过moduleFactory.create 创建modeuleFactory对象,然后执行buildModule

this.buildModule(module, false, null, null, err => {
							if (err) {
								this.semaphore.release();
								return errorAndCallback(err);
							}

							if (currentProfile) {
								const afterBuilding = Date.now();
								currentProfile.building = afterBuilding - afterFactory;
							}

							this.semaphore.release();
							afterBuild();
						});

对于loader的匹配,发生于moduleFactory.create()中,其中执行beforeResolve hook,执行完的回调函数中执行factory,factory中执行resolver,resolver是 resolver hook的回调函数,其中通过this.ruleSet.exec和request的分割分别完成loader的匹配,对module匹配到的loader的生成即在这里完成,之后注入到module对象中,接下来我们回到moduleFactory.create的回调函数 此时生成的module对象中有几个显著的属性,

userRequest:
loaders

即当前模块的路径和匹配到的loader,本例中index.js模块即匹配到了testloader,我们编写的测试loader,

testloader

module.exports = function(source){
    console.log("test loader")
    return source+";console.log(123)"
}

关于loader的编写本篇也不细讲,借用一句文档的描述

A loader is a node module that exports a function. This function is called when a resource should be transformed by this loader. The given function will have access to the Loader API using the thiscontext provided to it.

如何写一个loader

我们回到源码,moduleFactory.create回调函数中,执行了buildModule, buildModule中执行了module.build(),build中执行doBuild,doBuild中执行runloaders,自此开始即为对loader的执行,runloaders中执行iteratePitchingLoaders,然后执行loadLoader,通过import或者require等模块化方法加载loader资源,这里分为几种loaders,根据不同情况,最终执行runSyncOrAsync,runSyncOrAsync中

var result = (function LOADER_EXECUTION() {
			return fn.apply(context, args);
		}());

通过LOADER_EXECUTION()方法对loader进行,执行,返回执行结果,继续执行其他loader,loader的执行即为此处, loader执行完成之后,buildModule执行完成,进行callback的执行,其中执行了moduleFactory.create中定义的afterBuild函数,afterBuild函数执行了processModuleDependencies函数,processModuleDependencies函数中通过内部定义的addDependency和addDependenciesBlock方法,生成当前module所依赖的module,执行addModuleDependencies

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

传入此模块的依赖,addModuleDependencies中循环对sortedDependencies进行了factory.create,factory.create中又执行了beforeResolve hook,从而又执行上面流程,匹配loader,执行loader,对依赖进行遍历等步骤,所以,通过这个深度优先遍历,即可对所有模块及其依赖模块进行loade的匹配和处理,自此,loader学习的三个目标已经达成

make hook主要内容即是这些,之后又执行了seal,afterCopile等等等hook,这些即为一些关于代码分割,抽离等等插件的执行时机,为我们插件的编写提供了一些入口,compiler和compilation执行过程中的所有hook可以查看文档,一共有九十多个(汗颜💧)compiler hookcompilation hook

至此,loader的执行过程和plugin的执行过程已经非常清晰,本篇文章目的也已达到,如果大家对某些hook的执行位置感兴趣或者对某些插件某些loader感兴趣,即可使用debugger根据此流程进行跟踪,从而对插件,loader的使用更加得心应手,

本篇文章示例代码github地址

如果本篇文章对你了解webpack有一定的帮助,顺便留个star ><