丹尼爾:蛋兄,我們們今天聊些什麼呢?
蛋先生:今天就來聊下 webpack 中外掛系統實現的關鍵 - Tapable
丹尼爾:Tapable?
蛋先生:沒錯,我們們今天換種方式來聊吧,就聊你的一天
丹尼爾:我的一天?
蛋先生:首先,每個人的一天都有這麼幾個階段:早上,中午,下午,晚上。用 Tapable 的方式描述是以下這個樣子:
const { SyncHook } = require("tapable");
class Man {
constructor() {
this.hooks = {
morningHook: new SyncHook(),
noonHook: new SyncHook(),
afternoonHook: new SyncHook(),
nightHook: new SyncHook(),
};
}
startNewDay() {
this.hooks.morningHook.call();
this.hooks.noonHook.call();
this.hooks.afternoonHook.call();
this.hooks.nightHook.call();
}
}
丹尼爾:SyncHook 是啥?
蛋先生:先不著急,等會你就會明白的。首先你是一個人。
丹尼爾:不然呢?難道還會是禽獸嗎?(`へ´)
蛋先生:(lll¬ω¬) 誤會誤會,看看程式碼吧
const daniel = new Man();
daniel.startNewDay();
丹尼爾:哦,懂了。那我的一天都準備幹些啥呢?
蛋先生:首先是早上,早上你就做了三件事:起床,刷牙,吃早餐
丹尼爾:就這?還以為有什麼驚喜呢
蛋先生:我又不是講段子的 ╮(╯▽╰)╭,我只會講程式碼,來
const daniel = new Man();
// Morning
getUpAction(daniel);
brushTeethAction(daniel);
eatBreakfastAction(daniel);
daniel.startNewDay();
function getUpAction(manInst) {
manInst.hooks.morningHook.tap("getUp", () => console.log("Get up"));
}
function brushTeethAction(manInst) {
manInst.hooks.morningHook.tap("brushTeeth", () => console.log("Brush Teeth"));
}
function eatBreakfastAction(manInst) {
manInst.hooks.morningHook.tap("eatBreakfast", () =>
console.log("Eat breakfast")
);
}
輸出結果:
Get up
Brush Teeth
Eat breakfast
丹尼爾:我好像看出點什麼了,Man 只是定義了生命週期鉤子,但每個階段做什麼,都是通過增加行為來靈活擴充套件(PS:這裡的行為你可以理解為外掛,只是為了配合這個劇本而已)
蛋先生:是的沒錯。這裡的起床,刷牙等行為,彼此間獨立,且都是同步順序執行的,所以我們只用普通的同步 Hook 就行了,即 SyncHook。
class Man {
constructor() {
this.hooks = {
morningHook: new SyncHook(),
...
};
}
startNewDay() {
this.hooks.morningHook.call();
...
}
}
丹尼爾:這裡的 this.hooks.morningHook.call()
就是通知早上這個週期階段開始了,然後前面各個 Action 通過 manInst.hooks.morningHook.tap
已經提前註冊好要在這個週期做些什麼,所以此時各個 Action 也就忙碌起來了是吧
蛋先生:Yes。前面你不是問了 SyncHook 嗎?因為行為有同步和非同步,所以 Sync 開頭的 Hook 就是同步執行的,而 Async 開頭的就是非同步執行的
丹尼爾:原來如此,那一個週期階段上掛這麼多行為,是不是要等待所有行為結束才進到下個週期階段
蛋先生:是的沒錯。一個週期階段上可以掛多個行為,一般先掛先執行(SyncXXX 和 AsyncSeriesXXX),還有一種是併發執行,當然也只有非同步行為才能併發。接下來我們繼續通過你的一天來了解 Tapable 的各種 Hook 及其它資訊吧
丹尼爾:好的,早上聊完了,中午幹啥呢?
蛋先生:不不,還是早上。我們稍微調整下早上做的事,換成起床,做早餐,吃早餐
丹尼爾:額,還是一樣平平無奇啊
蛋先生:你在做早餐時搞砸了
丹尼爾:啊,這麼倒黴?那我豈不是要餓肚子了 X﹏X
蛋先生:做早餐完成不了,意味著吃早餐需要中斷,這個時候就需要 SyncBailHook
const { SyncBailHook } = require("tapable");
class Man {
constructor() {
this.hooks = {
morningHook: new SyncBailHook(),
...
};
}
...
}
const daniel = new Man();
// Morning
getUpAction(daniel);
makeBreakfastAction(daniel);
eatBreakfastAction(daniel);
daniel.startNewDay();
function getUpAction(manInst) {
manInst.hooks.morningHook.tap("getUp", () => console.log("Get up"));
}
function makeBreakfastAction(manInst) {
manInst.hooks.morningHook.tap("makeBreakfast", () => {
console.log("Make breakfast, but failed");
return false;
});
}
function eatBreakfastAction(manInst) {
manInst.hooks.morningHook.tap("eatBreakfast", () =>
console.log("Eat breakfast")
);
}
輸出結果
Get up
Make breakfast, but failed
丹尼爾:好吧,吃不了就算了,只能捱餓到中午了
蛋先生:早餐不吃對身體不好,我改下劇情。你成功地做完早餐,做了牛奶,雞蛋和麵包。但我們需要把做早餐的成果給到吃早餐,這樣吃早餐才有東西可以吃,這時就可以用 SyncWaterfallHook
const { SyncWaterfallHook } = require("tapable");
class Man {
constructor() {
this.hooks = {
morningHook: new SyncWaterfallHook(["breakfast"]),
...
};
}
...
}
function makeBreakfastAction(manInst) {
manInst.hooks.morningHook.tap("makeBreakfast", () => {
console.log("Make breakfast");
return "milk, bread, eggs";
});
}
function eatBreakfastAction(manInst) {
manInst.hooks.morningHook.tap("eatBreakfast", (breakfast) =>
console.log("Eat breakfast: ", breakfast)
);
}
輸出結果:
Get up
Make breakfast
Eat breakfast: milk, bread, eggs
丹尼爾:謝了蛋兄,對我真不錯。早餐也吃完了,要到中午了嗎?
蛋先生:是的,中午到了,你又開始做飯了
丹尼爾:啊,我就是個吃貨啊,煮啥呢?
蛋先生:你一邊煮飯一邊煲湯。
丹尼爾:一邊...一邊...,那就是同時做兩件事啊
蛋先生:是的,什麼行為可以同時做,當然是非同步行為啦,這時就可以用 AsyncParallelHook 了
const { AsyncParallelHook } = require("tapable");
class Man {
constructor() {
this.hooks = {
...
noonHook: new AsyncParallelHook(),
...
};
}
async startNewDay() {
...
await this.hooks.noonHook.promise();
...
}
}
const daniel = new Man();
// Morning
...
// Noon
soupAction(daniel);
cookRiceAction(daniel);
daniel.startNewDay();
...
function cookRiceAction(manInst) {
manInst.hooks.noonHook.tapPromise("cookRice", () => {
console.log("cookRice starting...");
return new Promise((resolve) => {
setTimeout(() => {
console.log("cookRice finishing...");
resolve();
}, 800);
});
});
}
function soupAction(manInst) {
manInst.hooks.noonHook.tapPromise("soup", () => {
console.log("soup starting...");
return new Promise((resolve) => {
setTimeout(() => {
console.log("soup finishing...");
resolve();
}, 1000);
});
});
}
輸出如下:
soup starting...
cookRice starting...
cookRice finishing...
soup finishing...
丹尼爾:好吧,中午看上去比早上順利多了
蛋先生:接下來到下午了,下午你開始用番茄工作法學習四個小時
丹尼爾:恩,你又知道我這麼好學,真是太瞭解我了
蛋先生:因為一個番茄鐘不斷地迴圈,直到 4 小時過去才結束,所以可以用到 SyncLoopHook
const { SyncLoopHook } = require("tapable");
class Man {
constructor() {
this.hooks = {
...
afternoonHook: new SyncLoopHook(),
...
};
}
async startNewDay() {
...
this.hooks.afternoonHook.call();
...
}
}
const daniel = new Man();
// Morning
...
// Noon
...
// Afternoon
studyAction(daniel);
restAction(daniel)
daniel.startNewDay();
...
let leftTime = 4 * 60;
function studyAction(manInst) {
manInst.hooks.afternoonHook.tap("study", () => {
console.log("study 25 minutes");
leftTime -= 25;
});
}
function restAction(manInst) {
manInst.hooks.afternoonHook.tap("study", () => {
console.log("rest 5 minutes");
leftTime -= 5;
if (leftTime <= 0) {
console.log("tomatoStudy: finish");
return;
}
return true;
});
}
輸出結果:
study 25 minutes
rest 5 minutes
study 25 minutes
rest 5 minutes
study 25 minutes
rest 5 minutes
study 25 minutes
rest 5 minutes
study 25 minutes
rest 5 minutes
study 25 minutes
rest 5 minutes
study 25 minutes
rest 5 minutes
study 25 minutes
rest 5 minutes
tomatoStudy: finish
丹尼爾:學到頭昏腦漲的,晚上該放鬆放鬆了
蛋先生:恩,到了晚上了,你可能玩遊戲,也可能看電影,這取決於有沒朋友找你上分
丹尼爾:哦,就是看情況而定,不是所有行為都執行是吧
蛋先生:是的,這就需要用到 HookMap
const { SyncHook, HookMap } = require("tapable");
class Man {
constructor() {
this.hooks = {
...
nightHook: new HookMap(() => new SyncHook()),
};
}
async startNewDay() {
...
this.hooks.nightHook.for("no friend invitation").call();
}
}
const daniel = new Man();
// Morning
...
// Noon
...
// Afternoon
...
// Night
playGameAction(daniel);
watchMovieAction(daniel);
daniel.startNewDay();
...
function playGameAction(manInst) {
manInst.hooks.nightHook.for("friend invitation").tap("playGame", () => {
console.log("play game");
});
}
function watchMovieAction(manInst) {
manInst.hooks.nightHook.for("no friend invitation").tap("watchMovie", () => {
console.log("watch movie");
});
}
輸出結果:
watch movie
丹尼爾:一天就這麼過完了,我們該說再見了
蛋先生:還沒完,你有寫日記的好習慣,而且是每做一件事就記
丹尼爾:每一件都記?這是記流水賬吧
蛋先生:差不多吧,你覺得怎麼記最好呢
丹尼爾:做每件事之前進行攔截咯
蛋先生:真聰明,這裡可以用 Interception
...
const daniel = new Man();
writeDiary(daniel);
...
daniel.startNewDay();
...
function writeDiary(manInst) {
const interceptFn = (hookName) => {
return {
tap: (tapInfo) => {
console.log(`write diary:`, tapInfo)
}
};
};
Object.keys(manInst.hooks).forEach((hookName) => {
if (manInst.hooks[hookName] instanceof HookMap) {
manInst.hooks[hookName].intercept({
factory: (key, hook) => {
hook.intercept(interceptFn(hookName));
return hook
},
});
} else {
manInst.hooks[hookName].intercept(interceptFn(hookName));
}
});
}
輸出結果:
write diary: { type: 'sync', fn: [Function], name: 'getUp' }
write diary: { type: 'sync', fn: [Function], name: 'makeBreakfast' }
write diary: { type: 'sync', fn: [Function], name: 'eatBreakfast' }
write diary: { type: 'promise', fn: [Function], name: 'soup' }
write diary: { type: 'promise', fn: [Function], name: 'cookRice' }
write diary: { type: 'sync', fn: [Function], name: 'study' }
write diary: { type: 'sync', fn: [Function], name: 'study' }
write diary: { type: 'sync', fn: [Function], name: 'study' }
write diary: { type: 'sync', fn: [Function], name: 'study' }
write diary: { type: 'sync', fn: [Function], name: 'study' }
write diary: { type: 'sync', fn: [Function], name: 'study' }
write diary: { type: 'sync', fn: [Function], name: 'study' }
write diary: { type: 'sync', fn: [Function], name: 'study' }
write diary: { type: 'sync', fn: [Function], name: 'watchMovie' }
丹尼爾:日記也寫完了,沒啥其它事了吧
蛋先生:最後的最後,聊一下 context 吧。因為每個行為都可能由不同的開發者提供,行為之間獨立,但有時又想共享一些資料,比如這裡需要共享下你的個人資訊,再看最後的一段程式碼,然後就可以散了,再堅持一小會
...
const daniel = new Man();
writeDiary(daniel);
...
daniel.startNewDay();
function getUpAction(manInst) {
manInst.hooks.morningHook.tap(
{
name: "getUp",
context: true,
},
(context) => {
console.log("Get up", context);
}
);
}
...
function writeDiary(manInst) {
const interceptFn = (hookName) => {
return {
context: true,
tap: (context, tapInfo) => {
context = context || {};
context.userInfo = {
name: "daniel",
};
}
};
};
Object.keys(manInst.hooks).forEach((hookName) => {
if (manInst.hooks[hookName] instanceof HookMap) {
manInst.hooks[hookName].intercept({
factory: (key, hook) => {
console.log(`[${hookName}][${key}]`);
hook.intercept(interceptFn(hookName));
return hook;
},
});
} else {
manInst.hooks[hookName].intercept(interceptFn(hookName));
}
});
}
輸出結果:
Get up { userInfo: { name: 'daniel' } }
丹尼爾:好睏,眼睛快要睜不開了
蛋先生:好了,你的一天就聊完了,再見
丹尼爾:告辭
堅持讀到這裡的小夥伴們,你們通過 Tapable 會怎麼定製你的一天的呢?