Webpack Tapable 源码简析

336 阅读5分钟

Introduction

Webpack作为主流的打包工具,提供了自定义扩展的loaderplugin,丰富了周边生态。如果想自己写一个plugin,需要对相关的hooks有所了解,而webpack对hooks的实现则是建立在tapable这个库上的,本篇文章希望通过对tapable源码的梳理,以加深webpack的相关知识以及部分设计模式的理解。

How to use tapable

tapable和常见发布订阅模式实现的代码用法基本一致。基本的SyncHook使用如下所示:

const { SyncHook } = require('tapable');

const hook = new SyncHook(['name']); //实例化一个hook

hook.tap('plugin1', (name) => {//注册回调
  console.log(name);
})

hook.call('xbrave'); //触发回调,打印'xbrave'

Hook

Hook类作为其它Hook类的基类,整体上把发布订阅的内容抽象了出来,整体代码不多,如下所示:

"use strict";

const util = require("util");

const deprecateContext = util.deprecate(() => {},
"Hook.context is deprecated and will be removed");

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._args = args;
		this.name = name;
		this.taps = [];
		this.interceptors = [];
		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;
		this._x = undefined;

		this.compile = this.compile;
		this.tap = this.tap;
		this.tapAsync = this.tapAsync;
		this.tapPromise = this.tapPromise;
	}

	compile(options) {
		throw new Error("Abstract: should be overridden");
	}

	_createCall(type) {
		return this.compile({
			taps: this.taps,
			interceptors: this.interceptors,
			args: this._args,
			type: type
		});
	}

	_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(options, fn) {
		this._tap("sync", options, fn);
	}

	tapAsync(options, fn) {
		this._tap("async", options, fn);
	}

	tapPromise(options, fn) {
		this._tap("promise", options, fn);
	}

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

	withOptions(options) {
		const mergeOptions = opt =>
			Object.assign({}, options, typeof opt === "string" ? { name: opt } : opt);

		return {
			name: this.name,
			tap: (opt, fn) => this.tap(mergeOptions(opt), fn),
			tapAsync: (opt, fn) => this.tapAsync(mergeOptions(opt), fn),
			tapPromise: (opt, fn) => this.tapPromise(mergeOptions(opt), fn),
			intercept: interceptor => this.intercept(interceptor),
			isUsed: () => this.isUsed(),
			withOptions: opt => this.withOptions(mergeOptions(opt))
		};
	}

	isUsed() {
		return this.taps.length > 0 || this.interceptors.length > 0;
	}

	intercept(interceptor) {
		this._resetCompilation();
		this.interceptors.push(Object.assign({}, interceptor));
		if (interceptor.register) {
			for (let i = 0; i < this.taps.length; i++) {
				this.taps[i] = interceptor.register(this.taps[i]);
			}
		}
	}

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

Object.setPrototypeOf(Hook.prototype, null);

module.exports = Hook;

init

这里需要注意的是this.call,this.callAsync等属性初始化的时候指向的是一个函数,主要是考虑到其它类继承时的初始化问题,其它类可以自行实现this.compile方法,同时compile初始化为:

	compile(options) {
		throw new Error("Abstract: should be overridden");
	}

如果子类不自行实现compile则会报错

subscribe

内部的_tap()方法主要作用是整合传入的options,然后调用内部的this._insert(options)方法,将相关回调注册到内部的this.tapsthis.insert()内部还针对options内的stagebefore做了一次排序,关于stagebefore的作用有兴趣的可以了解一下ref

HookCodeFactory

HookCodeFactory的主要作用是针对不同类型的Hook,相应的动态生成所需要的compile()函数。

setup()

注意到Hook类初始化时的_x并没有指定内容,这里对内部的_x重新赋值:

	setup(instance, options) {
		instance._x = options.taps.map(t => t.fn);
	}

new Function()

一般来说,js中创建函数一般都是函数声明和函数表达式居多,但是还有其他不太常用的方法,比如new Function(),示例如下:

const sum = new Function('a', 'b', 'return a + b');

console.log(sum(2, 6));
// expected output: 8

上述的参数'a''b'可以换为'a,b'即以英文逗号分割的字符串作为多个参数传入。以new Function()形式声明函数的好处是可以使用字符串动态创建函数,但是可读性比较差,日常开发中的代码不建议这么做。

create()

回到tapable源码中, create()方法就是用来动态生成函数的,一共有三种类型,sync,async,promise本篇文章主要关注sync类型,精简后的代码如下:

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;
		}
		this.deinit();
		return fn;
	}

可以看到create()方法最主要的还是调用new Function()来动态创建fn并返回

SyncHook

基于以上的基础,我们来看一下SyncHook是如何实现的。 SyncHook的源码比较简单,只有几十行,如下所示:

"use strict";

const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

class SyncHookCodeFactory extends HookCodeFactory {
	content({ onError, onDone, rethrowIfPossible }) {
		return this.callTapsSeries({
			onError: (i, err) => onError(err),
			onDone,
			rethrowIfPossible
		});
	}
}

const factory = new SyncHookCodeFactory();

const TAP_ASYNC = () => {
	throw new Error("tapAsync is not supported on a SyncHook");
};

const TAP_PROMISE = () => {
	throw new Error("tapPromise is not supported on a SyncHook");
};

const COMPILE = function(options) {
	factory.setup(this, options);
	return factory.create(options);
};

function SyncHook(args = [], name = undefined) {
	const hook = new Hook(args, name);
	hook.constructor = SyncHook;
	hook.tapAsync = TAP_ASYNC;
	hook.tapPromise = TAP_PROMISE;
	hook.compile = COMPILE;
	return hook;
}

SyncHook.prototype = null;

module.exports = SyncHook;

整体顺序为:

  1. SyncHookCodeFactory类继承HookCodeFactory基类,并自行实现content()方法用于动态生成代码
  2. SyncHook类继承Hook基类,并自行实现tapAsync(),tapPromise()compile()方法用于后续调用
  3. SyncHook.prototype = null;prototype指向null TODO: why?

关于订阅tap这块的内容,在之前讲Hook基类的时候描述过,这里不再赘述,这里主要关注点在如何动态生成call()方法。

content()

content()主要是生成最终函数的主要内容,相关方法及其后续调用顺序比较深,整体的调用顺序为: HookCodeFactory.create() -> HookCodeFactory.contentWithInterceptors() -> SyncHookCodeFactory.content() -> HookCodeFactory.callTapsSeries() -> HookCodeFactory.callTap() 我们重点关注callTap()"sync"部分:

case "sync":
				if (!rethrowIfPossible) {
					code += `var _hasError${tapIndex} = false;\n`;
					code += "try {\n";
				}
				if (onResult) {
					code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
						before: tap.context ? "_context" : undefined
					})});\n`;
				} else {
					code += `_fn${tapIndex}(${this.args({
						before: tap.context ? "_context" : undefined
					})});\n`;
				}
				if (!rethrowIfPossible) {
					code += "} catch(_err) {\n";
					code += `_hasError${tapIndex} = true;\n`;
					code += onError("_err");
					code += "}\n";
					code += `if(!_hasError${tapIndex}) {\n`;
				}
				if (onResult) {
					code += onResult(`_result${tapIndex}`);
				}
				if (onDone) {
					code += onDone();
				}
				if (!rethrowIfPossible) {
					code += "}\n";
				}
				break;

这里是将每个之前tap注册的函数从this._x拿出来,依次赋值给_fn并执行,生成的字符串类似于

var _fn0 = _x[0];
_fn0();
var _fn1 = _x[1];
_fn1();

至于为什么不用for循环,应该是考虑到简单可扩展

header()

回到之前的setup()部分,为什么要执行instance._x = options.taps.map(t => t.fn);,和header()放在一起看就比较明晰了, header()相关的代码如下:

	header() {
		let code = "";
		if (this.needContext()) {
			code += "var _context = {};\n";
		} else {
			code += "var _context;\n";
		}
		code += "var _x = this._x;\n";
		if (this.options.interceptors.length > 0) {
			code += "var _taps = this.taps;\n";
			code += "var _interceptors = this.interceptors;\n";
		}
		return code;
	}

可以看出来主要就是生成var _x = this._x;, 而这个_x就是callTap()中用到的_x, 最终header()content()及其相关的方法生成的结果类似如下形式:

"use strict";
var _context;
var _x = this._x;
var _fn0 = _x[0];
_fn0();
var _fn1 = _x[1];
_fn1();

再通过new Function()最终动态生成了SyncHookcall方法

Summary

  1. tapable主要基于Hook类的发布订阅模式,以及HookCodeFactory类的工厂模式来抽象代码,这两个类抽象比较高,搞懂这两个类后续就比较好理解了

  2. tapable基于new Function()来动态创建call,callAsync,promise方法

Reference

MDN Function() constructor

Query regarding coding paradigm used in this library