webpack外掛機制之Tapable
對於Webpack有一句話 Everything is a plugin ,Webpack本質上是一種事件流的機制,它的工作流程就是將各個外掛串聯起來,而實現這一切的核心就是Tapable。Tapable有點類似nodejs的events庫,核心原理也是依賴與釋出訂閱模式。webpack中最核心的負責編譯的Compiler和負責建立bundles的Compilation都是Tapable的例項。下面介紹一下tapable的用法和原理。
const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable"); 複製程式碼
Tapable Hook概覽

Tapable提供了很多型別的hook,分為同步和非同步兩大類(非同步中又區分非同步並行和非同步序列),而根據事件執行的終止條件的不同,由衍生出 Bail/Waterfall/Loop 型別。
下圖展示了每種型別的作用:


- BasicHook: 執行每一個,不關心函式的返回值,有 SyncHook、AsyncParallelHook、AsyncSeriesHook。
- BailHook: 順序執行 Hook,遇到第一個結果 result !== undefined 則返回,不再繼續執行。有:SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。
- WaterfallHook: 類似於 reduce,如果前一個 Hook 函式的結果 result !== undefined,則 result 會作為後一個 Hook 函式的第一個引數。既然是順序執行,那麼就只有 Sync 和 AsyncSeries 類中提供這個Hook:SyncWaterfallHook,AsyncSeriesWaterfallHook
- LoopHook: 不停的迴圈執行 Hook,直到所有函式結果 result === undefined。同樣的,由於對序列性有依賴,所以只有 SyncLoopHook 和 AsyncSeriseLoopHook (PS:暫時沒看到具體使用 Case)
SyncHook的用法及實現
Sync為同步序列的執行關係,用法如下:
let { SyncHook } = require("tapable"); class Lesson { constructor() { this.hooks = { arch: new SyncHook(["name"]) }; } // 註冊監聽函式 tap() { this.hooks.arch.tap("node", function(name) { console.log("node", name); }); this.hooks.arch.tap("react", function(name) { console.log("react", name); }); } start() { this.hooks.arch.call("musion"); } } let l = new Lesson(); // 註冊這兩個事件 l.tap(); // 啟動鉤子 l.start(); /** * 打印出來的值為: * node musion * react musion */ 複製程式碼
SyncHook是一個很典型的通過釋出訂閱方式實現的,實現方式如下:
// 鉤子是同步的 class SyncHook { // args => ["name"] constructor() { this.tasks = []; } tap(name, task) { this.tasks.push(task); } call(...args) { this.tasks.forEach(task => task(...args)); } } let hook = new SyncHook(["name"]); hook.tap("react", function(name) { console.log("react", name); }); hook.tap("node", function(name) { console.log("node", name); }); hook.call("musion"); /** * 打印出來的值為: * node musion * react musion */ 複製程式碼
SyncBailHook的用法及實現
SyncBailHook為同步序列的執行關係,只要監聽函式中有一個函式的返回值不為 null,則跳過剩下所有的邏輯,用法如下:
let { SyncBailHook } = require("tapable"); class Lesson { constructor() { this.hooks = { arch: new SyncBailHook(["name"]) }; } // 註冊監聽函式 tap() { this.hooks.arch.tap("node", function(name) { console.log("node", name); //return "stop"; return undefined; }); this.hooks.arch.tap("react", function(name) { console.log("react", name); }); } start() { this.hooks.arch.call("musion"); } } let l = new Lesson(); // 註冊這兩個事件 l.tap(); // 啟動鉤子 l.start(); /** * 打印出來的值為: * node musion * react musion */ 複製程式碼
SyncBailHook的實現:
// 鉤子是同步的,bail -> 保險 class SyncBailHook { // args => ["name"] constructor() { this.tasks = []; } tap(name, task) { this.tasks.push(task); } call(...args) { // 當前函式的返回值 let ret; // 當前要先執行第一個 let index = 0; do { ret = this.tasks[index++](...args); } while (ret === undefined && index < this.tasks.length); } } let hook = new SyncBailHook(["name"]); hook.tap("react", function(name) { console.log("react", name); return "stop"; }); hook.tap("node", function(name) { console.log("node", name); }); hook.call("musion"); /** * 打印出來的值為: * node musion * react musion */ 複製程式碼
SyncWaterfallHook的用法及實現
SyncWaterfallHook為同步序列的執行關係,上一個監聽函式的返回值可以傳給下一個監聽函式,用法如下:
let { SyncWaterfallHook } = require("tapable"); // waterfall 瀑布 上面會影響下面的 class Lesson { constructor() { this.hooks = { arch: new SyncWaterfallHook(["name"]) }; } // 註冊監聽函式 tap() { this.hooks.arch.tap("node", function(name) { console.log("node", name); return "node學得還不錯"; }); this.hooks.arch.tap("react", function(data) { console.log("react", data); }); } start() { this.hooks.arch.call("musion"); } } let l = new Lesson(); // 註冊這兩個事件 l.tap(); // 啟動鉤子 l.start(); /** * 打印出來的值為: * node musion * react node學得還不錯 */ 複製程式碼
SyncWaterfallHook的實現:
// 鉤子是同步的 class SyncWaterfallHook { // args => ["name"] constructor() { this.tasks = []; } tap(name, task) { this.tasks.push(task); } call(...args) { let [first, ...others] = this.tasks; let ret = first(...args); others.reduce((a, b) => { return b(a); }, ret); } } let hook = new SyncWaterfallHook(["name"]); hook.tap("react", function(name) { console.log("react", name); return "react ok"; }); hook.tap("node", function(data) { console.log("node", data); return "node ok"; }); hook.tap("webpack", function(data) { console.log("webpack", data); }); hook.call("musion"); /** * 打印出來的值為: * react musion * node react ok * webpack node ok */ 複製程式碼
SyncLoopHook的用法及實現
SyncLoopHook為同步迴圈的執行關係,當監聽函式被觸發的時候,如果該監聽函式返回true時則這個監聽函式會反覆執行,如果返回 undefined 則表示退出迴圈,用法如下:
let { SyncLoopHook } = require("tapable"); // 同步遇到某個不返回undefined的監聽函式會多次執行 class Lesson { constructor() { this.index = 0; this.hooks = { arch: new SyncLoopHook(["name"]) }; } // 註冊監聽函式 tap() { this.hooks.arch.tap("node", name => { console.log("node", name); return ++this.index === 3 ? undefined : "繼續學"; }); this.hooks.arch.tap("react", data => { console.log("react", data); }); } start() { this.hooks.arch.call("musion"); } } let l = new Lesson(); // 註冊這兩個事件 l.tap(); // 啟動鉤子 l.start(); /** * 打印出來的值為: * node musion * node musion * node musion * react musion */ 複製程式碼
SyncLoopHook的實現:
// 鉤子是同步的 class SyncLoopHook { // args => ["name"] constructor() { this.tasks = []; } tap(name, task) { this.tasks.push(task); } call(...args) { this.tasks.forEach(task => { let ret; do { ret = task(...args); } while (ret != undefined); }); } } let hook = new SyncLoopHook(["name"]); let total = 0; hook.tap("react", function(name) { console.log("react", name); return ++total === 3 ? undefined : "繼續學"; }); hook.tap("node", function(data) { console.log("node", data); }); hook.tap("webpack", function(data) { console.log("webpack", data); }); hook.call("musion"); /** * 打印出來的值為: * react musion * react musion * react musion * node musion * webpack musion */ 複製程式碼
AsyncParallelHook的用法及實現
AsyncParallelHook為非同步併發的執行關係,用法如下:
let { AsyncParallelHook } = require("tapable"); // 非同步的鉤子分為序列和並行 // 序列:第一個非同步執行完,才會執行第二個 // 並行:需要等待所有併發的非同步事件執行後再執行回撥方法 // 註冊方法: tap註冊 tapAsync註冊 class Lesson { constructor() { this.hooks = { arch: new AsyncParallelHook(["name"]) }; } // 註冊監聽函式 tap() { this.hooks.arch.tapAsync("node", (name, cb) => { setTimeout(() => { console.log("node", name); cb(); }, 1000); }); this.hooks.arch.tapAsync("react", (name, cb) => { setTimeout(() => { console.log("react", name); cb(); }, 1000); }); } start() { this.hooks.arch.callAsync("musion", function() { console.log("end"); }); } } let l = new Lesson(); // 註冊這兩個事件 l.tap(); // 啟動鉤子 l.start(); /** * 打印出來的值為: * node musion * react musion * end */ 複製程式碼
AsyncParallelHook的實現:
class SyncParralleHook { constructor() { this.tasks = []; } tapAsync(name, task) { this.tasks.push(task); } callAsync(...args) { // 拿出最終的函式 let finalCallBack = args.pop(); let index = 0; // 類似Promise.all let done = () => { index++; if (index === this.tasks.length) { finalCallBack(); } }; this.tasks.forEach(task => { task(...args, done); }); } } let hook = new SyncParralleHook(["name"]); hook.tapAsync("react", function(name, cb) { setTimeout(() => { console.log("react", name); cb(); }, 1000); }); hook.tapAsync("node", function(name, cb) { setTimeout(() => { console.log("node", name); cb(); }, 1000); }); hook.callAsync("musion", function() { console.log("end"); }); /** * 打印出來的值為: * react musion * react musion * react musion * node musion * webpack musion */ 複製程式碼
AsyncSeriesHook的用法及實現
AsyncSeriesHook為非同步序列的執行關係,用法如下:
// AsyncSeriesHook 非同步序列 let { AsyncSeriesHook } = require("tapable"); class Lesson { constructor() { this.hooks = { arch: new AsyncSeriesHook(["name"]) }; } // 註冊監聽函式 tap() { this.hooks.arch.tapAsync("node", (name, cb) => { setTimeout(() => { console.log("node", name); cb(); }, 4000); }); this.hooks.arch.tapAsync("react", (name, cb) => { setTimeout(() => { console.log("react", name); cb(); }, 1000); }); } start() { this.hooks.arch.callAsync("musion", function() { console.log("end"); }); } } let l = new Lesson(); // 註冊這兩個事件 l.tap(); // 啟動鉤子 l.start(); /** * 打印出來的值為: * node musion * react musion * end */ 複製程式碼
AsyncSeriesHook的實現:
class SyncSeriesHook { constructor() { this.tasks = []; } tapAsync(name, task) { this.tasks.push(task); } callAsync(...args) { let finalCallback = args.pop(); let index = 0; let next = () => { if (this.tasks.length === index) return finalCallback(); let task = this.tasks[index++]; task(...args, next); }; next(); } } 複製程式碼
AsyncSeriesWaterfallHook的用法及實現
AsyncSeriesWaterfallHook為非同步序列的執行關係,上一個監聽函式的中的callback(err, data)的第二個引數,可以作為下一個監聽函式的引數,用法如下:
class SyncSeriesWaterfallHook { constructor() { this.tasks = []; } tapAsync(name, task) { this.tasks.push(task); } callAsync(...args) { let finalCallback = args.pop(); let index = 0; let next = (err, data) => { let task = this.tasks[index]; if (!task) return finalCallback(); // 執行的是第一個 if (index === 0) { task(...args, next); } else { task(data, next); } index++; }; next(); } } let hook = new SyncSeriesWaterfallHook(["name"]); hook.tapAsync("react", function(name, cb) { setTimeout(() => { console.log("react", name); cb(null, "musion"); }, 3000); }); hook.tapAsync("node", function(name, cb) { setTimeout(() => { console.log("node", name); cb(null); }, 1000); }); hook.callAsync("musion", function() { console.log("end"); }); /** * 打印出來的值為: * node musion * end */ 複製程式碼
AsyncSeriesWaterfallHook的實現:
class SyncSeriesWaterfallHook { constructor() { this.tasks = []; } tapAsync(name, task) { this.tasks.push(task); } callAsync(...args) { let finalCallback = args.pop(); let index = 0; let next = (err, data) => { let task = this.tasks[index]; if (!task) return finalCallback(); // 執行的是第一個 if (index === 0) { task(...args, next); } else { task(data, next); } index++; }; next(); } } 複製程式碼