摘要:系列文章系列第一篇基礎(chǔ)雜記系列第二篇插件機(jī)制雜記系列第三篇流程雜記前言本身并不難,他所完成的各種復(fù)雜炫酷的功能都依賴于他的插件機(jī)制。的插件機(jī)制依賴于一個(gè)核心的庫,。是什么是一個(gè)類似于的的庫主要是控制鉤子函數(shù)的發(fā)布與訂閱。
系列文章
Webpack系列-第一篇基礎(chǔ)雜記
Webpack系列-第二篇插件機(jī)制雜記
Webpack系列-第三篇流程雜記
webpack本身并不難,他所完成的各種復(fù)雜炫酷的功能都依賴于他的插件機(jī)制?;蛟S我們?cè)谌粘5拈_發(fā)需求中并不需要自己動(dòng)手寫一個(gè)插件,然而,了解其中的機(jī)制也是一種學(xué)習(xí)的方向,當(dāng)插件出現(xiàn)問題時(shí),我們也能夠自己來定位。
TapableWebpack的插件機(jī)制依賴于一個(gè)核心的庫, Tapable。
在深入webpack的插件機(jī)制之前,需要對(duì)該核心庫有一定的了解。
tapable 是一個(gè)類似于nodejs 的EventEmitter 的庫, 主要是控制鉤子函數(shù)的發(fā)布與訂閱。當(dāng)然,tapable提供的hook機(jī)制比較全面,分為同步和異步兩個(gè)大類(異步中又區(qū)分異步并行和異步串行),而根據(jù)事件執(zhí)行的終止條件的不同,由衍生出 Bail/Waterfall/Loop 類型。
Tapable的使用 (該小段內(nèi)容引用文章)基本使用:
const { SyncHook } = require("tapable") // 創(chuàng)建一個(gè)同步 Hook,指定參數(shù) const hook = new SyncHook(["arg1", "arg2"]) // 注冊(cè) hook.tap("a", function (arg1, arg2) { console.log("a") }) hook.tap("b", function (arg1, arg2) { console.log("b") }) hook.call(1, 2)
鉤子類型:
BasicHook:執(zhí)行每一個(gè),不關(guān)心函數(shù)的返回值,有SyncHook、AsyncParallelHook、AsyncSeriesHook。
BailHook:順序執(zhí)行 Hook,遇到第一個(gè)結(jié)果result!==undefined則返回,不再繼續(xù)執(zhí)行。有:SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。
什么樣的場(chǎng)景下會(huì)使用到 BailHook 呢?設(shè)想如下一個(gè)例子:假設(shè)我們有一個(gè)模塊 M,如果它滿足 A 或者 B 或者 C 三者任何一個(gè)條件,就將其打包為一個(gè)多帶帶的。這里的 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。
但是當(dāng) A、B、C 的校驗(yàn),需要嚴(yán)格遵循先后順序時(shí),就需要使用有順序的 SyncBailHook(A、B、C 是同步函數(shù)時(shí)使用) 或者 AsyncSeriseBailHook(A、B、C 是異步函數(shù)時(shí)使用)。
WaterfallHook:類似于 reduce,如果前一個(gè) Hook 函數(shù)的結(jié)果 result !== undefined,則 result 會(huì)作為后一個(gè) Hook 函數(shù)的第一個(gè)參數(shù)。既然是順序執(zhí)行,那么就只有 Sync 和 AsyncSeries 類中提供這個(gè)Hook:SyncWaterfallHook,AsyncSeriesWaterfallHook
當(dāng)一個(gè)數(shù)據(jù),需要經(jīng)過 A,B,C 三個(gè)階段的處理得到最終結(jié)果,并且 A 中如果滿足條件 a 就處理,否則不處理,B 和 C 同樣,那么可以使用如下
x.hooks.tap("A", (data) => { if (滿足 A 需要處理的條件) { // 處理數(shù)據(jù) data return data } else { return } }) x.hooks.tap("B", (data) => { if (滿足B需要處理的條件) { // 處理數(shù)據(jù) data return data } else { return } }) x.hooks.tap("C", (data) => { if (滿足 C 需要處理的條件) { // 處理數(shù)據(jù) data return data } else { return } })
LoopHook:不停的循環(huán)執(zhí)行 Hook,直到所有函數(shù)結(jié)果 result === undefined。同樣的,由于對(duì)串行性有依賴,所以只有 SyncLoopHook 和 AsyncSeriseLoopHook (PS:暫時(shí)沒看到具體使用 Case)
Tapable的源碼分析Tapable 基本邏輯是,先通過類實(shí)例的 tap 方法注冊(cè)對(duì)應(yīng) Hook 的處理函數(shù), 這里直接分析sync同步鉤子的主要流程,其他的異步鉤子和攔截器等就不贅述了。
const hook = new SyncHook(["arg1", "arg2"])
從該句代碼, 作為源碼分析的入口,
class SyncHook extends Hook { // 錯(cuò)誤處理,防止調(diào)用者調(diào)用異步鉤子 tapAsync() { throw new Error("tapAsync is not supported on a SyncHook"); } // 錯(cuò)誤處理,防止調(diào)用者調(diào)用promise鉤子 tapPromise() { throw new Error("tapPromise is not supported on a SyncHook"); } // 核心實(shí)現(xiàn) compile(options) { factory.setup(this, options); return factory.create(options); } }
從類SyncHook看到, 他是繼承于一個(gè)基類Hook, 他的核心實(shí)現(xiàn)compile等會(huì)再講, 我們先看看基類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; }
初始化完成后, 通常會(huì)注冊(cè)一個(gè)事件, 如:
// 注冊(cè) hook.tap("a", function (arg1, arg2) { console.log("a") }) hook.tap("b", function (arg1, arg2) { console.log("b") })
很明顯, 這兩個(gè)語句都會(huì)調(diào)用基類中的tap方法:
tap(options, fn) { // 參數(shù)處理 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"); // 執(zhí)行攔截器的register函數(shù), 比較簡(jiǎn)單不分析 options = this._runRegisterInterceptors(options); // 處理注冊(cè)事件 this._insert(options); }
從上面的源碼分析, 可以看到_insert方法是注冊(cè)階段的關(guān)鍵函數(shù), 直接進(jìn)入該方法內(nèi)部
_insert(item) { // 重置所有的 調(diào)用 方法 this._resetCompilation(); // 將注冊(cè)事件排序后放進(jìn)taps數(shù)組 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數(shù)組里面, 排序的算法并不是特別復(fù)雜,這里就不贅述了, 到了這里, 注冊(cè)階段就已經(jīng)結(jié)束了, 繼續(xù)看觸發(fā)階段。
hook.call(1, 2) // 觸發(fā)函數(shù)
在基類hook中, 有一個(gè)初始化過程,
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返回一個(gè)名為lazyCompileHook的函數(shù),顧名思義,即懶編譯, 直到調(diào)用call的時(shí)候, 才會(huì)編譯出正在的call函數(shù)。
createCompileDelegate也是調(diào)用的_createCall, 而_createCall調(diào)用了Compier函數(shù)
_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函數(shù), 即我們一開始說的核心方法
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); } }
關(guān)鍵就在于SyncHookCodeFactory和工廠類HookCodeFactory, 先看setup函數(shù),
setup(instance, options) { // 這里的instance 是syncHook 實(shí)例, 其實(shí)就是把tap進(jìn)來的鉤子數(shù)組給到鉤子的_x屬性里. instance._x = options.taps.map(t => t.fn); }
然后是最關(guān)鍵的create函數(shù), 可以看到最后返回的fn,其實(shí)是一個(gè)new Function動(dòng)態(tài)生成的函數(shù)
create(options) { // 初始化參數(shù),保存options到本對(duì)象this.options,保存new Hook(["options"]) 傳入的參數(shù)到 this._args this.init(options); let fn; // 動(dòng)態(tài)構(gòu)建鉤子,這里是抽象層,分同步, 異步, promise switch (this.options.type) { // 先看同步 case "sync": // 動(dòng)態(tài)返回一個(gè)鉤子函數(shù) fn = new Function( // 生成函數(shù)的參數(shù),no before no after 返回參數(shù)字符串 xxx,xxx 在 // 注意這里this.args返回的是一個(gè)字符串, // 在這個(gè)例子中是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() + // 這個(gè) content 調(diào)用的是子類類的 content 函數(shù), // 參數(shù)由子類傳,實(shí)際返回的是 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; // 我們只有一個(gè)攔截器所以下面的只會(huì)生成一個(gè) _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的機(jī)制, 然而本篇的主要對(duì)象其實(shí)是基于tapable實(shí)現(xiàn)的compile和compilation對(duì)象。不過由于他們都是基于tapable,所以介紹的篇幅相對(duì)短一點(diǎn)。
compile compile是什么compiler 對(duì)象代表了完整的 webpack 環(huán)境配置。這個(gè)對(duì)象在啟動(dòng) webpack 時(shí)被一次性建立,并配置好所有可操作的設(shè)置,包括 options,loader 和 plugin。當(dāng)在 webpack 環(huán)境中應(yīng)用一個(gè)插件時(shí),插件將收到此 compiler 對(duì)象的引用。可以使用 compiler 來訪問 webpack 的主環(huán)境。
也就是說, compile是webpack的整體環(huán)境。
compile的內(nèi)部實(shí)現(xiàn)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, 并且在實(shí)例上綁定了一個(gè)hook對(duì)象, 使得Compier的實(shí)例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 操作構(gòu)建結(jié)果 compilation.addModule(/* ... */); callback(); } );compilation 什么是compilation
compilation 對(duì)象代表了一次資源版本構(gòu)建。當(dāng)運(yùn)行 webpack 開發(fā)環(huán)境中間件時(shí),每當(dāng)檢測(cè)到一個(gè)文件變化,就會(huì)創(chuàng)建一個(gè)新的 compilation,從而生成一組新的編譯資源。一個(gè) compilation 對(duì)象表現(xiàn)了當(dāng)前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態(tài)信息。compilation 對(duì)象也提供了很多關(guān)鍵時(shí)機(jī)的回調(diào),以供插件做自定義處理時(shí)選擇使用。compilation的實(shí)現(xiàn)
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實(shí)現(xiàn)。
編寫一個(gè)插件了解到tapablecompilercompilation之后, 再來看插件的實(shí)現(xiàn)就不再一頭霧水了
以下代碼源自官方文檔
class MyExampleWebpackPlugin { // 定義 `apply` 方法 apply(compiler) { // 指定要追加的事件鉤子函數(shù) 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 操作構(gòu)建結(jié)果 compilation.addModule(/* ... */); callback(); } ); } }
可以看到其實(shí)就是在apply中傳入一個(gè)Compiler實(shí)例, 然后基于該實(shí)例注冊(cè)事件, compilation同理, 最后webpack會(huì)在各流程執(zhí)行call方法。
compiler和compilation一些比較重要的事件鉤子 compier事件鉤子 | 觸發(fā)時(shí)機(jī) | 參數(shù) | 類型 |
---|---|---|---|
entry-option | 初始化 option | - | SyncBailHook |
run | 開始編譯 | compiler | AsyncSeriesHook |
compile | 真正開始的編譯,在創(chuàng)建 compilation 對(duì)象之前 | compilation | SyncHook |
compilation | 生成好了 compilation 對(duì)象,可以操作這個(gè)對(duì)象啦 | compilation | SyncHook |
make | 從 entry 開始遞歸分析依賴,準(zhǔn)備對(duì)每個(gè)模塊進(jìn)行 build | compilation | AsyncParallelHook |
after-compile | 編譯 build 過程結(jié)束 | compilation | AsyncSeriesHook |
emit | 在將內(nèi)存中 assets 內(nèi)容寫到磁盤文件夾之前 | compilation | AsyncSeriesHook |
after-emit | 在將內(nèi)存中 assets 內(nèi)容寫到磁盤文件夾之后 | compilation | AsyncSeriesHook |
done | 完成所有的編譯過程 | stats | AsyncSeriesHook |
failed | 編譯失敗的時(shí)候 | error | SyncHook |
事件鉤子 | 觸發(fā)時(shí)機(jī) | 參數(shù) | 類型 |
---|---|---|---|
normal-module-loader | 普通模塊 loader,真正(一個(gè)接一個(gè)地)加載模塊圖(graph)中所有模塊的函數(shù)。 | loaderContext module | SyncHook |
seal | 編譯(compilation)停止接收新模塊時(shí)觸發(fā)。 | - | SyncHook |
optimize | 優(yōu)化階段開始時(shí)觸發(fā)。 | - | SyncHook |
optimize-modules | 模塊的優(yōu)化 | modules | SyncBailHook |
optimize-chunks | 優(yōu)化 chunk | chunks | SyncBailHook |
additional-assets | 為編譯(compilation)創(chuàng)建附加資源(asset)。 | - | AsyncSeriesHook |
optimize-chunk-assets | 優(yōu)化所有 chunk 資源(asset)。 | chunks | AsyncSeriesHook |
optimize-assets | 優(yōu)化存儲(chǔ)在 compilation.assets 中的所有資源(asset) | assets | AsyncSeriesHook |
插件機(jī)制并不復(fù)雜,webpack也不復(fù)雜,復(fù)雜的是插件本身..
另外, 本應(yīng)該先寫流程的, 流程只能后面補(bǔ)上了。
不滿足于只會(huì)使用系列: tapable
webpack系列之二Tapable
編寫一個(gè)插件
Compiler
Compilation
compiler和comnpilation鉤子
看清楚真正的 Webpack 插件
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/102367.html
摘要:系列文章系列第一篇基礎(chǔ)雜記系列第二篇插件機(jī)制雜記系列第三篇流程雜記前言公司的前端項(xiàng)目基本都是用來做工程化的,而雖然只是一個(gè)工具,但內(nèi)部涉及到非常多的知識(shí),之前一直靠來解決問題,之知其然不知其所以然,希望這次能整理一下相關(guān)的知識(shí)點(diǎn)。 系列文章 Webpack系列-第一篇基礎(chǔ)雜記 Webpack系列-第二篇插件機(jī)制雜記 Webpack系列-第三篇流程雜記 前言 公司的前端項(xiàng)目基本都是用...
摘要:最后執(zhí)行了的回調(diào)函數(shù),觸發(fā)了事件點(diǎn),并回到函數(shù)的回調(diào)函數(shù)觸發(fā)了事件點(diǎn)執(zhí)行對(duì)于當(dāng)前模塊,或許存在著多個(gè)依賴模塊。 系列文章 Webpack系列-第一篇基礎(chǔ)雜記 Webpack系列-第二篇插件機(jī)制雜記 Webpack系列-第三篇流程雜記 前言 本文章個(gè)人理解, 只是為了理清webpack流程, 沒有關(guān)注內(nèi)部過多細(xì)節(jié), 如有錯(cuò)誤, 請(qǐng)輕噴~ 調(diào)試 1.使用以下命令運(yùn)行項(xiàng)目,./scrip...
摘要:入口文件打包出口地址在中可以配置我們的地址這里你要有一個(gè)七牛云的賬戶。特別像是七牛云這樣擁有圖片處理引擎的服務(wù)商,我們還可以通過來處理上傳至的圖片。 本項(xiàng)目源碼均可在 這里 找到。 之前公司的官網(wǎng)項(xiàng)目靜態(tài)文件都是放在靜態(tài)服務(wù)器中,這其中的弊端就不贅述了。簡(jiǎn)單說一下 CDN 的好處: CDN 可以解決因分布、帶寬、服務(wù)器性能帶來的訪問延遲問題,適用于站點(diǎn)加速、點(diǎn)播、直播等場(chǎng)景。使用戶可就...
摘要:前提最近通過閱讀官方文檔的事件模塊,有了一些思考和收獲,在這里記錄一下調(diào)用方法時(shí)需要手動(dòng)綁定先從一段官方代碼看起代碼中的注釋提到了一句話的綁定是必須的,其實(shí)這一塊是比較容易理解的,因?yàn)檫@并不是的一個(gè)特殊點(diǎn),而是這門語言的特性。 前提 最近通過閱讀React官方文檔的事件模塊,有了一些思考和收獲,在這里記錄一下~ 調(diào)用方法時(shí)需要手動(dòng)綁定this 先從一段官方代碼看起: showImg(...
閱讀 3488·2021-10-13 09:39
閱讀 1470·2021-10-08 10:05
閱讀 2276·2021-09-26 09:56
閱讀 2291·2021-09-03 10:28
閱讀 2690·2019-08-29 18:37
閱讀 2049·2019-08-29 17:07
閱讀 611·2019-08-29 16:23
閱讀 2200·2019-08-29 11:24