0x03 webpack源码分析之compiler的构建

前言

上一章分析了启动代码,并且在webpack-cli/cli.js里下了几处断点。今天从compiler = webpack(options)跳入,继续跟踪分析。

try {
	compiler = webpack(options);// break here
} catch (err) {
	.............
}

webpack函数

const webpack = (options, callback) => {
	//验证option
	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") {// 但配置情况,此次分析的入口
		// 生成webpack默认参数
		options = new WebpackOptionsDefaulter().process(options);
		// 构建compiler,从这里跳入,继续跟踪分析
		compiler = new Compiler(options.context);
		compiler.options = options;
		// 加入一个plugin
		new NodeEnvironmentPlugin().apply(compiler);
		// 安装所有的plugins
		if (options.plugins && Array.isArray(options.plugins)) {
			for (const plugin of options.plugins) {
				if (typeof plugin === "function") {
					plugin.apply(compiler);
				} else {
					plugin.apply(compiler);
				}
			}
		}
		// 触发几个生命周期钩子,一些plugin会起作用
		compiler.hooks.environment.call();
		compiler.hooks.afterEnvironment.call();
		compiler.options = new WebpackOptionsApply().process(options, compiler);
	} else {
		throw new Error("Invalid argument: options");
	}
	// compiler = webpack(options); callback为undefine,这里没执行 暂时不管
	if (callback) {
		..............
	}
	return compiler;
};

代码很短,也很简单,validateSchema(验证)和WebpackOptionsDefaulter(默认配置)我就不分析了,因为我主要关注编译过程。对compiler = new Compiler(options.context);这一行下断点,f7跳入分析Compiler的构造函数。

Compiler.constructor

constructor(compiler) {
		super();
		// compiler的生命周期,重点分析一下
		this.hooks = {
			/** @type {SyncHook} */
			buildModule: new SyncHook(["module"]),
			/** @type {SyncHook} */
			rebuildModule: new SyncHook(["module"]),
			/** @type {SyncHook} */
			failedModule: new SyncHook(["module", "error"]),
			/** @type {SyncHook} */
			succeedModule: new SyncHook(["module"]),

			...............省略一些hook,实在是太多了
		};
		// 一些属性,暂时不做分析
		/** @type {string=} */
		this.name = undefined;
		/** @type {Compiler} */
		this.compiler = compiler;
		this.resolverFactory = compiler.resolverFactory;
		this.inputFileSystem = compiler.inputFileSystem;
		this.requestShortener = compiler.requestShortener;

		const options = compiler.options;
		this.options = options;
		this.outputOptions = options && options.output;
		
		..........
		全是一些属性,暂时不做分析
	}

this.hooks里包含了compiler所有的生命周期(类似于vue的生命周期,但compiler的生命周期是真的复杂),这里用到了SyncHook等几个类,下面就说一下这几个Hook类。

tapable

tapable这个包提供了几个Hook类,能够用来给plugin创建hook点。这里提前说一下,webpack的plugin的工作方式就是对compiler的生命周期进行hook,在某个编译阶段对源码进行一系列的处理。compiler本身只是一个架子,一个状态机而已。compiler.run()就会推动生命周期往下走,而具体的编译工作都由各种plugin实现。这就是为什么webpack有那么多的plugin,让人头皮发麻。webpack4.0采用了大量的默认配置,做到了开箱即用,简单了不少。回到这一节的内容,tapable的文档已经写的很明白了,大家可以去github上看,这里做一个简单的解释。tapable的源码分析这里就不去写了,因为和webpack的编译流程关系不大。
直接上demo代码和注释

const myHook = new SyncHook();// 创建一个hook对象
// 添加一个监听,第一个参数是一个随意的名字用来标识,第二个参数是该监听的回调函数
myHook.tap('helloPlugin', () => {console.log('hello world')})
myHook.tap('helloPlugin2', () => {console.log('hello world2')})
// 触发hook,所有的监听函数被执行
myHook.call()

加上参数

const myHook = new SyncHook(['message']);// 创建一个hook对象
// 添加一个监听,第一个参数是一个随意的名字用来标识,第二个参数是该监听的回调函数
myHook.tap('echoPlugin', (message) => {console.log(message)})
myHook.tap('echoPlugin2', (message) => {console.log(message + ' twice')})
// 触发hook,所有的监听函数被执行
myHook.call('hello world')

上面两个都是同步的Hook,回调函数里都是同步执行的代码,下面再来看一下异步Hook

const findRoute = new AsyncParallelHook(["source", "target", "routesList"]);// 创建一个异步hook对象
// 添加一个监听,第一个参数是一个随意的名字用来标识,第二个参数是该监听的回调函数
findRoute.tapPromise("GoogleMapsPlugin", (source, target, routesList) => {
	// 返回一个Promise
	return google.maps.findRoute(source, target).then(route => {
		routesList.add(route);
	});
});
// 触发hook,所有的监听函数被执行
findRoute.promise(source, target, routesList).then(() => {
	// do something
})

不使用Promise的异步Hook,使用callback模式

const findRoute = new AsyncParallelHook(["source", "target", "routesList"]);// 创建一个异步hook对象
// 添加一个监听,第一个参数是一个随意的名字用来标识,第二个参数是该监听的回调函数,回调函数里最后加上一个callback参数,用于异步操作完成后执行回调
findRoute.tapAsync("GoogleMapsPlugin", (source, target, routesList, callback) => {
	bing.findRoute(source, target, (err, route) => {
		if(err) return callback(err);
		routesList.add(route);
		// 找到路径后执行callback
		callback();
	});
});
// 触发hook,所有的监听函数被执行,最后一个参数传入回调函数,用于被各个异步的监听函数回调
findRoute.callAsync(source, target, routesList,(err) => {
	// do some thing
})

所以在webpack的plugin里,主要就是

apply (compiler) {// plugin的入口函数
	// 对compiler的生命周期进行监听
	compiler.hooks.xxxx1.tap('xxPlugin',  (compilation) => {
	})
	compiler.hooks.xxxx2.tap('xxPlugin',  (compilation) => {
	})
}

在compiler.run里主要就是

	// 触发监听函数
	compiler.hooks.xxx1.call(compilation)
	compiler.hooks.xxx2.call(compilation)

总结

这么一看,编译功能的实现都在各个plugin里,我猜loader的调用,依赖图的构建,抽象语法树的生成等,搞不好都是在某几个plugin里,下面的工作就是把几个关键的plugin找出来,进行分析。

你可能感兴趣的:(nodejs,webpack,源码)