webpack 核心模塊 tapable 用法解析
前不久寫了一篇 webpack 基本原理和 AST 用法的文章 [1],本來想接着寫webpack plugin
的原理的,但是發現webpack plugin
高度依賴 tapable[2] 這個庫,不清楚tapable
而直接去看webpack plugin
始終有點霧裏看花的意思。所以就先去看了下tapable
的文檔和源碼,發現這個庫非常有意思,是增強版的發佈訂閱模式
。發佈訂閱模式
在源碼世界實在是太常見了,我們已經在多個庫源碼裏面見過了:
-
redux 的 subscribe 和 dispatch[3]
-
Node.js 的 EventEmitter[4]
-
redux-saga 的 take 和 put[5]
這些庫基本都自己實現了自己的發佈訂閱模式
,實現方式主要是用來滿足自己的業務需求,而tapable
並沒有具體的業務邏輯,是一個專門用來實現事件訂閱或者他自己稱爲hook
(鉤子) 的工具庫,其根本原理還是發佈訂閱模式
,但是他實現了多種形式的發佈訂閱模式
,還包含了多種形式的流程控制。
tapable
暴露多個 API,提供了多種流程控制方式,連使用都是比較複雜的,所以我想分兩篇文章來寫他的原理:
-
先看看用法,體驗下他的多種流程控制方式
-
通過用法去看看源碼是怎麼實現的
本文就是講用法的文章,知道了他的用法,大家以後如果有自己實現hook
或者事件監聽的需求,可以直接拿過來用,非常強大!
本文例子已經全部上傳到 GitHub,大家可以拿下來做個參考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage[6]
tapable 是什麼
tapable
是webpack
的核心模塊,也是webpack
團隊維護的,是webpack plugin
的基本實現方式。他的主要功能是爲使用者提供強大的hook
機制,webpack plugin
就是基於hook
的。
主要 API
下面是官方文檔中列出來的主要 API,所有 API 的名字都是以Hook
結尾的:
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
這些 API 的名字其實就解釋了他的作用,注意這些關鍵字:Sync
, Async
, Bail
, Waterfall
, Loop
, Parallel
, Series
。下面分別來解釋下這些關鍵字:
Sync:這是一個同步的hook
Async:這是一個異步的hook
Bail:Bail
在英文中的意思是保險,保障
的意思,實現的效果是,當一個hook
註冊了多個回調方法,任意一個回調方法返回了不爲undefined
的值,就不再執行後面的回調方法了,就起到了一個 “保險絲” 的作用。
Waterfall:Waterfall
在英語中是瀑布
的意思,在編程世界中表示順序執行各種任務,在這裏實現的效果是,當一個hook
註冊了多個回調方法,前一個回調執行完了纔會執行下一個回調,而前一個回調的執行結果會作爲參數傳給下一個回調函數。
Loop:Loop
就是循環的意思,實現的效果是,當一個hook
註冊了回調方法,如果這個回調方法返回了true
就重複循環這個回調,只有當這個回調返回undefined
才執行下一個回調。
Parallel:Parallel
是並行的意思,有點類似於Promise.all
,就是當一個hook
註冊了多個回調方法,這些回調同時開始並行執行。
Series:Series
就是串行的意思,就是當一個hook
註冊了多個回調方法,前一個執行完了纔會執行下一個。
Parallel
和Series
的概念只存在於異步的hook
中,因爲同步hook
全部是串行的。
下面我們分別來介紹下每個 API 的用法和效果。
同步 API
同步 API 就是這幾個:
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
} = require("tapable");
前面說了,同步 API 全部是串行的,所以這幾個的區別就在流程控制上。
SyncHook
SyncHook
是一個最基礎的hook
,其使用方法和效果接近我們經常使用的發佈訂閱模式
,注意tapable
導出的所有hook
都是類,基本用法是這樣的:
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
因爲SyncHook
是一個類,所以使用new
來生成一個實例,構造函數接收的參數是一個數組["arg1", "arg2", "arg3"]
,這個數組有三項,表示生成的這個實例註冊回調的時候接收三個參數。實例hook
主要有兩個實例方法:
-
tap
:就是註冊事件回調的方法。 -
call
:就是觸發事件,執行回調的方法。
下面我們擴展下官方文檔中小汽車加速的例子來說明下具體用法:
const { SyncHook } = require("tapable");
// 實例化一個加速的hook
const accelerate = new SyncHook(["newSpeed"]);
// 註冊第一個回調,加速時記錄下當前速度
accelerate.tap("LoggerPlugin", (newSpeed) =>
console.log("LoggerPlugin", `加速到 ${newSpeed}`)
);
// 再註冊一個回調,用來檢測是否超速
accelerate.tap("OverspeedPlugin", (newSpeed) => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速!!");
}
});
// 再註冊一個回調,用來檢測速度是否快到損壞車子了
accelerate.tap("DamagePlugin", (newSpeed) => {
if (newSpeed > 300) {
console.log("DamagePlugin", "速度實在太快,車子快散架了。。。");
}
});
// 觸發一下加速事件,看看效果吧
accelerate.call(500);
然後運行下看看吧,當加速事件出現的時候,會依次執行這三個回調:
image-20210309160302799
上面這個例子主要就是用了tap
和call
這兩個實例方法,其中tap
接收兩個參數,第一個是個字符串,並沒有實際用處,僅僅是一個註釋的作用,第二個參數就是一個回調函數,用來執行事件觸發時的具體邏輯。
accelerate.tap("LoggerPlugin", (newSpeed) =>
console.log("LoggerPlugin", `加速到 ${newSpeed}`)
);
上述這種寫法其實與 webpack 官方文檔中對於 plugin 的介紹非常像了 [7],因爲webpack
的plguin
就是用tapable
實現的,第一個參數一般就是plugin
的名字:
image-20210309154641835
而call
就是簡單的觸發這個事件,在webpack
的plguin
中一般不需要開發者去觸發事件,而是webpack
自己在不同階段會觸發不同的事件,比如beforeRun
, run
等等,plguin
開發者更多的會關注這些事件出現時應該進行什麼操作,也就是在這些事件上註冊自己的回調。
SyncBailHook
上面的SyncHook
其實就是一個簡單的發佈訂閱模式
,SyncBailHook
就是在這個基礎上加了一點流程控制,前面我們說過了,Bail
就是個保險,實現的效果是,前面一個回調返回一個不爲undefined
的值,就中斷這個流程。比如我們現在將前面這個例子的SyncHook
換成SyncBailHook
,然後在檢測超速的這個插件裏面加點邏輯,當它超速了就返回錯誤,後面的DamagePlugin
就不會執行了:
const { SyncBailHook } = require("tapable"); // 使用的是SyncBailHook
// 實例化一個加速的hook
const accelerate = new SyncBailHook(["newSpeed"]);
accelerate.tap("LoggerPlugin", (newSpeed) =>
console.log("LoggerPlugin", `加速到 ${newSpeed}`)
);
// 再註冊一個回調,用來檢測是否超速
// 如果超速就返回一個錯誤
accelerate.tap("OverspeedPlugin", (newSpeed) => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速!!");
return new Error('您已超速!!');
}
});
accelerate.tap("DamagePlugin", (newSpeed) => {
if (newSpeed > 300) {
console.log("DamagePlugin", "速度實在太快,車子快散架了。。。");
}
});
accelerate.call(500);
然後再運行下看看:
image-20210309161001682
可以看到由於OverspeedPlugin
返回了一個不爲undefined
的值,DamagePlugin
被阻斷,沒有運行了。
SyncWaterfallHook
SyncWaterfallHook
也是在SyncHook
的基礎上加了點流程控制,前面說了,Waterfall
實現的效果是將上一個回調的返回值作爲參數傳給下一個回調。所以通過call
傳入的參數只會傳遞給第一個回調函數,後面的回調接受都是上一個回調的返回值,最後一個回調的返回值會作爲call
的返回值返回給最外層:
const { SyncWaterfallHook } = require("tapable");
const accelerate = new SyncWaterfallHook(["newSpeed"]);
accelerate.tap("LoggerPlugin", (newSpeed) => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
return "LoggerPlugin";
});
accelerate.tap("Plugin2", (data) => {
console.log(`上一個插件是: ${data}`);
return "Plugin2";
});
accelerate.tap("Plugin3", (data) => {
console.log(`上一個插件是: ${data}`);
return "Plugin3";
});
const lastPlugin = accelerate.call(100);
console.log(`最後一個插件是:${lastPlugin}`);
然後看下運行效果吧:
image-20210309162008465
SyncLoopHook
SyncLoopHook
是在SyncHook
的基礎上添加了循環的邏輯,也就是如果一個插件返回true
就會一直執行這個插件,直到他返回undefined
纔會執行下一個插件:
const { SyncLoopHook } = require("tapable");
const accelerate = new SyncLoopHook(["newSpeed"]);
accelerate.tap("LoopPlugin", (newSpeed) => {
console.log("LoopPlugin", `循環加速到 ${newSpeed}`);
return new Date().getTime() % 5 !== 0 ? true : undefined;
});
accelerate.tap("LastPlugin", (newSpeed) => {
console.log("循環加速總算結束了");
});
accelerate.call(100);
執行效果如下:
image-20210309163514680
異步 API
所謂異步 API 是相對前面的同步 API 來說的,前面的同步 API 的所有回調都是按照順序同步執行的,每個回調內部也全部是同步代碼。但是實際項目中,可能需要回調裏面處理異步情況,也可能希望多個回調可以同時並行執行,也就是Parallel
。這些需求就需要用到異步 API 了,主要的異步 API 就是這些:
const {
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
既然涉及到了異步,那肯定還需要異步的處理方式,tapable
支持回調函數和Promise
兩種異步的處理方式。所以這些異步 API 除了用前面的tap
來註冊回調外,還有兩個註冊回調的方法:tapAsync
和tapPromise
,對應的觸發事件的方法爲callAsync
和promise
。下面分別來看下每個 API 吧:
AsyncParallelHook
AsyncParallelHook
從前面介紹的命名規則可以看出,他是一個異步並行執行的Hook
,我們先用tapAsync
的方式來看下怎麼用吧。
tapAsync 和 callAsync
還是那個小汽車加速的例子,只不過這個小汽車加速沒那麼快了,需要一秒才能加速完成,然後我們在 2 秒的時候分別檢測是否超速和是否損壞,爲了看出並行的效果,我們記錄下整個過程從開始到結束的時間:
const { AsyncParallelHook } = require("tapable");
const accelerate = new AsyncParallelHook(["newSpeed"]);
console.time("total time"); // 記錄起始時間
// 注意註冊異步事件需要使用tapAsync
// 接收的最後一個參數是done,調用他來表示當前任務執行完畢
accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
// 1秒後加速才完成
setTimeout(() => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
done();
}, 1000);
});
accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
// 2秒後檢測是否超速
setTimeout(() => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速!!");
}
done();
}, 2000);
});
accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
// 2秒後檢測是否損壞
setTimeout(() => {
if (newSpeed > 300) {
console.log("DamagePlugin", "速度實在太快,車子快散架了。。。");
}
done();
}, 2000);
});
accelerate.callAsync(500, () => {
console.log("任務全部完成");
console.timeEnd("total time"); // 記錄總共耗時
});
上面代碼需要注意的是,註冊回調要使用tapAsync
,而且回調函數里面最後一個參數會自動傳入done
,你可以調用他來通知tapable
當前任務已經完成。觸發任務需要使用callAsync
,他最後也接收一個函數,可以用來處理所有任務都完成後需要執行的操作。所以上面的運行結果就是:
image-20210309171527773
從這個結果可以看出,最終消耗的時間大概是 2 秒,也就是三個任務中最長的單個任務耗時,而不是三個任務耗時的總額,這就實現了Parallel
並行的效果。
tapPromise 和 promise
現在都流行Promise
,所以tapable
也是支持的,執行效果是一樣的,只是寫法不一樣而已。要用tapPromise
,需要註冊的回調返回一個promise
,同時觸發事件也需要用promise
,任務運行完執行的處理可以直接使用then
,所以上述代碼改爲:
const { AsyncParallelHook } = require("tapable");
const accelerate = new AsyncParallelHook(["newSpeed"]);
console.time("total time"); // 記錄起始時間
// 注意註冊異步事件需要使用tapPromise
// 回調函數要返回一個promise
accelerate.tapPromise("LoggerPlugin", (newSpeed) => {
return new Promise((resolve) => {
// 1秒後加速才完成
setTimeout(() => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
resolve();
}, 1000);
});
});
accelerate.tapPromise("OverspeedPlugin", (newSpeed) => {
return new Promise((resolve) => {
// 2秒後檢測是否超速
setTimeout(() => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速!!");
}
resolve();
}, 2000);
});
});
accelerate.tapPromise("DamagePlugin", (newSpeed) => {
return new Promise((resolve) => {
// 2秒後檢測是否損壞
setTimeout(() => {
if (newSpeed > 300) {
console.log("DamagePlugin", "速度實在太快,車子快散架了。。。");
}
resolve();
}, 2000);
});
});
// 觸發事件使用promise,直接用then處理最後的結果
accelerate.promise(500).then(() => {
console.log("任務全部完成");
console.timeEnd("total time"); // 記錄總共耗時
});
這段代碼的邏輯和運行結果和上面那個是一樣的,只是寫法不一樣:
image-20210309172537951
tapAsync 和 tapPromise 混用
既然tapable
支持這兩種異步寫法,那這兩種寫法可以混用嗎?我們來試試吧:
const { AsyncParallelHook } = require("tapable");
const accelerate = new AsyncParallelHook(["newSpeed"]);
console.time("total time"); // 記錄起始時間
// 來一個promise寫法
accelerate.tapPromise("LoggerPlugin", (newSpeed) => {
return new Promise((resolve) => {
// 1秒後加速才完成
setTimeout(() => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
resolve();
}, 1000);
});
});
// 再來一個async寫法
accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
// 2秒後檢測是否超速
setTimeout(() => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速!!");
}
done();
}, 2000);
});
// 使用promise觸發事件
// accelerate.promise(500).then(() => {
// console.log("任務全部完成");
// console.timeEnd("total time"); // 記錄總共耗時
// });
// 使用callAsync觸發事件
accelerate.callAsync(500, () => {
console.log("任務全部完成");
console.timeEnd("total time"); // 記錄總共耗時
});
這段代碼無論我是使用promise
觸發事件還是callAsync
觸發運行的結果都是一樣的,所以tapable
內部應該是做了兼容轉換的,兩種寫法可以混用:
image-20210309173217034
由於tapAsync
和tapPromise
只是寫法上的不一樣,我後面的例子就全部用tapAsync
了。
AsyncParallelBailHook
前面已經看了SyncBailHook
,知道帶Bail
的功能就是當一個任務返回不爲undefined
的時候,阻斷後面任務的執行。但是由於Parallel
任務都是同時開始的,阻斷是阻斷不了了,實際效果是如果有一個任務返回了不爲undefined
的值,最終的回調會立即執行,並且獲取Bail
任務的返回值。我們將上面三個任務執行時間錯開,分別爲 1 秒,2 秒,3 秒,然後在 2 秒的任務觸發Bail
就能看到效果了:
const { AsyncParallelBailHook } = require("tapable");
const accelerate = new AsyncParallelBailHook(["newSpeed"]);
console.time("total time"); // 記錄起始時間
accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
// 1秒後加速才完成
setTimeout(() => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
done();
}, 1000);
});
accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
// 2秒後檢測是否超速
setTimeout(() => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速!!");
}
// 這個任務的done返回一個錯誤
// 注意第一個參數是node回調約定俗成的錯誤
// 第二個參數纔是Bail的返回值
done(null, new Error("您已超速!!"));
}, 2000);
});
accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
// 3秒後檢測是否損壞
setTimeout(() => {
if (newSpeed > 300) {
console.log("DamagePlugin", "速度實在太快,車子快散架了。。。");
}
done();
}, 3000);
});
accelerate.callAsync(500, (error, data) => {
if (data) {
console.log("任務執行出錯:", data);
} else {
console.log("任務全部完成");
}
console.timeEnd("total time"); // 記錄總共耗時
});
可以看到執行到任務 2 時,由於他返回了一個錯誤,所以最終的回調會立即執行,但是由於任務 3 之前已經同步開始了,所以他自己仍然會運行完,只是已經不影響最終結果了:
image-20210311142451224
AsyncSeriesHook
AsyncSeriesHook
是異步串行hook
,如果有多個任務,這多個任務之間是串行的,但是任務本身卻可能是異步的,下一個任務必須等上一個任務done
了才能開始:
const { AsyncSeriesHook } = require("tapable");
const accelerate = new AsyncSeriesHook(["newSpeed"]);
console.time("total time"); // 記錄起始時間
accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
// 1秒後加速才完成
setTimeout(() => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
done();
}, 1000);
});
accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
// 2秒後檢測是否超速
setTimeout(() => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速!!");
}
done();
}, 2000);
});
accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
// 2秒後檢測是否損壞
setTimeout(() => {
if (newSpeed > 300) {
console.log("DamagePlugin", "速度實在太快,車子快散架了。。。");
}
done();
}, 2000);
});
accelerate.callAsync(500, () => {
console.log("任務全部完成");
console.timeEnd("total time"); // 記錄總共耗時
});
每個任務代碼跟AsyncParallelHook
是一樣的,只是使用的Hook
不一樣,而最終效果的區別是:AsyncParallelHook
所有任務同時開始,所以最終總耗時就是耗時最長的那個任務的耗時;AsyncSeriesHook
的任務串行執行,下一個任務要等上一個任務完成了才能開始,所以最終總耗時是所有任務耗時的總和,上面這個例子就是1 + 2 + 2
,也就是 5 秒:
image-20210311144738884
AsyncSeriesBailHook
AsyncSeriesBailHook
就是在AsyncSeriesHook
的基礎上加上了Bail
的邏輯,也就是中間任何一個任務返回不爲undefined
的值,終止執行,直接執行最後的回調,並且將這個返回值傳給最終的回調:
const { AsyncSeriesBailHook } = require("tapable");
const accelerate = new AsyncSeriesBailHook(["newSpeed"]);
console.time("total time"); // 記錄起始時間
accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
// 1秒後加速才完成
setTimeout(() => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
done();
}, 1000);
});
accelerate.tapAsync("OverspeedPlugin", (newSpeed, done) => {
// 2秒後檢測是否超速
setTimeout(() => {
if (newSpeed > 120) {
console.log("OverspeedPlugin", "您已超速!!");
}
// 這個任務的done返回一個錯誤
// 注意第一個參數是node回調約定俗成的錯誤
// 第二個參數纔是Bail的返回值
done(null, new Error("您已超速!!"));
}, 2000);
});
accelerate.tapAsync("DamagePlugin", (newSpeed, done) => {
// 2秒後檢測是否損壞
setTimeout(() => {
if (newSpeed > 300) {
console.log("DamagePlugin", "速度實在太快,車子快散架了。。。");
}
done();
}, 2000);
});
accelerate.callAsync(500, (error, data) => {
if (data) {
console.log("任務執行出錯:", data);
} else {
console.log("任務全部完成");
}
console.timeEnd("total time"); // 記錄總共耗時
});
這個執行結果跟AsyncParallelBailHook
的區別就是AsyncSeriesBailHook
被阻斷後,後面的任務由於還沒開始,所以可以被完全阻斷,而AsyncParallelBailHook
後面的任務由於已經開始了,所以還會繼續執行,只是結果已經不關心了。
image-20210311145241190
AsyncSeriesWaterfallHook
Waterfall
的作用是將前一個任務的結果傳給下一個任務,其他的跟AsyncSeriesHook
一樣的,直接來看代碼吧:
const { AsyncSeriesWaterfallHook } = require("tapable");
const accelerate = new AsyncSeriesWaterfallHook(["newSpeed"]);
console.time("total time"); // 記錄起始時間
accelerate.tapAsync("LoggerPlugin", (newSpeed, done) => {
// 1秒後加速才完成
setTimeout(() => {
console.log("LoggerPlugin", `加速到 ${newSpeed}`);
// 注意done的第一個參數會被當做error
// 第二個參數纔是傳遞給後面任務的參數
done(null, "LoggerPlugin");
}, 1000);
});
accelerate.tapAsync("Plugin2", (data, done) => {
setTimeout(() => {
console.log(`上一個插件是: ${data}`);
done(null, "Plugin2");
}, 2000);
});
accelerate.tapAsync("Plugin3", (data, done) => {
setTimeout(() => {
console.log(`上一個插件是: ${data}`);
done(null, "Plugin3");
}, 2000);
});
accelerate.callAsync(500, (error, data) => {
console.log("最後一個插件是:", data);
console.timeEnd("total time"); // 記錄總共耗時
});
運行效果如下:
image-20210311150510851
總結
本文例子已經全部上傳到 GitHub,大家可以拿下來做個參考:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage[8]
-
tapable
是webpack
實現plugin
的核心庫,他爲webpack
提供了多種事件處理和流程控制的Hook
。 -
這些
Hook
主要有同步 (Sync
) 和異步 (Async
) 兩種,同時還提供了阻斷 (Bail
),瀑布 (Waterfall
),循環 (Loop
) 等流程控制,對於異步流程還提供了並行 (Paralle
) 和串行 (Series
) 兩種控制方式。 -
tapable
其核心原理還是事件的發佈訂閱模式
,他使用tap
來註冊事件,使用call
來觸發事件。 -
異步
hook
支持兩種寫法:回調和Promise
,註冊和觸發事件分別使用tapAsync/callAsync
和tapPromise/promise
。 -
異步
hook
使用回調寫法的時候要注意,回調函數的第一個參數默認是錯誤,第二個參數纔是向外傳遞的數據,這也符合node
回調的風格。
參考資料
[1]
webpack 基本原理和 AST 用法的文章: https://juejin.cn/post/6930877602840182791
[2]
tapable: https://github.com/webpack/tapable
[3]
redux
的subscribe
和dispatch
: https://juejin.cn/post/6845166891682512909
[4]
Node.js
的EventEmitter
: https://juejin.cn/post/6844904101331877895
[5]
redux-saga
的take
和put
: https://juejin.cn/post/6885223002703822855
[6]
https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage: https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage
[7]
webpack 官方文檔中對於 plugin 的介紹非常像了: https://www.webpackjs.com/concepts/plugins/#%E5%89%96%E6%9E%90
[8]
https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage: https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-usage
[9]
進擊的大前端: https://test-dennis.oss-cn-hangzhou.aliyuncs.com/QRCode/QR430.jpg
[10]
https://juejin.im/post/5e3ffc85518825494e2772fd: https://juejin.im/post/5e3ffc85518825494e2772fd
[11]
https://github.com/dennis-jiang/Front-End-Knowledges: https://github.com/dennis-jiang/Front-End-Knowledges
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/M588PKlRj6YYu7O6D4ff2w