【前端必会】tapable、hook,webpack的灵魂

背景

  1. 什么是tapable、hook,平时做vue开发时的webpack 配置一直都没弄懂,你也有这种情况吗?
  2. 还是看源码,闲来无聊又看一下webpack的源码,看看能否找到一些宝藏
  3. tapable和webpack没有特定关系,可以先看下这篇文章,了解下这个小型库
    https://webpack.docschina.org/api/plugins/#tapable
    https://blog.csdn.net/mafan121/article/details/113120081
    4.下面记录下寻宝过程

开始

执行一次webpack经历了什么,先看一下代码
【前端必会】tapable、hook,webpack的灵魂

我们分析一下4点

  1. 引用了webpack
  2. 我们使用的配置文件
  3. 调用webpack函数,传入配置,返回一个compiler(编译器)
  4. 执行编译器的run方法

分析

引用webpack,先把这个函数找出来
https://github.com/webpack/webpack/blob/main/package.json
"main": "lib/index.js",

https://github.com/webpack/webpack/blob/main/lib/index.js

module.exports = mergeExports(fn, { 	get webpack() { 		return require("./webpack"); 	}, 

https://github.com/webpack/webpack/blob/main/lib/webpack.js

const webpack = /** @type {WebpackFunctionSingle & WebpackFunctionMulti} */ ( 	/** 	 * @param {WebpackOptions | (ReadonlyArray<WebpackOptions> & MultiCompilerOptions)} options options 	 * @param {Callback<Stats> & Callback<MultiStats>=} callback callback 	 * @returns {Compiler | MultiCompiler} 	 */ 	(options, callback) => { 		const create = () => { 			if (!asArray(options).every(webpackOptionsSchemaCheck)) { 				getValidateSchema()(webpackOptionsSchema, options); 				util.deprecate( 					() => {}, 					"webpack bug: Pre-compiled schema reports error while real schema is happy. This has performance drawbacks.", 					"DEP_WEBPACK_PRE_COMPILED_SCHEMA_INVALID" 				)(); 			} 			/** @type {MultiCompiler|Compiler} */ 			let compiler; 			let watch = false; 			/** @type {WatchOptions|WatchOptions[]} */ 			let watchOptions; 			if (Array.isArray(options)) { 				/** @type {MultiCompiler} */ 				compiler = createMultiCompiler( 					options, 					/** @type {MultiCompilerOptions} */ (options) 				); 				watch = options.some(options => options.watch); 				watchOptions = options.map(options => options.watchOptions || {}); 			} else { 				const webpackOptions = /** @type {WebpackOptions} */ (options); 				/** @type {Compiler} */ 				compiler = createCompiler(webpackOptions); 				watch = webpackOptions.watch; 				watchOptions = webpackOptions.watchOptions || {}; 			} 			return { compiler, watch, watchOptions }; 		}; 		if (callback) { 			try { 				const { compiler, watch, watchOptions } = create(); 				if (watch) { 					compiler.watch(watchOptions, callback); 				} else { 					compiler.run((err, stats) => { 						compiler.close(err2 => { 							callback(err || err2, stats); 						}); 					}); 				} 				return compiler; 			} catch (err) { 				process.nextTick(() => callback(err)); 				return null; 			} 		} else { 			const { compiler, watch } = create(); 			if (watch) { 				util.deprecate( 					() => {}, 					"A 'callback' argument needs to be provided to the 'webpack(options, callback)' function when the 'watch' option is set. There is no way to handle the 'watch' option without a callback.", 					"DEP_WEBPACK_WATCH_WITHOUT_CALLBACK" 				)(); 			} 			return compiler; 		} 	} );  module.exports = webpack; 

这里主要就是调用create创建一个Compiler(先不理watch)
【前端必会】tapable、hook,webpack的灵魂

在看一下create,这里是调用的createCompiler或者createMultiCompiler
【前端必会】tapable、hook,webpack的灵魂

在看一下createCompiler,这里主要就是new一个Compiler。这个时候已经开始了webpack编译的生命周期。

/**  * @param {WebpackOptions} rawOptions options object  * @returns {Compiler} a compiler  */ const createCompiler = rawOptions => { 	const options = getNormalizedWebpackOptions(rawOptions); 	applyWebpackOptionsBaseDefaults(options); 	const compiler = new Compiler(options.context, options); 	new NodeEnvironmentPlugin({ 		infrastructureLogging: options.infrastructureLogging 	}).apply(compiler); 	if (Array.isArray(options.plugins)) { 		for (const plugin of options.plugins) { 			if (typeof plugin === "function") { 				plugin.call(compiler, compiler); 			} else { 				plugin.apply(compiler); 			} 		} 	} 	applyWebpackOptionsDefaults(options); 	compiler.hooks.environment.call(); 	compiler.hooks.afterEnvironment.call(); 	new WebpackOptionsApply().process(options, compiler); 	compiler.hooks.initialize.call(); 	return compiler; }; 

我们简单看一下Compiler类的一些hooks

const { 	SyncHook, 	SyncBailHook, 	AsyncParallelHook, 	AsyncSeriesHook } = require("tapable");  ......  class Compiler { 	/** 	 * @param {string} context the compilation path 	 * @param {WebpackOptions} options options 	 */ 	constructor(context, options = /** @type {WebpackOptions} */ ({})) { 		this.hooks = Object.freeze({ 			/** @type {SyncHook<[]>} */ 			initialize: new SyncHook([]),  			/** @type {SyncBailHook<[Compilation], boolean>} */ 			shouldEmit: new SyncBailHook(["compilation"]), 			/** @type {AsyncSeriesHook<[Stats]>} */ 			done: new AsyncSeriesHook(["stats"]), 			/** @type {SyncHook<[Stats]>} */ 			afterDone: new SyncHook(["stats"]), 			/** @type {AsyncSeriesHook<[]>} */ 			additionalPass: new AsyncSeriesHook([]), 			/** @type {AsyncSeriesHook<[Compiler]>} */ 			beforeRun: new AsyncSeriesHook(["compiler"]), 			/** @type {AsyncSeriesHook<[Compiler]>} */ 			run: new AsyncSeriesHook(["compiler"]), 			/** @type {AsyncSeriesHook<[Compilation]>} */ 			emit: new AsyncSeriesHook(["compilation"]), 			/** @type {AsyncSeriesHook<[string, AssetEmittedInfo]>} */ 			assetEmitted: new AsyncSeriesHook(["file", "info"]), 			/** @type {AsyncSeriesHook<[Compilation]>} */ 			afterEmit: new AsyncSeriesHook(["compilation"]), 

每个hook都是一个 tapable包里对应后hook的实例

在回到创建编译器那里,这时创建一个插件的实例,并且执行apply方法,插件就会向自己关系的hook添加事件处理函数(其实还是一个事件监听),NodeEnvironmentPlugin代码可以自行在源码中查看

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

一切都准备好了之后,我们再看一下编译器的run方法

/** 	 * @param {Callback<Stats>} callback signals when the call finishes 	 * @returns {void} 	 */ 	run(callback) { 		if (this.running) { 			return callback(new ConcurrentCompilationError()); 		}  		let logger;  		const finalCallback = (err, stats) => { 			if (logger) logger.time("beginIdle"); 			this.idle = true; 			this.cache.beginIdle(); 			this.idle = true; 			if (logger) logger.timeEnd("beginIdle"); 			this.running = false; 			if (err) { 				this.hooks.failed.call(err); 			} 			if (callback !== undefined) callback(err, stats); 			this.hooks.afterDone.call(stats); 		};  		const startTime = Date.now();  		this.running = true;  		const onCompiled = (err, compilation) => { 			if (err) return finalCallback(err);  			if (this.hooks.shouldEmit.call(compilation) === false) { 				compilation.startTime = startTime; 				compilation.endTime = Date.now(); 				const stats = new Stats(compilation); 				this.hooks.done.callAsync(stats, err => { 					if (err) return finalCallback(err); 					return finalCallback(null, stats); 				}); 				return; 			}  			process.nextTick(() => { 				logger = compilation.getLogger("webpack.Compiler"); 				logger.time("emitAssets"); 				this.emitAssets(compilation, err => { 					logger.timeEnd("emitAssets"); 					if (err) return finalCallback(err);  					if (compilation.hooks.needAdditionalPass.call()) { 						compilation.needAdditionalPass = true;  						compilation.startTime = startTime; 						compilation.endTime = Date.now(); 						logger.time("done hook"); 						const stats = new Stats(compilation); 						this.hooks.done.callAsync(stats, err => { 							logger.timeEnd("done hook"); 							if (err) return finalCallback(err);  							this.hooks.additionalPass.callAsync(err => { 								if (err) return finalCallback(err); 								this.compile(onCompiled); 							}); 						}); 						return; 					}  					logger.time("emitRecords"); 					this.emitRecords(err => { 						logger.timeEnd("emitRecords"); 						if (err) return finalCallback(err);  						compilation.startTime = startTime; 						compilation.endTime = Date.now(); 						logger.time("done hook"); 						const stats = new Stats(compilation); 						this.hooks.done.callAsync(stats, err => { 							logger.timeEnd("done hook"); 							if (err) return finalCallback(err); 							this.cache.storeBuildDependencies( 								compilation.buildDependencies, 								err => { 									if (err) return finalCallback(err); 									return finalCallback(null, stats); 								} 							); 						}); 					}); 				}); 			}); 		};  		const 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); 					}); 				}); 			}); 		};  		if (this.idle) { 			this.cache.endIdle(err => { 				if (err) return finalCallback(err);  				this.idle = false; 				run(); 			}); 		} else { 			run(); 		} 	} 

https://github.com/webpack/webpack/blob/main/lib/Compiler.js

这里简单分析一下,主要就是执行run相关生命周期,以及编译。并且编译完成后传入回调函数onCompiled

const 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启动后对hook的一些使用方式。整体的逻辑差不多都是一样的。是不是很简单。

总结

  1. 想了解一个框架,一定要找到入口函数,一点一点向前探索。
  2. tapable 是个好东西,
  3. 关于webpack生命周期,有疑问的时候,除了看文档意外,还可以结合源码去理解,去感受

你学会了吗?欢迎留下你的感受!

发表评论

相关文章