前端工程化3-tapable

前言

在讲webpack插件之前,我们要先讲一下tapable,tapable是webpack的核心库。我们都知道webpack的编译(Compiler)和构建(Compilation)是最核心的东西,它们都是tapable的子类。我们查看webpack的官方文档时会看到如下api

run
AsyncSeriesHook
在开始读取 records 之前调用。

回调参数:compiler

其中AsyncSeriesHook表示钩子类型,它是由tapable提供的。

tapable

在写了一系列的demo之后才慢慢对tapable有所了解。原来tapable是管理事件的类库,它能帮助我们处理异步同步、串并行等各种复杂的事件,提供了相对完善的钩子(hook)。

其中webpack就是扩展自tapable,它的工作就是管理众多插件,对外提供tap(注册)和call(调用)方法。tapable是一种事件流机制,它主要是事件的监听和触发,本质上是订阅-发布模式,包含多种不同的监听和触发事件的方式。tapable提供的钩子(hook)有以下几个

const {
  // 同步钩子
  SyncHook,
  // 多个同步钩子,可以选择某一个进行熔断(Bail)
  SyncBailHook,
  // 多个同步钩子串联叠加,下一步依赖上一步的结果,就像瀑布一样(Waterfall)
  SyncWaterfallHook,
  // 同步循环钩子,只要返回一个非undefined,就会一直循环(Loop)执行下去
  SyncLoopHook,
  // 异步钩子,多个并行(Parallel)的异步事件都执行完毕
  AsyncParallelHook,
  // 多个并行异步钩子,可以选择进行熔断(Bail),直接执行最终的回调,无论其他事件是否执行完成
  AsyncParallelBailHook,
  // 多个串行的异步钩子
  AsyncSeriesHook,
  // 多个串行的异步钩子,可选择性的进行熔断
  AsyncSeriesBailHook,
  // 多个串行执行的异步钩子,下一步的执行依赖上一步的结果
	AsyncSeriesWaterfallHook
 } = require("tapable");

tapable的使用有了基本的了解,下面我们来看一看tapable的核心源码,我们先来看一下SyncHook,代码很少,直接贴上。

/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/
"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;

精简一下,就是如下几行代码,我们发现这个钩子直接new了一个Hook的实例,然后再给它赋上几个基本的属性,tapAsyn和tapPromise直接报错,在同步钩子中不允许使用。compile也是从factory直接create一个。实现上使用了面向对象的继承和工厂模式,现在我们要看的就是Hook和HookCodeFactory这两个公共类。

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

我们再来看下其他钩子,AsyncParallelHook、AsyncParallelHook等异步钩子的核心代码都和上面类似,最重要的是Hook类和comiple方法。现在问题来了,这些钩子并没有直接对外透出tap和call方法,反倒是多了一个不知道干嘛的compile方法,所以Hook类和HookCodeFactory类是公共的、可复用的父类。

通过代码追踪我们发现,new一个钩子的时候我们会将插件注册进taps里同时会有一个用于存储拦截的interceptors,还有一个compile方法,当我们调用call的时候,我们等于调用了compile方法。compile方法的作用是找到我们对应的注册插件并执行它,执行逻辑统一抽取到了HookCodeFactory类中,而Hook类的职责只是注册插件以及对外提供调用的方法(call、tap等)。

Hook类解读

Hook.js源码地址
结构分析
看源码的时候我们要先对其进行删减,把最精华的核心代码提炼出来阅读,然后一个就是要对其函数进行分类,一类一类来看。首先先看结构,我们对代码精简一下,大概是如下结构代码。Hook类外面有3个委托函数,分别对应sync、async和promise,这3个委托方法都会调用Hook类的_createCall方法。taps属性用于存储插件、interceptors属性用于存储拦截器,compile方法是个抽象方法,需要子类进行重写。还有我们熟悉的tap、tapAsync、tapPromise方法和call方法,intercept方法用于插件的拦截。

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.taps = [];
		this.interceptors = [];

		this.call = CALL_DELEGATE;
		this.callAsync = CALL_ASYNC_DELEGATE;
		this.promise = PROMISE_DELEGATE;

		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) {
		...
	}
	tap(options, fn) {
		...
	}

	tapAsync(options, fn) {
		...
	}

	tapPromise(options, fn) {
		...
	}
	intercept(interceptor) {
		...
	}
}

_insert方法是Hook类的核心方法,它的主要作用是根据before和stage的值做顺序调整来插入到在taps数组中,我们从源代码可以看到在调用_insert方法之前,会注册一层拦截器,在调用call之前执行。从整体上来看,Hook基类的主要作用就是options做一些调整然后插入到taps中,并且注册拦截器等。

HookCodeFactory类解读

HookCodeFactory源码
看HookCodeFactory的源码也是遵循上面的原则,先看结构,对该类进行拆解。通过观察我们发现有以下几个方法:create、setup、contentWithInterceptors、callTap、callTapsSeries、callTapsLooping、callTapsParallel等。
HookCodeFactory主要是用在在compile方法中,我们可以看到compile方法都是一下两行,又由上面我们的推导,compile方法主要是执行拦截器和call。

	factory.setup(this, options);
	return factory.create(options);
  • setup方法
	instance._x = options.taps.map(t => t.fn);

我们看到setup只有一行代码,很简单,这一行代码就是将taps里的每一个fn方法收集到_x里,方便后面的操作。

  • create方法我们精简一下
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":
				...
				break;
			case "promise":
				...
				break;
		}
		this.deinit();
		return fn;
	}

以上代码我们可以看到基本的结构,通过switch...case...来分别执行tap、tapAsync、tapPromise方法。我们还可以看到是通过new Function来生成一个函数。header方法主要是定义一些变量,contentWithInterceptors方法来执行call方法,在这个方法里我们可以看到还有一个content方法的调用,这是一个子类自定义的方法。我们可以看看SyncHook.js

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

content方法是调用了HookCodeFactory基类的callTapsSeries方法,这就完美的呼应上了。tapable的钩子主要逻辑代码都封装在了HookCodeFactory类中,callTap、callTapsSeries、callTapsLooping、callTapsParallel这几个方法完全涵盖了对外透出的所有钩子的调用逻辑

参考

posted @ 2021-08-26 10:37  mingL  阅读(134)  评论(0编辑  收藏  举报