1. 程式人生 > >webpack核心模組tapable原始碼解析

webpack核心模組tapable原始碼解析

[上一篇文章我寫了`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