• 【前端必会】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经历了什么,先看一下代码

    我们分析一下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 & MultiCompilerOptions)} options options
    	 * @param {Callback & Callback=} 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)

    在看一下create,这里是调用的createCompiler或者createMultiCompiler

    在看一下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} 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生命周期,有疑问的时候,除了看文档意外,还可以结合源码去理解,去感受

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

  • 相关阅读:
    Java 通过反射修改字符串 String 类型变量的取值而不改变字符串变量的指向
    目标检测论文解读复现之二十:基于改进Yolov5的地铁隧道附属设施与衬砌表观病害检测方法
    【离别信】对小伙伴的留言
    java变量-未分类
    《中国棒球》:MLB谱写中国棒球·点燃全民运动
    Springboot整合阿里云OSS进行上传单个图片,多个图片,删除图片功能
    YOLOv7中的数据集处理【代码分析】
    qmake language ~= 字符串替换操作 正则表达式
    SpringCloud Feign异步调用传参问题
    Haskell Stack编译cannot encode character ‘8226‘ 错误 (Win10)
  • 原文地址:https://www.cnblogs.com/lee35/p/16743594.html