聊聊 Webpack 外掛系統的關鍵實現 Tapable

蛋先生DX發表於2022-03-27

image.png

丹尼爾:蛋兄,我們們今天聊些什麼呢?

蛋先生:今天就來聊下 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 會怎麼定製你的一天的呢?

相關文章