上述 9 個 hooks 都繼承自 Hook 這個 classjavascript
hook 對外提供了 isUsed
call
promise
callAsync
compile
tap
tapAsync
tapPromise
intercept
這些方法java
其中 tap
開頭的方法是用來訂閱事件的,call
promise
callAsync
是用來觸發事件的,isUsed
返回了一個 boolean
值用來標記當前 hook
中註冊的事件是否被執行完成。webpack
isUsed
源碼webisUsed() { 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
調用會返回一個 Promise
,callAsync
默認支持傳入一個 callback
。bash
Sync
開頭的 hook
不支持使用 tapAsync
和 tapPromise
,能夠看下述的以 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;
複製代碼
在這裏面咱們能夠看到 tapAsync
和 tapPromise
是被重寫了直接 throw error
了async
下面的例子會給你們帶來一個簡單地示範函數
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
的配置文件後,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
這一段,這一段分了兩種狀況來進行插件的插入
webpack
調用,也就是說咱們能夠用函數來寫插件,這個函數的做用域是當前的 compiler
,函數也會接收到一個 compiler
apply
方法的對象實例,apply
方法會被傳入 compiler
因此這也就解釋了爲何咱們的插件須要 new
出來以後傳入到 webpack
上一個中咱們瞭解到了 plugins
是什麼時候被注入的,咱們能夠看到在 plugin
的注入時傳入了當前被實例化出來的 Compiler
,因此如今咱們須要瞭解下 Compiler
中作了什麼
進入 Compiler.js
(也在 lib 中)咱們能夠第一時間看到 Compiler
的 constructor
中定義了一個龐大的 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
,咱們能夠看下他是如何運行的,在 HtmlWebpackPlugin
的 apply
中咱們能夠找到這樣一段代碼:
compiler.hooks.emit.tapAsync('HtmlWebpackPlugin', (compiler, callback) => {
...
})
複製代碼
說明 HtmlWebpackPlugin
是利用了 Compiler
的 emit
的 hook
來完成的
經過深刻了解,webpack
是在龐大的插件上運行的,他本身內置了不少插件
上述內容若有錯誤,請指正