tapable 助你解析 webpack 的插件系統

tapable

tapable 導出了 9 個 hooks

  • SyncHook
  • SyncBailHook
  • SyncWaterfallHook
  • SyncLoopHook
  • AsyncParallelHook
  • AsyncParallelBailHook
  • AsyncSeriesHook
  • AsyncSeriesBailHook
  • AsyncSeriesWaterfallHook

上述 9 個 hooks 都繼承自 Hook 這個 classjavascript

tapable Hook 解析

hook 對外提供了 isUsed call promise callAsync compile tap tapAsync tapPromise intercept 這些方法java

其中 tap 開頭的方法是用來訂閱事件的,call promise callAsync 是用來觸發事件的,isUsed 返回了一個 boolean 值用來標記當前 hook 中註冊的事件是否被執行完成。webpack

isUsed 源碼web

isUsed() {
		return this.taps.length > 0 || this.interceptors.length > 0;
}
複製代碼

tap tapAsync tapPromise 這三個方法第一個參數傳入能夠支持傳入 string(通常是指 plugin 的名稱) 或者一個 Tap 類型,第二個參數是一個回調用來接收事件被 emit 時的調用。數組

export interface Tap {
    name: string; // 事件名稱,通常就是 plugin 的名字
    type: TapType; // 支持三種類型 'sync' 'async' 'promise'
    fn: Function;
    stage: number;
    context: boolean;
}
複製代碼

call promise callAsync 這三個方法在傳入參數的時候是依賴於 hook 被實例化的時候傳入的 args 數組佔位符的數量的,以下示例:promise

const sync = new SyncHook(['arg1', 'arg2']) // 'arg1' 'arg2' 爲參數佔位符
sync.tap('Test', (arg1, arg2) => {
  console.log(arg1, arg2) // a2
})
sync.call('a', '2')
複製代碼

其中 promise 調用會返回一個 PromisecallAsync 默認支持傳入一個 callbackbash

Sync 開頭的 hook 不支持使用 tapAsynctapPromise,能夠看下述的以 SyncHook 的源碼爲例app

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;
複製代碼

在這裏面咱們能夠看到 tapAsynctapPromise 是被重寫了直接 throw errorasync

一個簡單的使用示範

下面的例子會給你們帶來一個簡單地示範函數

class TapableTest {
  constructor() {
    this.hooks = {
      sync: new SyncHook(['context', 'hi']),
      syncBail: new SyncBailHook(),
      syncLoop: new SyncLoopHook(),
      syncWaterfall: new SyncWaterfallHook(['syncwaterfall']),
      asyncParallel: new AsyncParallelHook(),
      asyncParallelBail: new AsyncParallelBailHook(),
      asyncSeries: new AsyncSeriesHook(),
      asyncSeriesBail: new AsyncSeriesBailHook(),
      asyncSeriesWaterfall: new AsyncSeriesWaterfallHook(['asyncwaterfall']) 
    }
  }
  emitSync() {
    this.hooks.sync.call(this, err => {
        console.log(this.hooks.sync.promise)
        console.log(err)
    })
  }
  emitAyncSeries() { 
    this.hooks.asyncSeries.callAsync(err => {
        if (err) console.log(err)
    })
  }
}

const test = new TapableTest()
test.hooks.sync.tap('TestPlugin', (context, callback) => {
  console.log('trigger: ', context)
  callback(new Error('this is sync error'))
})
test.hooks.asyncSeries.tapAsync('AsyncSeriesPlugin', callback => {
    callback(new Error('this is async series error'))
})
test.emitSync()
test.emitAyncSeries()
複製代碼

上述的運行結果能夠這查看 runkit

下面來聊一聊 webpack 中的插件是如何依賴 tapable 的

webpack 插件被注入的時機

當咱們定義了 webpack 的配置文件後,webpack 會根據這些配置生成一個或多個 compiler ,而插件就是在建立 compiler 時被添加到 webpack 的整個運行期間的, 能夠看下述源碼:(相關源碼能夠在 webpack lib 下的 webpack.js 中找到)

const createCompiler = rawOptions => {
	const options = getNormalizedWebpackOptions(rawOptions);
	applyWebpackOptionsBaseDefaults(options);
	const compiler = new Compiler(options.context);
	compiler.options = 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;
};
複製代碼

咱們能夠看到遍歷 options.plugins 這一段,這一段分了兩種狀況來進行插件的插入

  • 咱們的 plugin 能夠以函數的方式被 webpack 調用,也就是說咱們能夠用函數來寫插件,這個函數的做用域是當前的 compiler,函數也會接收到一個 compiler
  • 能夠傳入一個包含 apply 方法的對象實例,apply 方法會被傳入 compiler

因此這也就解釋了爲何咱們的插件須要 new 出來以後傳入到 webpack

進入 Compiler 一探究竟

上一個中咱們瞭解到了 plugins 是什麼時候被注入的,咱們能夠看到在 plugin 的注入時傳入了當前被實例化出來的 Compiler,因此如今咱們須要瞭解下 Compiler 中作了什麼

進入 Compiler.js (也在 lib 中)咱們能夠第一時間看到 Compilerconstructor 中定義了一個龐大的 hooks

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,webpack 正是依賴於這些複雜的構建 hook 而完成了咱們的代碼構建,因此在咱們編寫 plugin 時就能夠利用這些 hook 來完成咱們的特殊需求。

好比咱們常常用到的 HtmlWebpackPlugin ,咱們能夠看下他是如何運行的,在 HtmlWebpackPluginapply 中咱們能夠找到這樣一段代碼:

compiler.hooks.emit.tapAsync('HtmlWebpackPlugin', (compiler, callback) => {
  ...
})
複製代碼

說明 HtmlWebpackPlugin 是利用了 Compileremithook 來完成的

經過深刻了解,webpack 是在龐大的插件上運行的,他本身內置了不少插件

上述內容若有錯誤,請指正

相關文章
相關標籤/搜索