Tapable v1.1文件翻譯+簡單解釋

zzszzs發表於2019-03-28
Tapable是一個為外掛創造鉤子的庫,他也是webpack的核心庫。Tapable v1之後的版本跟之前的用法差別非常大,主要區別是以前用繼承class A extends tapable, 現在直接在類裡面定義私有成員this.hooks. 貌似網上很多都是老版本的用法,鑑於馬上就要v2了,翻譯走一波,順便點一下目前1.1版本的坑
原文 
github.com/webpack/tap…

Tapable

tapable提供很多鉤子類(Hook classes),他們可以被用來為外掛創造鉤子。

const {
	SyncHook,                    // 同步鉤子
	SyncBailHook,                // 同步早退鉤子
	SyncWaterfallHook,           // 同步瀑布鉤子
	SyncLoopHook,                // 同步迴圈鉤子
	AsyncParallelHook,           // 非同步併發鉤子
	AsyncParallelBailHook,       // 非同步併發可早退鉤子
	AsyncSeriesHook,             // 非同步順序鉤子
	AsyncSeriesBailHook,         // 非同步順序可早退鉤子
	AsyncSeriesWaterfallHook     // 非同步順序瀑布鉤子
 } = require("tapable");複製程式碼

安裝

npm install --save tapable複製程式碼

使用

所有的鉤子類的構造器都接受一個可選引數,它是一個 這個鉤子所接受引數的引數名陣列。

const hook = new SyncHook(["arg1", "arg2", "arg3"]);複製程式碼

最佳做法是一次性在hooks屬性裡面定義好所用的鉤子:

class Car {
	constructor() {
		this.hooks = {
                        // 以下分別是油門,剎車,計算路線鉤子
			accelerate: new SyncHook(["newSpeed"]),
			brake: new SyncHook(),
			calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
		};
	}

	/* ... */
}複製程式碼

其他人現在就能使用以上的鉤子了:

const myCar = new Car();

// 使用tap方法新增具體的執行邏輯
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on()); // 亮燈外掛,邏輯為剎車時亮燈複製程式碼

為了定位你的外掛,一個合適的名字(上面WarningLampPlugin)是必須的。

你定義的函式可以接收引數

myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));複製程式碼

對於同步的鉤子,tap是僅有的新增外掛的有效方法。非同步鉤子還支援非同步外掛,除了tap外,還有tapPromise,tapAsync等方法。

myCar.hooks.calculateRoutes.tapPromise("GoogleMapsPlugin", (source, target, routesList) => {
	// 谷歌的找路線的非同步方法返回promise
	return google.maps.findRoute(source, target).then(route => {
		routesList.add(route);
	});
});

myCar.hooks.calculateRoutes.tapAsync("BingMapsPlugin", (source, target, routesList, callback) => {
        // bing的找路線非同步方法用的callback方式
	bing.findRoute(source, target, (err, route) => {
		if(err) return callback(err);
		routesList.add(route);
		// call the callback
		callback();
	});
});

// 非同步鉤子也可以使用同步方法,比如下例取出快取的版本
myCar.hooks.calculateRoutes.tap("CachedRoutesPlugin", (source, target, routesList) => {
	const cachedRoute = cache.get(source, target);
	if(cachedRoute)
		routesList.add(cachedRoute);
})複製程式碼

然後宣告瞭這些鉤子的類需要用他們時:

class Car {
	/* 我作為一輛車,我只在乎我有以下功能,但這些功能的具體實現交給了第三方,
         * 我給這些第三方提供能修改邏輯的許可權就好了 */

	setSpeed(newSpeed) {
                // 下面的call沒有返回值
		this.hooks.accelerate.call(newSpeed);
	}

	useNavigationSystemPromise(source, target) {
		const routesList = new List();
		return this.hooks.calculateRoutes.promise(source, target, routesList).then((res) => {
		    // res是undefined	
                    return routesList.getRoutes();
		});
	}

	useNavigationSystemAsync(source, target, callback) {
		const routesList = new List();
		this.hooks.calculateRoutes.callAsync(source, target, routesList, err => {
			if(err) return callback(err);
			callback(null, routesList.getRoutes());
		});
	}
}複製程式碼

:此處例子用的是SyncHook和AsyncParallelHook, 所以他們是沒有返回值的,即使你返回了也只能得到undefined。要想得到返回值請用SyncWaterfallHook和AsyncSeriesWaterfallHook!而且注意waterfall鉤子總會返回值(即使你不return))

我們會用最高效的方式編譯一個執行你提供的外掛的方法,生成的程式碼取決於:

  • 註冊外掛的數量(0,1,多個)
  • 註冊外掛的型別(同步,非同步回撥,非同步promise)
  • 使用的呼叫方法(call, promise,callAsync)
  • 引數的數量
  • 是否用攔截器
這點特性保證了最快的執行。

鉤子型別

每個鉤子可以關聯多個函式,它們怎麼執行取決於鉤子型別:
  • 基礎鉤子(名字裡沒有waterfall, bail, loop的):這種鉤子簡單地按順序呼叫每個新增的函式。
  • 瀑布鉤子(waterfall):也會按順序呼叫函式,不同的是,他會傳遞每個函式的返回值到下一個函式。如果你不顯式地return值,那麼函式會返回你的第一個引數當返回值,所以記得總要返回一個值(我會return 'defined';)!
  • 早退鉤子(bail):當有任何新增的函式返回了任何值,這種鉤子就會停止執行後面的函式。
  • 迴圈鉤子(loop):還在開發中...
另外鉤子還分為同步或非同步:
  • 同步(sync):同步鉤子只能新增同步函式(使用myHook.tap())
  • 非同步序列(AsyncSeries):可以新增同步方法,基於回撥的非同步方法,基於promise的非同步方法(使用.tap(), .tapAsync(), .tapPromise())。按出現的順序呼叫新增的非同步方法。
  • 非同步平行(AsyncParallel):跟上面一樣,只不過併發的呼叫新增的非同步方法。
你可以通過這些鉤子類的名字判斷他們的模型, 比如AsyncSeriesWaterfallHook代表按順序執行非同步方法,並且按順序傳遞返回值。

攔截器

所有的鉤子都提供攔截器介面:
myCar.hooks.calculateRoutes.intercept({
	call: (source, target, routesList) => {
		console.log("Starting to calculate routes");
	},
	register: (tapInfo) => {
		// tapInfo = { type: "promise", name: "GoogleMapsPlugin", fn: ... }
		console.log(`${tapInfo.name} is doing its job`);
		return tapInfo; // may return a new tapInfo object
	}
})複製程式碼
call:(...args) => void 當你的鉤子被觸發時,攔截器裡面的call方法就被觸發,此時你可以訪問到鉤子的引數。
tap:(tap: Tap) => void 當你的自定義外掛被新增進鉤子時觸發,此時你可以訪問這個tap物件,但只讀。
register: (tap: Tap) => Tap | undefined 當你的自定義外掛被新增進鉤子時觸發,此時你可以訪問這個tap物件,可修改並返回新的tap物件。
loop: (...args) => void 迴圈鉤子的每個迴圈都會被觸發。

上下文

外掛和攔截器可以可選地訪問上下文物件context,它可以被用來傳遞任意值給後面的外掛或者攔截器。
myCar.hooks.accelerate.intercept({
	context: true,
	tap: (context, tapInfo) => {
		// tapInfo = { type: "sync", name: "NoisePlugin", fn: ... }
		console.log(`${tapInfo.name} is doing it's job`);

		// `context` 從一個空物件開始如果至少有一個外掛裡寫了 `context: true`.
		// 如果沒有外掛定義 `context: true`, 那麼 `context` 是 undefined.
		if (context) {
			// 你可以新增任意值,之後的外掛都能訪問到.
			context.hasMuffler = true;
		}
	}
});

myCar.hooks.accelerate.tap({
	name: "NoisePlugin",
	context: true
}, (context, newSpeed) => {
	if (context && context.hasMuffler) {
		console.log("Silence...");
	} else {
		console.log("Vroom!");
	}
});複製程式碼

HookMap

這是一個鉤子的字典幫助類,比起你直接用js的字典類new Map([['key', hook]]),這個類可能用起來更簡單:
const keyedHook = new HookMap(key => new SyncHook(["arg"]))複製程式碼
keyedHook.tap("some-key", "MyPlugin", (arg) => { /* ... */ });
keyedHook.tapAsync("some-key", "MyPlugin", (arg, callback) => { /* ... */ });
keyedHook.tapPromise("some-key", "MyPlugin", (arg) => { /* ... */ });複製程式碼
const hook = keyedHook.get("some-key");
if(hook !== undefined) {
	hook.callAsync("arg", err => { /* ... */ });
}複製程式碼

Hook/HookMap介面

公有的

interface Hook {
	tap: (name: string | Tap, fn: (context?, ...args) => Result) => void,
	tapAsync: (name: string | Tap, fn: (context?, ...args, callback: (err, result: Result) => void) => void) => void,
	tapPromise: (name: string | Tap, fn: (context?, ...args) => Promise<Result>) => void,
	intercept: (interceptor: HookInterceptor) => void
}

interface HookInterceptor {
	call: (context?, ...args) => void,
	loop: (context?, ...args) => void,
	tap: (context?, tap: Tap) => void,
	register: (tap: Tap) => Tap,
	context: boolean
}

interface HookMap {
	for: (key: any) => Hook,
	tap: (key: any, name: string | Tap, fn: (context?, ...args) => Result) => void,
	tapAsync: (key: any, name: string | Tap, fn: (context?, ...args, callback: (err, result: Result) => void) => void) => void,
	tapPromise: (key: any, name: string | Tap, fn: (context?, ...args) => Promise<Result>) => void,
	intercept: (interceptor: HookMapInterceptor) => void
}

interface HookMapInterceptor {
	factory: (key: any, hook: Hook) => Hook
}

interface Tap {
	name: string,
	type: string
	fn: Function,
	stage: number,
	context: boolean
}複製程式碼

protected(定義鉤子的類才能用的)

interface Hook {
	isUsed: () => boolean,
	call: (...args) => Result,
	promise: (...args) => Promise<Result>,
	callAsync: (...args, callback: (err, result: Result) => void) => void,
}

interface HookMap {
	get: (key: any) => Hook | undefined,
	for: (key: any) => Hook
}複製程式碼

MultiHook類

這是一個像鉤子的類,用來重定向鉤子的外掛到其他鉤子:

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

this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);複製程式碼


相關文章