Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack - tapable #14

Open
Hazlank opened this issue Apr 1, 2022 · 0 comments
Open

webpack - tapable #14

Hazlank opened this issue Apr 1, 2022 · 0 comments
Labels

Comments

@Hazlank
Copy link
Owner

Hazlank commented Apr 1, 2022

Tapable

以下代码来自V2.2.1

Tapable是webpack最重要的库了,如果你需要阅读源码,需要写一个webpack plugin,那么最好先了解Tapable是什么

Tapable其实就是一个EventEmitter库,但它相比普通的EventEmitter,它还支持异步的Event,这样就可以支持异步插件。

const {
	SyncHook,
	SyncBailHook,
	SyncWaterfallHook,
	SyncLoopHook,
	AsyncParallelHook,
	AsyncParallelBailHook,
	AsyncSeriesHook,
	AsyncSeriesBailHook,
	AsyncSeriesWaterfallHook
 } = Tapable

上面是来自Tapable的库导出的函数方法,除了Sync的方法,还有Async。

SyncHook

const hook = new SyncHook(["arg1", "arg2"]);
hook.tap('LoggerPlugin', (arg1, arg2) => { console.log(arg1 , arg2) })
hook.tap('SumPlugin', (arg1, arg2) => { console.log(arg1 + arg2) })

hook.call(1,2)
// 1, 2
// 3

同步的钩子很简单,调用call会执行执行所有被tapped的钩子。
同步方法除了普通的钩子还有其他的,比如:

  • SyncWaterfallHook WaterfallHook可以将上一个回调返回值传入下一个钩子
  • SyncBailHook 如果上一个回调有返回值,BailHook可以立即退出不再执行后面的钩子
  • SyncLoopHook SyncLoopHook在回调有返回值的时候会从第一个钩子重新开始执行,一直循环到所有钩子都返回undefined为止

AsyncHook

异步的钩子和同步钩子差异的地方在于有两个不同的关键字眼,ParalleSeries。一个可以并行执行钩子,一个可以按顺序执行钩子。

const asyncHook = new AsyncParallelHook(["arg"])
asyncHook.tapAsync('SetTimeoutPlugin', (arg, cb) => {
	setTimeout(() => {
		console.log('macrotask runed')
		cb()
	}, 2000)
})

asyncHook.tapPromise('PromisePlugin', (arg, cb) => {
	//return promise
	return new Promise((resolve) => {
	    setTimeout(() => {
	        console.log('microtask runed');
	        resolve();
	      }, 1000);
	})
})

asyncHook.callAsync("arg", () => {
	console.log("task clearout")
})
// microtask runed
// macrotask runed
// task clearout

可以看到有两种注入异步钩子的方式,一种需要调用回调参数cb,一种需要返回Promise的形式。

上面的AsyncParallelHook以一种并行的方式执行,所以执行顺序跟着回调走.另外一种AsyncSeriesHook根据注册顺序走,只有前面的钩子执行回调函数才会继续下一个钩子,也就是说// macrotask runed会被先打印

Interception和context

hooks还提供了Interception,可以拦截hooks的行为。Context能做为上下文在Interception中传递

asyncHook.intercept({
	call: (...arg) => {
		console.log("Starting asyncHook");
	},
	loop: (...args) => {
		console.log('restart looping')
	},
	tap: (tapInfo) => {
		console.log('new hooks be tapped')
	}
	register: (tapInfo) => {
		// tapInfo = { type: "promise", name: "PromisePlugin", fn: ... }
		console.log(`${tapInfo.name} is doing its job`);
		return tapInfo; // may return a new tapInfo object
	}
})

//Context

hooks.intercept({
	context: true,
	tap: (context, tapInfo) => {
		// tapInfo = { type: "sync", name: "NoisePlugin", fn: ... }
		console.log(`${tapInfo.name} is doing it's job`);

		// `context` starts as an empty object if at least one plugin uses `context: true`.
		// If no plugins use `context: true`, then `context` is undefined.
		if (context) {
			// Arbitrary properties can be added to `context`, which plugins can then access.
			context.hasMuffler = true;
		}
	}
});

hooks.tap({
	name: "NoisePlugin",
	context: true
}, (context, newSpeed) => {
	if (context && context.hasMuffler) {
		console.log("Silence...");
	} else {
		console.log("Vroom!");
	}
});

HookMap和MultiHook

Tapable还提供了其他辅助方法

//HookMap
const { HookMap } = require("tapable");
const keyedHook = new HookMap(key => new SyncHook(["arg"]))
keyedHook.for("some-key").tap("MyPlugin", (arg) => { /* ... */ });
keyedHook.for("some-key").tapAsync("MyPlugin", (arg, callback) => { /* ... */ });
keyedHook.for("some-key").tapPromise("MyPlugin", (arg) => { /* ... */ });
const hook = keyedHook.get("some-key");
if(hook !== undefined) {
	hook.callAsync("arg", err => { /* ... */ });
}

//MultiHook
const { MultiHook } = require("tapable");

this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);

源码解析

Hook

https://github.com/webpack/tapable/blob/master/lib/Hook.js

hooks文件实现了需对对外暴露的方法,比如tap,tapAsync,tapAsync,还有一些处理intercept的方法。

_tap(type, options, fn) {
	if (typeof options === "string") {
		options = {
			name: options.trim()
		};
	} else if (typeof options !== "object" || options === null) {
		throw new Error("Invalid tap options");
	}
	if (typeof options.name !== "string" || options.name === "") {
		throw new Error("Missing name for tap");
	}
	if (typeof options.context !== "undefined") {
		deprecateContext();
	}
	options = Object.assign({ type, fn }, options);
	options = this._runRegisterInterceptors(options);
	this._insert(options);
}

所有的tap都会调用_tap函数。首先处理options,然后调用_runRegisterInterceptors_insert

const CALL_DELEGATE = function(...args) {
	this.call = this._createCall("sync");
	return this.call(...args);
};
const CALL_ASYNC_DELEGATE = function(...args) {
	this.callAsync = this._createCall("async");
	return this.callAsync(...args);
};
const PROMISE_DELEGATE = function(...args) {
	this.promise = this._createCall("promise");
	return this.promise(...args);
};

class Hook {
	constructor(args = [], name = undefined) {
		this._call = CALL_DELEGATE;
		this.call = CALL_DELEGATE;
		this._callAsync = CALL_ASYNC_DELEGATE;
		this.callAsync = CALL_ASYNC_DELEGATE;
		this._promise = PROMISE_DELEGATE;
		this.promise = PROMISE_DELEGATE;
	}
}

hooks里的调用都会有一个副本,先说明这是因为暴露给用户的函数是通过new Function生成使用的,所以需要保存生产函数的方法,下次再重新赋值

_runRegisterInterceptors(options) {
	for (const interceptor of this.interceptors) {
		if (interceptor.register) {
			const newOptions = interceptor.register(options);
			if (newOptions !== undefined) {
				options = newOptions;
			}
		}
	}
	return options;
}

_resetCompilation() {
	this.call = this._call;
	this.callAsync = this._callAsync;
	this.promise = this._promise;
}


_insert(item) {
	this._resetCompilation();
	let before;
	if (typeof item.before === "string") {
		before = new Set([item.before]);
	} else if (Array.isArray(item.before)) {
		before = new Set(item.before);
	}
	let stage = 0;
	if (typeof item.stage === "number") {
		stage = item.stage;
	}
	let i = this.taps.length;
	while (i > 0) {
		i--;
		const x = this.taps[i];
		this.taps[i + 1] = x;
		const xStage = x.stage || 0;
		if (before) {
			if (before.has(x.name)) {
				before.delete(x.name);
				continue;
			}
			if (before.size > 0) {
				continue;
			}
		}
		if (xStage > stage) {
			continue;
		}
		i++;
		break;
	}
	this.taps[i] = item;
}

_runRegisterInterceptors实现了interceptor.register的功能,用于返回新的options。insert第一行会运行_resetCompilation去重新赋值,前面提前说明了callcallAsyncpromise的方法是生成出来的,所以每次tap的时候都需要重新去生成函数。

SyncHook和AsyncParallelHook

https://github.com/webpack/tapable/blob/master/lib/SyncHook.js

https://github.com/webpack/tapable/blob/master/lib/AsyncParallelHook.js
每个hooks的代码都大同小异,它们都extends HookCodeFactory ,并且返回实例化的Hooks。

HookCodeFactory

https://github.com/webpack/tapable/blob/master/lib/HookCodeFactory.js

所有的hooks都扩展于HookCodeFactory ,当hooks.call执行时,会调用HookCodeFactory.prototype.create方法通过new Function生成调用函数。

class HookCodeFactory {
	constructor(config) {
		this.config = config;
		this.options = undefined;
		this._args = undefined;
	}

	create(options) {
		this.init(options);
		let fn;
		switch (this.options.type) {
			case "sync":
				fn = new Function(
					this.args(),
					'"use strict";\n' +
						this.header() +
						this.contentWithInterceptors({
							onError: err => `throw ${err};\n`,
							onResult: result => `return ${result};\n`,
							resultReturns: true,
							onDone: () => "",
							rethrowIfPossible: true
						})
				);
				break;
			case "async":
				fn = new Function(
					this.args({
						after: "_callback"
					}),
					'"use strict";\n' +
						this.header() +
						this.contentWithInterceptors({
							onError: err => `_callback(${err});\n`,
							onResult: result => `_callback(null, ${result});\n`,
							onDone: () => "_callback();\n"
						})
				);
				break;
			case "promise":
				let errorHelperUsed = false;
				const content = this.contentWithInterceptors({
					onError: err => {
						errorHelperUsed = true;
						return `_error(${err});\n`;
					},
					onResult: result => `_resolve(${result});\n`,
					onDone: () => "_resolve();\n"
				});
				let code = "";
				code += '"use strict";\n';
				code += this.header();
				code += "return new Promise((function(_resolve, _reject) {\n";
				if (errorHelperUsed) {
					code += "var _sync = true;\n";
					code += "function _error(_err) {\n";
					code += "if(_sync)\n";
					code +=
						"_resolve(Promise.resolve().then((function() { throw _err; })));\n";
					code += "else\n";
					code += "_reject(_err);\n";
					code += "};\n";
				}
				code += content;
				if (errorHelperUsed) {
					code += "_sync = false;\n";
				}
				code += "}));\n";
				fn = new Function(this.args(), code);
				break;
		}
		this.deinit();
		return fn;
	}

call会调用compile生成函数并保存起来,只有intercept和tap才会重新去编译。

const hook = new SyncHook(["arg1", "arg2"]);
hook.tap('LoggerPlugin', (arg1, arg2) => { console.log(arg1 , arg2) })
hook.tap('SumPlugin', (arg1, arg2) => { console.log(arg1 + arg2) })
hook.intercept({call: ()=> {}})
hook.call(1,2)

// compile后的匿名函数
ƒ anonymous(arg1, arg2) {
	  "use strict";
	  var _context;
	  var _x = this._x;
	  var _taps = this.taps;
	  var _interceptors = this.interceptors;
	  _interceptors[0].call(arg1, arg2); //调用拦截器
	  var _fn0 = _x[0];
	  _fn0(arg1, arg2);  //LoggerPlugin callback
	  var _fn1 = _x[1];  //SumPlugin callback
	  _fn1(arg1, arg2);
}

通过字符拼接和new Function,可以创造出异步或者同步代码,并且还能够插入拦截代码。

结尾

Tapable是webpack比较重要的库,基本大部分重要的模块都有它,并需要依赖它来与其他功能模块通信,来完成webpack构建。理解Tapable能够让你更容易去阅读webpack源码或者你只是想写一个Plugin,比如应该在compiler的哪个钩子阶段才能拿到编译产生的compiltion,应该以怎样的方式注入一个钩子进去修改构建产物的信息等等。这种解耦的方式能够很好帮助webpack扩展功能,但是因为源码实现问题,导致async hook不能被await,会产生callback hell,让阅读变得困难

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant