11

聊聊 Webpack 插件系统的关键实现 Tapable

 2 years ago
source link: https://xie.infoq.cn/article/cadf9a45c25be872cd7dfe17d
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

聊聊 Webpack 插件系统的关键实现 Tapable

丹尼尔:蛋兄,咱们今天聊些什么呢?

蛋先生:今天就来聊下 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();// MorninggetUpAction(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 upBrush TeethEat 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();// MorninggetUpAction(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 upMake 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 upMake breakfastEat 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...// NoonsoupAction(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...// AfternoonstudyAction(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 minutesrest 5 minutesstudy 25 minutesrest 5 minutesstudy 25 minutesrest 5 minutesstudy 25 minutesrest 5 minutesstudy 25 minutesrest 5 minutesstudy 25 minutesrest 5 minutesstudy 25 minutesrest 5 minutesstudy 25 minutesrest 5 minutestomatoStudy: 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...// NightplayGameAction(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 会怎么定制你的一天的呢?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK