webpack核心模組tapable原始碼解析
阿新 • • 發佈:2021-04-01
[上一篇文章我寫了`tapable`的基本用法](https://www.cnblogs.com/dennisj/p/14538668.html),我們知道他是一個增強版版的`釋出訂閱模式`,本文想來學習下他的原始碼。`tapable`的原始碼我讀了一下,發現他的抽象程度比較高,直接扎進去反而會讓人云裡霧裡的,所以本文會從最簡單的`SyncHook`和`釋出訂閱模式`入手,再一步一步抽象,慢慢變成他原始碼的樣子。
**本文可執行示例程式碼已經上傳GitHub,大家拿下來一邊玩一邊看文章效果更佳:[https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-source-code](https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-source-code)**。
## `SyncHook`的基本實現
上一篇文章已經講過`SyncHook`的用法了,我這裡就不再展開了,他使用的例子就是這樣子:
```javascript
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.call(500);
```
其實這種用法就是一個最基本的`釋出訂閱模式`,我之前[講釋出訂閱模式的文章](https://www.cnblogs.com/dennisj/p/12559029.html)講過,我們可以仿照那個很快實現一個`SyncHook`:
```javascript
class SyncHook {
constructor(args = []) {
this._args = args; // 接收的引數存下來
this.taps = []; // 一個存回撥的陣列
}
// tap例項方法用來註冊回撥
tap(name, fn) {
// 邏輯很簡單,直接儲存下傳入的回撥引數就行
this.taps.push(fn);
}
// call例項方法用來觸發事件,執行所有回撥
call(...args) {
// 邏輯也很簡單,將註冊的回撥一個一個拿出來執行就行
const tapsLength = this.taps.length;
for(let i = 0; i < tapsLength; i++) {
const fn = this.taps[i];
fn(...args);
}
}
}
```
這段程式碼非常簡單,是一個最基礎的`釋出訂閱模式`,使用方法跟上面是一樣的,將`SyncHook`從`tapable`匯出改為使用我們自己的:
```javascript
// const { SyncHook } = require("tapable");
const { SyncHook } = require("./SyncHook");
```
執行效果是一樣的:
![image-20210323153234354](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0e36aeb7e1fe47bcb7ea3e05b79b8b7f~tplv-k3u1fbpfcp-zoom-1.image)
**注意:** 我們建構函式裡面傳入的`args`並沒有用上,`tapable`主要是用它來動態生成`call`的函式體的,在後面講程式碼工廠的時候會看到。
## `SyncBailHook`的基本實現
再來一個`SyncBailHook`的基本實現吧,`SyncBailHook`的作用是當前一個回撥返回不為`undefined`的值的時候,阻止後面的回撥執行。基本使用是這樣的:
```javascript
const { SyncBailHook } = require("tapable"); // 使用的是SyncBailHook
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('您已超速!!');
}
});
// 由於上一個回撥返回了一個不為undefined的值
// 這個回撥不會再運行了
accelerate.tap("DamagePlugin", (newSpeed) => {
if (newSpeed > 300) {
console.log("DamagePlugin", "速度實在太快,車子快散架了。。。");
}
});
accelerate.call(500);
```
他的實現跟上面的`SyncHook`也非常像,只是`call`在執行的時候不一樣而已,`SyncBailHook`需要檢測每個回撥的返回值,如果不為`undefined`就終止執行後面的回撥,所以程式碼實現如下:
```javascript
class SyncBailHook {
constructor(args = []) {
this._args = args;
this.taps = [];
}
tap(name, fn) {
this.taps.push(fn);
}
// 其他程式碼跟SyncHook是一樣的,就是call的實現不一樣
// 需要檢測每個返回值,如果不為undefined就終止執行
call(...args) {
const tapsLength = this.taps.length;
for(let i = 0; i < tapsLength; i++) {
const fn = this.taps[i];
const res = fn(...args);
if( res !== undefined) return res;
}
}
}
```
然後改下`SyncBailHook`從我們自己的引入就行:
```javascript
// const { SyncBailHook } = require("tapable");
const { SyncBailHook } = require("./SyncBailHook");
```
執行效果是一樣的:
![image-20210323155857678](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ce47227a914a4e6b98602dd525f3257c~tplv-k3u1fbpfcp-zoom-1.image)
## 抽象重複程式碼
現在我們只實現了`SyncHook`和`SyncBailHook`兩個`Hook`而已,上一篇講用法的文章裡面總共有9個`Hook`,如果每個`Hook`都像前面這樣實現也是可以的。但是我們再仔細看下`SyncHook`和`SyncBailHook`兩個類的程式碼,發現他們除了`call`的實現不一樣,其他程式碼一模一樣,所以作為一個有追求的工程師,我們可以把這部分重複的程式碼提出來作為一個基類:`Hook`類。
`Hook`類需要包含一些公共的程式碼,`call`這種不一樣的部分由各個子類自己實現。所以`Hook`類就長這樣:
```javascript
const CALL_DELEGATE = function(...args) {
this.call = this._createCall();
return this.call(...args);
};
// Hook是SyncHook和SyncBailHook的基類
// 大體結構是一樣的,不一樣的地方是call
// 不同子類的call是不一樣的
// tapable的Hook基類提供了一個抽象介面compile來動態生成call函式
class Hook {
constructor(args = []) {
this._args = args;
this.taps = [];
// 基類的call初始化為CALL_DELEGATE
// 為什麼這裡需要這樣一個代理,而不是直接this.call = _createCall()
// 等我們後面子類實現了再一起講
this.call = CALL_DELEGATE;
}
// 一個抽象介面compile
// 由子類實現,基類compile不能直接呼叫
compile(options) {
throw new Error("Abstract: should be overridden");
}
tap(name, fn) {
this.taps.push(fn);
}
// _createCall呼叫子類實現的compile來生成call方法
_createCall() {
return this.compile({
taps: this.taps,
args: this._args,
});
}
}
```
官方對應的原始碼看這裡:[https://github.com/webpack/tapable/blob/master/lib/Hook.js](https://github.com/webpack/tapable/blob/master/lib/Hook.js)
### 子類SyncHook實現
現在有了`Hook`基類,我們的`SyncHook`就需要繼承這個基類重寫,`tapable`在這裡繼承的時候並沒有使用`class extends`,而是手動繼承的:
```javascript
const Hook = require('./Hook');
function SyncHook(args = []) {
// 先手動繼承Hook
const hook = new Hook(args);
hook.constructor = SyncHook;
// 然後實現自己的compile函式
// compile的作用應該是建立一個call函式並返回
hook.compile = function(options) {
// 這裡call函式的實現跟前面實現是一樣的
const { taps } = options;
const call = function(...args) {
const tapsLength = taps.length;
for(let i = 0; i < tapsLength; i++) {
const fn = this.taps[i];
fn(...args);
}
}
return call;
};
return hook;
}
SyncHook.prototype = null;
```
**注意**:我們在基類`Hook`建構函式中初始化`this.call`為`CALL_DELEGATE`這個函式,這是有原因的,最主要的原因是**確保`this`的正確指向**。思考一下假如我們不用`CALL_DELEGATE`,而是直接`this.call = this._createCall()`會發生什麼?我們來分析下這個執行流程:
1. 使用者使用時,肯定是使用`new SyncHook()`,這時候會執行`const hook = new Hook(args);`
2. `new Hook(args)`會去執行`Hook`的建構函式,也就是會執行`this.call = this._createCall()`
3. 這時候的`this`指向的是基類`Hook`的例項,`this._createCall()`會呼叫基類的`this.compile()`
4. 由於基類的`complie`函式是一個抽象介面,直接呼叫會報錯`Abstract: should be overridden`。
**那我們採用`this.call = CALL_DELEGATE`是怎麼解決這個問題的呢**?
1. 採用`this.call = CALL_DELEGATE`後,基類`Hook`上的`call`就只是被賦值為一個代理函式而已,這個函式不會立馬呼叫。
2. 使用者使用時,同樣是`new SyncHook()`,裡面會執行`Hook`的建構函式
3. `Hook`建構函式會給`this.call`賦值為`CALL_DELEGATE`,但是不會立即執行。
4. `new SyncHook()`繼續執行,新建的例項上的方法`hook.complie`被覆寫為正確方法。
5. 當用戶呼叫`hook.call`的時候才會真正執行`this._createCall()`,這裡面會去呼叫`this.complie()`
6. 這時候呼叫的`complie`已經是被正確覆寫過的了,所以得到正確的結果。
### 子類SyncBailHook的實現
子類`SyncBailHook`的實現跟上面`SyncHook`的也是非常像,只是`hook.compile`實現不一樣而已:
```javascript
const Hook = require('./Hook');
function SyncBailHook(args = []) {
// 基本結構跟SyncHook都是一樣的
const hook = new Hook(args);
hook.constructor = SyncBailHook;
// 只是compile的實現是Bail版的
hook.compile = function(options) {
const { taps } = options;
const call = function(...args) {
const tapsLength = taps.length;
for(let i = 0; i < tapsLength; i++) {
const fn = this.taps[i];
const res = fn(...args);
if( res !== undefined) break;
}
}
return call;
};
return hook;
}
SyncBailHook.prototype = null;
```
## 抽象程式碼工廠
上面我們通過對`SyncHook`和`SyncBailHook`的抽象提煉出了一個基類`Hook`,減少了重複程式碼。基於這種結構子類需要實現的就是`complie`方法,但是如果我們將`SyncHook`和`SyncBailHook`的`complie`方法拿出來對比下:
**SyncHook**:
```javascript
hook.compile = function(options) {
const { taps } = options;
const call = function(...args) {
const tapsLength = taps.length;
for(let i = 0; i < tapsLength; i++) {
const fn = this.taps[i];
fn(...args);
}
}
return call;
};
```
**SyncBailHook**:
```javascript
hook.compile = function(options) {
const { taps } = options;
const call = function(...args) {
const tapsLength = taps.length;
for(let i = 0; i < tapsLength; i++) {
const fn = this.taps[i];
const res = fn(...args);
if( res !== undefined) return res;
}
}
return call;
};
```
我們發現這兩個`complie`也非常像,有大量重複程式碼,所以`tapable`為了解決這些重複程式碼,又進行了一次抽象,也就是程式碼工廠`HookCodeFactory`。`HookCodeFactory`的作用就是用來生成`complie`返回的`call`函式體,而`HookCodeFactory`在實現時也採用了`Hook`類似的思路,也是先實現了一個基類`HookCodeFactory`,然後不同的`Hook`再繼承這個類來實現自己的程式碼工廠,比如`SyncHookCodeFactory`。
### 建立函式的方法
在繼續深入程式碼工廠前,我們先來回顧下JS裡面建立函式的方法。一般我們會有這幾種方法:
1. 函式申明
```javascript
function add(a, b) {
return a + b;
}
```
2. 函式表示式
```javascript
const add = function(a, b) {
return a + b;
}
```
但是除了這兩種方法外,還有種不常用的方法:**使用Function建構函式**。比如上面這個函式使用建構函式建立就是這樣的:
```javascript
const add = new Function('a', 'b', 'return a + b;');
```
上面的呼叫形式裡,最後一個引數是函式的函式體,前面的引數都是函式的形參,最終生成的函式跟用函式表示式的效果是一樣的,可以這樣呼叫:
```javascript
add(1, 2); // 結果是3
```
**注意**:上面的`a`和`b`形參放在一起用逗號隔開也是可以的:
```javascript
const add = new Function('a, b', 'return a + b;'); // 這樣跟上面的效果是一樣的
```
當然函式並不是一定要有引數,沒有引數的函式也可以這樣建立:
```javascript
const sayHi = new Function('alert("Hello")');
sayHi(); // Hello
```
這樣建立函式和前面的函式申明和函式表示式有什麼區別呢?**使用Function建構函式來建立函式最大的一個特徵就是,函式體是一個字串,也就是說我們可以動態生成這個字串,從而動態生成函式體**。因為`SyncHook`和`SyncBailHook`的`call`函式很像,我們可以像拼一個字串那樣拼出他們的函式體,為了更簡單的拼湊,`tapable`最終生成的`call`函式裡面並沒有迴圈,而是在拼函式體的時候就將迴圈展開了,比如`SyncHook`拼出來的`call`函式的函式體就是這樣的:
```javascript
"use strict";
var _x = this._x;
var _fn0 = _x[0];
_fn0(newSpeed);
var _fn1 = _x[1];
_fn1(newSpeed);
```
上面程式碼的`_x`其實就是儲存回撥的陣列`taps`,這裡重新命名為`_x`,我想是為了節省程式碼大小吧。這段程式碼可以看到,`_x`,也就是`taps`裡面的內容已經被展開了,是一個一個取出來執行的。
而`SyncBailHook`最終生成的`call`函式體是這樣的:
```javascript
"use strict";
var _x = this._x;
var _fn0 = _x[0];
var _result0 = _fn0(newSpeed);
if (_result0 !== undefined) {
return _result0;
;
} else {
var _fn1 = _x[1];
var _result1 = _fn1(newSpeed);
if (_result1 !== undefined) {
return _result1;
;
} else {
}
}
```
這段生成的程式碼主體邏輯其實跟`SyncHook`是一樣的,都是將`_x`展開執行了,他們的區別是`SyncBailHook`會對每次執行的結果進行檢測,如果結果不是`undefined`就直接`return`了,後面的回撥函式就沒有機會執行了。
### 建立程式碼工廠基類
基於這個目的,我們的程式碼工廠基類應該可以生成最基本的`call`函式體。我們來寫個最基本的`HookCodeFactory`吧,目前他只能生成`SyncHook`的`call`函式體:
```javascript
class HookCodeFactory {
constructor() {
// 建構函式定義兩個變數
this.options = undefined;
this._args = undefined;
}
// init函式初始化變數
init(options) {
this.options = options;
this._args = options.args.slice();
}
// deinit重置變數
deinit() {
this.options = undefined;
this._args = undefined;
}
// args用來將傳入的陣列args轉換為New Function接收的逗號分隔的形式
// ['arg1', 'args'] ---> 'arg1, arg2'
args() {
return this._args.join(", ");
}
// setup其實就是給生成程式碼的_x賦值
setup(instance, options) {
instance._x = options.taps.map(t => t);
}
// create建立最終的call函式
create(options) {
this.init(options);
let fn;
// 直接將taps展開為平鋪的函式呼叫
const { taps } = options;
let code = '';
for (let i = 0; i < taps.length; i++) {
code += `
var _fn${i} = _x[${i}];
_fn${i}(${this.args()});
`
}
// 將展開的迴圈和頭部連線起來
const allCodes = `
"use strict";
var _x = this._x;
` + code;
// 用傳進來的引數和生成的函式體建立一個函數出來
fn = new Function(this.args(), allCodes);
this.deinit(); // 重置變數
return fn; // 返回生成的函式
}
}
```
上面程式碼最核心的其實就是`create`函式,這個函式會動態建立一個`call`函式並返回,所以`SyncHook`可以直接使用這個`factory`建立程式碼了:
```javascript
// SyncHook.js
const Hook = require('./Hook');
const HookCodeFactory = require("./HookCodeFactory");
const factory = new HookCodeFactory();
// COMPILE函式會去呼叫factory來生成call函式
const COMPILE = function(options) {
factory.setup(this, options);
return factory.create(options);
};
function SyncHook(args = []) {
const hook = new Hook(args);
hook.constructor = SyncHook;
// 使用HookCodeFactory來建立最終的call函式
hook.compile = COMPILE;
return hook;
}
SyncHook.prototype = null;
```
### 讓程式碼工廠支援`SyncBailHook`
現在我們的`HookCodeFactory`只能生成最簡單的`SyncHook`程式碼,我們需要對他進行一些改進,讓他能夠也生成`SyncBailHook`的`call`函式體。你可以拉回前面再仔細觀察下這兩個最終生成程式碼的區別:
1. `SyncBailHook`需要對每次執行的`result`進行處理,如果不為`undefined`就返回
2. `SyncBailHook`生成的程式碼其實是`if...else`巢狀的,我們生成的時候可以考慮使用一個遞迴函式
為了讓`SyncHook`和`SyncBailHook`的子類程式碼工廠能夠傳入差異化的`result`處理,我們先將`HookCodeFactory`基類的`create`拆成兩部分,將程式碼拼裝的邏輯單獨拆成一個函式:
```javascript
class HookCodeFactory {
// ...
// 省略其他一樣的程式碼
// ...
// create建立最終的call函式
create(options) {
this.init(options);
let fn;
// 拼裝程式碼頭部
const header = `
"use strict";
var _x = this._x;
`;
// 用傳進來的引數和函式體建立一個函數出來
fn = new Function(this.args(),
header +
this.content()); // 注意這裡的content函式並沒有在基類HookCodeFactory實現,而是子類實現的
this.deinit();
return fn;
}
// 拼裝函式體
// callTapsSeries也沒在基類呼叫,而是子類呼叫的
callTapsSeries() {
const { taps } = this.options;
let code = '';
for (let i = 0; i < taps.length; i++) {
code += `
var _fn${i} = _x[${i}];
_fn${i}(${this.args()});
`
}
return code;
}
}
```
**上面程式碼裡面要特別注意`create`函式裡面生成函式體的時候呼叫的是`this.content`,但是`this.content`並沒與在基類實現,這要求子類在使用`HookCodeFactory`的時候都需要繼承他並實現自己的`content`函式,所以這裡的`content`函式也是一個抽象介面。那`SyncHook`的程式碼就應該改成這樣:**
```javascript
// SyncHook.js
// ... 省略其他一樣的程式碼 ...
// SyncHookCodeFactory繼承HookCodeFactory並實現content函式
class SyncHookCodeFactory extends HookCodeFactory {
content() {
return this.callTapsSeries(); // 這裡的callTapsSeries是基類的
}
}
// 使用SyncHookCodeFactory來建立factory
const factory = new SyncHookCodeFactory();
const COMPILE = function (options) {
factory.setup(this, options);
return factory.create(options);
};
```
**注意這裡:**子類實現的`content`其實又呼叫了基類的`callTapsSeries`來生成最終的函式體。所以這裡這幾個函式的呼叫關係其實是這樣的:
![image-20210401111739814](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8783fc54c4dd4725b2fcb0c200ef8d6b~tplv-k3u1fbpfcp-zoom-1.image)
**那這樣設計的目的是什麼呢**?**為了讓子類`content`能夠傳遞引數給基類`callTapsSeries`,從而生成不一樣的函式體**。我們馬上就能在`SyncBailHook`的程式碼工廠上看到了。
為了能夠生成`SyncBailHook`的函式體,我們需要讓`callTapsSeries`支援一個`onResult`引數,就是這樣:
```javascript
class HookCodeFactory {
// ... 省略其他相同的程式碼 ...
// 拼裝函式體,需要支援options.onResult引數
callTapsSeries(options) {
const { taps } = this.options;
let code = '';
let i = 0;
const onResult = options && options.onResult;
// 寫一個next函式來開啟有onResult回撥的函式體生成
// next和onResult相互遞迴呼叫來生成最終的函式體
const next = () => {
if(i >= taps.length) return '';
const result = `_result${i}`;
const code = `
var _fn${i} = _x[${i}];
var ${result} = _fn${i}(${this.args()});
${onResult(i++, result, next)}
`;
return code;
}
// 支援onResult引數
if(onResult) {
code = next();
} else {
// 沒有onResult引數的時候,即SyncHook跟之前保持一樣
for(; i< taps.length; i++) {
code += `
var _fn${i} = _x[${i}];
_fn${i}(${this.args()});
`
}
}
return code;
}
}
```
然後我們的`SyncBailHook`的程式碼工廠在繼承工廠基類的時候需要傳一個`onResult`引數,就是這樣:
```javascript
const Hook = require('./Hook');
const HookCodeFactory = require("./HookCodeFactory");
// SyncBailHookCodeFactory繼承HookCodeFactory並實現content函式
// content裡面傳入定製的onResult函式,onResult回去呼叫next遞迴生成巢狀的if...else...
class SyncBailHookCodeFactory extends HookCodeFactory {
content() {
return this.callTapsSeries({
onResult: (i, result, next) =>
`if(${result} !== undefined) {\nreturn ${result};\n} else {\n${next()}}\n`,
});
}
}
// 使用SyncHookCodeFactory來建立factory
const factory = new SyncBailHookCodeFactory();
const COMPILE = function (options) {
factory.setup(this, options);
return factory.create(options);
};
function SyncBailHook(args = []) {
// 基本結構跟SyncHook都是一樣的
const hook = new Hook(args);
hook.constructor = SyncBailHook;
// 使用HookCodeFactory來建立最終的call函式
hook.compile = COMPILE;
return hook;
}
```
現在執行下程式碼,效果跟之前一樣的,大功告成~
## 其他Hook的實現
到這裡,`tapable`的原始碼架構和基本實現我們已經弄清楚了,但是本文只用了`SyncHook`和`SyncBailHook`做例子,其他的,比如`AsyncParallelHook`並沒有展開講。因為`AsyncParallelHook`之類的其他`Hook`的實現思路跟本文是一樣的,比如我們可以先實現一個獨立的`AsyncParallelHook`類:
```javascript
class AsyncParallelHook {
constructor(args = []) {
this._args = args;
this.taps = [];
}
tapAsync(name, task) {
this.taps.push(task);
}
callAsync(...args) {
// 先取出最後傳入的回撥函式
let finalCallback = args.pop();
// 定義一個 i 變數和 done 函式,每次執行檢測 i 值和佇列長度,決定是否執行 callAsync 的最終回撥函式
let i = 0;
let done = () => {
if (++i === this.taps.length) {
finalCallback();
}
};
// 依次執行事件處理函式
this.taps.forEach(task => task(...args, done));
}
}
```
然後對他的`callAsync`函式進行抽象,將其抽象到程式碼工廠類裡面,使用字串拼接的方式動態構造出來就行了,整體思路跟前面是一樣的。具體實現過程可以參考`tapable`原始碼:
[Hook類原始碼](https://github.com/webpack/tapable/blob/v2.2.0/lib/Hook.js)
[SyncHook類原始碼](https://github.com/webpack/tapable/blob/v2.2.0/lib/SyncHook.js)
[SyncBailHook類原始碼](https://github.com/webpack/tapable/blob/v2.2.0/lib/SyncBailHook.js)
[HookCodeFactory類原始碼](https://github.com/webpack/tapable/blob/v2.2.0/lib/HookCodeFactory.js)
## 總結
**本文可執行示例程式碼已經上傳GitHub,大家拿下來一邊玩一邊看文章效果更佳:[https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-source-code](https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/tapable-source-code)**。
下面再對本文的思路進行一個總結:
1. `tapable`的各種`Hook`其實都是基於釋出訂閱模式。
2. 各個`Hook`自己獨立實現其實也沒有問題,但是因為都是釋出訂閱模式,會有大量重複程式碼,所以`tapable`進行了幾次抽象。
3. 第一次抽象是提取一個`Hook`基類,這個基類實現了初始化和事件註冊等公共部分,至於每個`Hook`的`call`都不一樣,需要自己實現。
4. 第二次抽象是每個`Hook`在實現自己的`call`的時候,發現程式碼也有很多相似之處,所以提取了一個程式碼工廠,用來動態生成`call`的函式體。
5. 總體來說,`tapable`的程式碼並不難,但是因為有兩次抽象,整個程式碼架構顯得不那麼好讀,經過本文的梳理後,應該會好很多了。
**文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支援是作者持續創作的動力。**
**歡迎關注我的公眾號[進擊的大前端](https://test-dennis.oss-cn-hangzhou.aliyuncs.com/QRCode/QR430.jpg)第一時間獲取高質量原創~**
**“前端進階知識”系列文章:[https://juejin.im/post/5e3ffc85518825494e2772fd](https://juejin.im/post/5e3ffc85518825494e2772fd)**
**“前端進階知識”系列文章原始碼GitHub地址: [https://github.com/dennis-jiang/Front-End-Knowledges](https://github.com/dennis-jiang/Front-End-Knowledges)**
![QR1270](https://test-dennis.oss-cn-hangzhou.aliyuncs.com/QRCode/QR1270.png)
## 參考資料
`tapable`用法介紹:[https://www.cnblogs.com/dennisj/p/14538668.html](https://www.cnblogs.com/dennisj/p/14538668.html)
`tapable`原始碼地址:[https://github.com/webpack/tapable](https://github.com/webpack/t