摘要:系列文章系列第一篇基礎雜記系列第二篇插件機制雜記系列第三篇流程雜記前言本身并不難,他所完成的各種復雜炫酷的功能都依賴于他的插件機制。的插件機制依賴于一個核心的庫,。是什么是一個類似于的的庫主要是控制鉤子函數的發布與訂閱。
系列文章
Webpack系列-第一篇基礎雜記
Webpack系列-第二篇插件機制雜記
Webpack系列-第三篇流程雜記
webpack本身并不難,他所完成的各種復雜炫酷的功能都依賴于他的插件機制。或許我們在日常的開發需求中并不需要自己動手寫一個插件,然而,了解其中的機制也是一種學習的方向,當插件出現問題時,我們也能夠自己來定位。
TapableWebpack的插件機制依賴于一個核心的庫, Tapable。
在深入webpack的插件機制之前,需要對該核心庫有一定的了解。
tapable 是一個類似于nodejs 的EventEmitter 的庫, 主要是控制鉤子函數的發布與訂閱。當然,tapable提供的hook機制比較全面,分為同步和異步兩個大類(異步中又區分異步并行和異步串行),而根據事件執行的終止條件的不同,由衍生出 Bail/Waterfall/Loop 類型。
Tapable的使用 (該小段內容引用文章)基本使用:
const { SyncHook } = require("tapable") // 創建一個同步 Hook,指定參數 const hook = new SyncHook(["arg1", "arg2"]) // 注冊 hook.tap("a", function (arg1, arg2) { console.log("a") }) hook.tap("b", function (arg1, arg2) { console.log("b") }) hook.call(1, 2)
鉤子類型:
BasicHook:執行每一個,不關心函數的返回值,有SyncHook、AsyncParallelHook、AsyncSeriesHook。
BailHook:順序執行 Hook,遇到第一個結果result!==undefined則返回,不再繼續執行。有:SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。
什么樣的場景下會使用到 BailHook 呢?設想如下一個例子:假設我們有一個模塊 M,如果它滿足 A 或者 B 或者 C 三者任何一個條件,就將其打包為一個多帶帶的。這里的 A、B、C 不存在先后順序,那么就可以使用 AsyncParallelBailHook 來解決:
x.hooks.拆分模塊的Hook.tap("A", () => { if (A 判斷條件滿足) { return true } }) x.hooks.拆分模塊的Hook.tap("B", () => { if (B 判斷條件滿足) { return true } }) x.hooks.拆分模塊的Hook.tap("C", () => { if (C 判斷條件滿足) { return true } })
如果 A 中返回為 true,那么就無須再去判斷 B 和 C。
但是當 A、B、C 的校驗,需要嚴格遵循先后順序時,就需要使用有順序的 SyncBailHook(A、B、C 是同步函數時使用) 或者 AsyncSeriseBailHook(A、B、C 是異步函數時使用)。
WaterfallHook:類似于 reduce,如果前一個 Hook 函數的結果 result !== undefined,則 result 會作為后一個 Hook 函數的第一個參數。既然是順序執行,那么就只有 Sync 和 AsyncSeries 類中提供這個Hook:SyncWaterfallHook,AsyncSeriesWaterfallHook
當一個數據,需要經過 A,B,C 三個階段的處理得到最終結果,并且 A 中如果滿足條件 a 就處理,否則不處理,B 和 C 同樣,那么可以使用如下
x.hooks.tap("A", (data) => { if (滿足 A 需要處理的條件) { // 處理數據 data return data } else { return } }) x.hooks.tap("B", (data) => { if (滿足B需要處理的條件) { // 處理數據 data return data } else { return } }) x.hooks.tap("C", (data) => { if (滿足 C 需要處理的條件) { // 處理數據 data return data } else { return } })
LoopHook:不停的循環執行 Hook,直到所有函數結果 result === undefined。同樣的,由于對串行性有依賴,所以只有 SyncLoopHook 和 AsyncSeriseLoopHook (PS:暫時沒看到具體使用 Case)
Tapable的源碼分析Tapable 基本邏輯是,先通過類實例的 tap 方法注冊對應 Hook 的處理函數, 這里直接分析sync同步鉤子的主要流程,其他的異步鉤子和攔截器等就不贅述了。
const hook = new SyncHook(["arg1", "arg2"])
從該句代碼, 作為源碼分析的入口,
class SyncHook extends Hook { // 錯誤處理,防止調用者調用異步鉤子 tapAsync() { throw new Error("tapAsync is not supported on a SyncHook"); } // 錯誤處理,防止調用者調用promise鉤子 tapPromise() { throw new Error("tapPromise is not supported on a SyncHook"); } // 核心實現 compile(options) { factory.setup(this, options); return factory.create(options); } }
從類SyncHook看到, 他是繼承于一個基類Hook, 他的核心實現compile等會再講, 我們先看看基類Hook
// 變量的初始化 constructor(args) { if (!Array.isArray(args)) args = []; this._args = args; this.taps = []; this.interceptors = []; this.call = this._call; this.promise = this._promise; this.callAsync = this._callAsync; this._x = undefined; }
初始化完成后, 通常會注冊一個事件, 如:
// 注冊 hook.tap("a", function (arg1, arg2) { console.log("a") }) hook.tap("b", function (arg1, arg2) { console.log("b") })
很明顯, 這兩個語句都會調用基類中的tap方法:
tap(options, fn) { // 參數處理 if (typeof options === "string") options = { name: options }; if (typeof options !== "object" || options === null) throw new Error( "Invalid arguments to tap(options: Object, fn: function)" ); options = Object.assign({ type: "sync", fn: fn }, options); if (typeof options.name !== "string" || options.name === "") throw new Error("Missing name for tap"); // 執行攔截器的register函數, 比較簡單不分析 options = this._runRegisterInterceptors(options); // 處理注冊事件 this._insert(options); }
從上面的源碼分析, 可以看到_insert方法是注冊階段的關鍵函數, 直接進入該方法內部
_insert(item) { // 重置所有的 調用 方法 this._resetCompilation(); // 將注冊事件排序后放進taps數組 let before; if (typeof item.before === "string") before = new Set([item.before]); else if (Array.isArray(item.before)) { before = new Set(item.before); } let stage = 0; if (typeof item.stage === "number") stage = item.stage; let i = this.taps.length; while (i > 0) { i--; const x = this.taps[i]; this.taps[i + 1] = x; const xStage = x.stage || 0; if (before) { if (before.has(x.name)) { before.delete(x.name); continue; } if (before.size > 0) { continue; } } if (xStage > stage) { continue; } i++; break; } this.taps[i] = item; } }
_insert主要是排序tap并放入到taps數組里面, 排序的算法并不是特別復雜,這里就不贅述了, 到了這里, 注冊階段就已經結束了, 繼續看觸發階段。
hook.call(1, 2) // 觸發函數
在基類hook中, 有一個初始化過程,
this.call = this._call; Object.defineProperties(Hook.prototype, { _call: { value: createCompileDelegate("call", "sync"), configurable: true, writable: true }, _promise: { value: createCompileDelegate("promise", "promise"), configurable: true, writable: true }, _callAsync: { value: createCompileDelegate("callAsync", "async"), configurable: true, writable: true } });
我們可以看出_call是由createCompileDelegate生成的, 往下看
function createCompileDelegate(name, type) { return function lazyCompileHook(...args) { this[name] = this._createCall(type); return this[name](...args); }; }
createCompileDelegate返回一個名為lazyCompileHook的函數,顧名思義,即懶編譯, 直到調用call的時候, 才會編譯出正在的call函數。
createCompileDelegate也是調用的_createCall, 而_createCall調用了Compier函數
_createCall(type) { return this.compile({ taps: this.taps, interceptors: this.interceptors, args: this._args, type: type }); } compile(options) { throw new Error("Abstract: should be overriden"); }
可以看到compiler必須由子類重寫, 返回到syncHook的compile函數, 即我們一開始說的核心方法
class SyncHookCodeFactory extends HookCodeFactory { content({ onError, onResult, onDone, rethrowIfPossible }) { return this.callTapsSeries({ onError: (i, err) => onError(err), onDone, rethrowIfPossible }); } } const factory = new SyncHookCodeFactory(); class SyncHook extends Hook { ... compile(options) { factory.setup(this, options); return factory.create(options); } }
關鍵就在于SyncHookCodeFactory和工廠類HookCodeFactory, 先看setup函數,
setup(instance, options) { // 這里的instance 是syncHook 實例, 其實就是把tap進來的鉤子數組給到鉤子的_x屬性里. instance._x = options.taps.map(t => t.fn); }
然后是最關鍵的create函數, 可以看到最后返回的fn,其實是一個new Function動態生成的函數
create(options) { // 初始化參數,保存options到本對象this.options,保存new Hook(["options"]) 傳入的參數到 this._args this.init(options); let fn; // 動態構建鉤子,這里是抽象層,分同步, 異步, promise switch (this.options.type) { // 先看同步 case "sync": // 動態返回一個鉤子函數 fn = new Function( // 生成函數的參數,no before no after 返回參數字符串 xxx,xxx 在 // 注意這里this.args返回的是一個字符串, // 在這個例子中是options this.args(), ""use strict"; " + this.header() + this.content({ onError: err => `throw ${err}; `, onResult: result => `return ${result}; `, onDone: () => "", rethrowIfPossible: true }) ); break; case "async": fn = new Function( this.args({ after: "_callback" }), ""use strict"; " + this.header() + // 這個 content 調用的是子類類的 content 函數, // 參數由子類傳,實際返回的是 this.callTapsSeries() 返回的類容 this.content({ onError: err => `_callback(${err}); `, onResult: result => `_callback(null, ${result}); `, onDone: () => "_callback(); " }) ); break; case "promise": let code = ""; code += ""use strict"; "; code += "return new Promise((_resolve, _reject) => { "; code += "var _sync = true; "; code += this.header(); code += this.content({ onError: err => { let code = ""; code += "if(_sync) "; code += `_resolve(Promise.resolve().then(() => { throw ${err}; })); `; code += "else "; code += `_reject(${err}); `; return code; }, onResult: result => `_resolve(${result}); `, onDone: () => "_resolve(); " }); code += "_sync = false; "; code += "}); "; fn = new Function(this.args(), code); break; } // 把剛才init賦的值初始化為undefined // this.options = undefined; // this._args = undefined; this.deinit(); return fn; }
最后生成的代碼大致如下, 參考文章
"use strict"; function (options) { var _context; var _x = this._x; var _taps = this.taps; var _interterceptors = this.interceptors; // 我們只有一個攔截器所以下面的只會生成一個 _interceptors[0].call(options); var _tap0 = _taps[0]; _interceptors[0].tap(_tap0); var _fn0 = _x[0]; _fn0(options); var _tap1 = _taps[1]; _interceptors[1].tap(_tap1); var _fn1 = _x[1]; _fn1(options); var _tap2 = _taps[2]; _interceptors[2].tap(_tap2); var _fn2 = _x[2]; _fn2(options); var _tap3 = _taps[3]; _interceptors[3].tap(_tap3); var _fn3 = _x[3]; _fn3(options); }
ok, 以上就是Tapabled的機制, 然而本篇的主要對象其實是基于tapable實現的compile和compilation對象。不過由于他們都是基于tapable,所以介紹的篇幅相對短一點。
compile compile是什么compiler 對象代表了完整的 webpack 環境配置。這個對象在啟動 webpack 時被一次性建立,并配置好所有可操作的設置,包括 options,loader 和 plugin。當在 webpack 環境中應用一個插件時,插件將收到此 compiler 對象的引用。可以使用 compiler 來訪問 webpack 的主環境。
也就是說, compile是webpack的整體環境。
compile的內部實現class Compiler extends Tapable { constructor(context) { super(); this.hooks = { /** @type {SyncBailHook} */ shouldEmit: new SyncBailHook(["compilation"]), /** @type {AsyncSeriesHook } */ done: new AsyncSeriesHook(["stats"]), /** @type {AsyncSeriesHook<>} */ additionalPass: new AsyncSeriesHook([]), /** @type {AsyncSeriesHook } */ ...... ...... some code }; ...... ...... some code }
可以看到, Compier繼承了Tapable, 并且在實例上綁定了一個hook對象, 使得Compier的實例compier可以像這樣使用
compiler.hooks.compile.tapAsync( "afterCompile", (compilation, callback) => { console.log("This is an example plugin!"); console.log("Here’s the `compilation` object which represents a single build of assets:", compilation); // 使用 webpack 提供的 plugin API 操作構建結果 compilation.addModule(/* ... */); callback(); } );compilation 什么是compilation
compilation 對象代表了一次資源版本構建。當運行 webpack 開發環境中間件時,每當檢測到一個文件變化,就會創建一個新的 compilation,從而生成一組新的編譯資源。一個 compilation 對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息。compilation 對象也提供了很多關鍵時機的回調,以供插件做自定義處理時選擇使用。compilation的實現
class Compilation extends Tapable { /** * Creates an instance of Compilation. * @param {Compiler} compiler the compiler which created the compilation */ constructor(compiler) { super(); this.hooks = { /** @type {SyncHook} */ buildModule: new SyncHook(["module"]), /** @type {SyncHook } */ rebuildModule: new SyncHook(["module"]), /** @type {SyncHook } */ failedModule: new SyncHook(["module", "error"]), /** @type {SyncHook } */ succeedModule: new SyncHook(["module"]), /** @type {SyncHook } */ addEntry: new SyncHook(["entry", "name"]), /** @type {SyncHook } */ } } }
具體參考上面提到的compiler實現。
編寫一個插件了解到tapablecompilercompilation之后, 再來看插件的實現就不再一頭霧水了
以下代碼源自官方文檔
class MyExampleWebpackPlugin { // 定義 `apply` 方法 apply(compiler) { // 指定要追加的事件鉤子函數 compiler.hooks.compile.tapAsync( "afterCompile", (compilation, callback) => { console.log("This is an example plugin!"); console.log("Here’s the `compilation` object which represents a single build of assets:", compilation); // 使用 webpack 提供的 plugin API 操作構建結果 compilation.addModule(/* ... */); callback(); } ); } }
可以看到其實就是在apply中傳入一個Compiler實例, 然后基于該實例注冊事件, compilation同理, 最后webpack會在各流程執行call方法。
compiler和compilation一些比較重要的事件鉤子 compier事件鉤子 | 觸發時機 | 參數 | 類型 |
---|---|---|---|
entry-option | 初始化 option | - | SyncBailHook |
run | 開始編譯 | compiler | AsyncSeriesHook |
compile | 真正開始的編譯,在創建 compilation 對象之前 | compilation | SyncHook |
compilation | 生成好了 compilation 對象,可以操作這個對象啦 | compilation | SyncHook |
make | 從 entry 開始遞歸分析依賴,準備對每個模塊進行 build | compilation | AsyncParallelHook |
after-compile | 編譯 build 過程結束 | compilation | AsyncSeriesHook |
emit | 在將內存中 assets 內容寫到磁盤文件夾之前 | compilation | AsyncSeriesHook |
after-emit | 在將內存中 assets 內容寫到磁盤文件夾之后 | compilation | AsyncSeriesHook |
done | 完成所有的編譯過程 | stats | AsyncSeriesHook |
failed | 編譯失敗的時候 | error | SyncHook |
事件鉤子 | 觸發時機 | 參數 | 類型 |
---|---|---|---|
normal-module-loader | 普通模塊 loader,真正(一個接一個地)加載模塊圖(graph)中所有模塊的函數。 | loaderContext module | SyncHook |
seal | 編譯(compilation)停止接收新模塊時觸發。 | - | SyncHook |
optimize | 優化階段開始時觸發。 | - | SyncHook |
optimize-modules | 模塊的優化 | modules | SyncBailHook |
optimize-chunks | 優化 chunk | chunks | SyncBailHook |
additional-assets | 為編譯(compilation)創建附加資源(asset)。 | - | AsyncSeriesHook |
optimize-chunk-assets | 優化所有 chunk 資源(asset)。 | chunks | AsyncSeriesHook |
optimize-assets | 優化存儲在 compilation.assets 中的所有資源(asset) | assets | AsyncSeriesHook |
插件機制并不復雜,webpack也不復雜,復雜的是插件本身..
另外, 本應該先寫流程的, 流程只能后面補上了。
不滿足于只會使用系列: tapable
webpack系列之二Tapable
編寫一個插件
Compiler
Compilation
compiler和comnpilation鉤子
看清楚真正的 Webpack 插件
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/102367.html
摘要:系列文章系列第一篇基礎雜記系列第二篇插件機制雜記系列第三篇流程雜記前言公司的前端項目基本都是用來做工程化的,而雖然只是一個工具,但內部涉及到非常多的知識,之前一直靠來解決問題,之知其然不知其所以然,希望這次能整理一下相關的知識點。 系列文章 Webpack系列-第一篇基礎雜記 Webpack系列-第二篇插件機制雜記 Webpack系列-第三篇流程雜記 前言 公司的前端項目基本都是用...
摘要:最后執行了的回調函數,觸發了事件點,并回到函數的回調函數觸發了事件點執行對于當前模塊,或許存在著多個依賴模塊。 系列文章 Webpack系列-第一篇基礎雜記 Webpack系列-第二篇插件機制雜記 Webpack系列-第三篇流程雜記 前言 本文章個人理解, 只是為了理清webpack流程, 沒有關注內部過多細節, 如有錯誤, 請輕噴~ 調試 1.使用以下命令運行項目,./scrip...
摘要:入口文件打包出口地址在中可以配置我們的地址這里你要有一個七牛云的賬戶。特別像是七牛云這樣擁有圖片處理引擎的服務商,我們還可以通過來處理上傳至的圖片。 本項目源碼均可在 這里 找到。 之前公司的官網項目靜態文件都是放在靜態服務器中,這其中的弊端就不贅述了。簡單說一下 CDN 的好處: CDN 可以解決因分布、帶寬、服務器性能帶來的訪問延遲問題,適用于站點加速、點播、直播等場景。使用戶可就...
摘要:前提最近通過閱讀官方文檔的事件模塊,有了一些思考和收獲,在這里記錄一下調用方法時需要手動綁定先從一段官方代碼看起代碼中的注釋提到了一句話的綁定是必須的,其實這一塊是比較容易理解的,因為這并不是的一個特殊點,而是這門語言的特性。 前提 最近通過閱讀React官方文檔的事件模塊,有了一些思考和收獲,在這里記錄一下~ 調用方法時需要手動綁定this 先從一段官方代碼看起: showImg(...
閱讀 3475·2021-10-13 09:39
閱讀 1458·2021-10-08 10:05
閱讀 2260·2021-09-26 09:56
閱讀 2275·2021-09-03 10:28
閱讀 2673·2019-08-29 18:37
閱讀 2032·2019-08-29 17:07
閱讀 600·2019-08-29 16:23
閱讀 2191·2019-08-29 11:24