webpack外掛機制之Tapable

慕晨同學發表於2019-02-08

文章首發於:github.com/USTB-musion…

Tapable

對於Webpack有一句話Everything is a plugin,Webpack本質上是一種事件流的機制,它的工作流程就是將各個外掛串聯起來,而實現這一切的核心就是Tapable。Tapable有點類似nodejs的events庫,核心原理也是依賴與釋出訂閱模式。webpack中最核心的負責編譯的Compiler和負責建立bundles的Compilation都是Tapable的例項。下面介紹一下tapable的用法和原理。以下例項的程式碼原文地址為github.com/USTB-musion…

const {
	SyncHook,
	SyncBailHook,
	SyncWaterfallHook,
	SyncLoopHook,
	AsyncParallelHook,
	AsyncParallelBailHook,
	AsyncSeriesHook,
	AsyncSeriesBailHook,
	AsyncSeriesWaterfallHook
 } = require("tapable");
複製程式碼

Tapable Hook概覽

webpack外掛機制之Tapable
Tapable提供了很多型別的hook,分為同步和非同步兩大類(非同步中又區分非同步並行和非同步序列),而根據事件執行的終止條件的不同,由衍生出 Bail/Waterfall/Loop 型別。

下圖展示了每種型別的作用:

webpack外掛機制之Tapable
webpack外掛機制之Tapable

  • 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();
  }
}
複製程式碼

參考文章

webpack系列之二Tapable

tapable

相關文章