玩轉webpack(二):webpack的核心對象

歡迎你們前往雲+社區,獲取更多騰訊海量技術實踐乾貨哦~html

做者介紹:陳柏信,騰訊前端開發,目前主要負責手Q遊戲中心業務開發,以及項目相關的技術升級、架構優化等工做。

前言

webpack 是一個強大的模塊打包工具,之因此強大的一個緣由在於它擁有靈活、豐富的插件機制。可是 webpack 的文檔不太友好,就我本身的學習經從來說,官方的文檔並不詳細,網上的學習資料又少有完整的概述和例子。因此,在研究了一段時間的 webpack 源碼以後,本身但願寫個系列文章,結合本身的實踐一塊兒來談談 webpack 插件這個主題,也但願可以幫助其餘人更全面地瞭解 webpack。前端

這篇文章是系列文章的第二篇,將會從對象的角度來說解 webpack。若是你想從總體角度瞭解 webpack,能夠先閱讀系列文章的第一篇:node

P.S. 如下的分析都基於 webpack 3.6.0
P.S. 本文將繼續沿用第一篇文章的名詞,任務點表示經過 plugin 方法註冊的名稱webpack

webpack中的核心對象

跟第一篇文章相似,咱們不會將全部 webpack 中的對象都拿出來說解,而是整理了一些比較核心的概念。咱們能夠先看看下面的類圖:web

下面的論述將會逐一講述類圖中的對象,首先咱們先來看一下最頂層的類 Tapable。算法

Tapable

Tapable 提供了 webpack 中基於任務點的架構基礎,它將提供任務點註冊的方法以及觸發的方法。緩存

一個簡單的例子,使用 plugin 方法來註冊一個任務點,而後使用 applyPlugins 方法觸發:bash

Tapable 裏面註冊任務點只有 plugin 方法,可是觸發任務點的方法是提供了不少,能夠分爲同步和異步執行兩類:網絡

同步執行:架構

  • applyPlugins(name,...params)
  • applyPlugins0(name)
  • applyPlugins1(name,param)
  • applyPlugins2(name,param1,param2)
  • applyPluginsWaterfall(name,init,...params)
  • applyPluginsWaterfall0(name,init)
  • applyPluginsWaterfall1(name,init,param)
  • applyPluginsWaterfall2(name,init,param1,param2)
  • applyPluginsBailResult(name,...params)
  • applyPluginsBailResult0(name)
  • applyPluginsBailResult1(name,param)
  • applyPluginsBailResult2(name,param1,param2)
  • applyPluginsBailResult3(name,param1,param2,param3)
  • applyPluginsBailResult4(name,param1,param2,param3,param4)
  • applyPluginsBailResult5(name,param1,param2,param3,param4,param5)

異步執行:

  • applyPluginsAsync(name,...params,callback)
  • applyPluginsAsyncSeries(name,...params,callback)
  • applyPluginsAsyncSeries1(name,param,callback)
  • applyPluginsAsyncSeriesBailResult(name,...params,callback)
  • applyPluginsAsyncSeriesBailResult1(name,param,callback)
  • applyPluginsAsyncWaterfall(name,init,...params,callback)
  • applyPluginsParallel(name,...params,callback)
  • applyPluginsParallelBailResult(name,...params,callback)
  • applyPluginsParallelBailResult1(name,param,callback)

雖然上面的方法看起來不少,但從函數名就聯想到函數的實際功能:

  • *Waterfall 的方法會將上一個監聽器的執行結果傳給下一個
  • *BailResult 的方法只會執行到第一個返回結果不爲undefined的監聽器
  • *Series 的方法會嚴格線性來執行異步監聽器,只有上一個結束下一個纔會開始
  • *Parallel 的方法會並行執行異步監聽器
  • 函數名稱最後若是帶有數字,那麼會按照實際的參數傳給監聽器。若是有數字,則嚴格按照數字來傳遞參數個數。

最後 Tapable 類還提供了一個方法 apply,它的做用是提供了外部插件註冊任務點的統一接口,要求都在 apply 方法內部進行任務點註冊邏輯:

webpack 中自定義插件就是調用 Compiler 實例對象(繼承於 Tapable)的 apply 方法:

webpack 源碼中隨處能夠見 Tapable 的身影,在瞭解其工做原理對理解源碼頗有幫助。 Compiler 繼承了 Tapable,同時也做爲構建的入口對象,下面咱們來看一下。

Compiler

Compiler 是一個編譯器實例,在 webpack 的每一個進程中只會建立一個對象,它用來建立構建對象 Compilation,自己須要注意的屬性和方法並非不少。下面咱們找幾個主要的屬性來講一下。

options屬性

當 webpack 開始運行時,第一件事就是解析咱們傳入的配置,而後將配置賦值給 Compiler 實例:

所以,咱們能夠直接經過這個屬性來獲取到解析後的 webpack 配置:

若是你不知足於官網給出的配置文檔,想要了解更多配置解析,能夠看看 WebpackOptionsDefaulter.js 這個文件,這裏再也不贅述。

輸入輸出

Compiler 實例在一開始也會初始化輸入輸出,分別是 inputFileSystem 和 outputFileSystem 屬性,通常狀況下這兩個屬性都是對應的 nodejs 中拓展後的 fs 對象。可是有一點要注意,當 Compiler 實例以 watch模式運行時, outputFileSystem 會被重寫成內存輸出對象。也就是說,實際上在 watch 模式下,webpack 構建後的文件並不會生成真正的文件,而是保存在內存中。

咱們可使用 inputFileSystem 和 outputFileSystem 屬性來幫助咱們實現一些文件操做,若是你但願自定義插件的一些輸入輸出行爲可以跟 webpack 儘可能同步,那麼最好使用 Compiler 提供的這兩個變量:

webpack 的 inputFileSystem 會相對更復雜一點,它內部實現了一些緩存的機制,使得性能效率更高。若是對這部分有興趣,能夠從這個 NodeEnvironmentPlugin 插件開始看起,它是內部初始化了 inputFileSystem 和 outputFileSystem:

建立子編譯器

在第一篇文章講解 Compilation 實例化的時候,有略微說起到建立子編譯器的內容:

這裏爲何會有 compilation 和 this-compilation 兩個任務點?實際上是跟子編譯器有關, Compiler 實例經過
createChildCompiler 方法能夠建立子編譯器實例 childCompiler,建立時 childCompiler 會複製
compiler 實例的任務點監聽器。任務點 compilation 的監聽器會被複制,而任務點 this-compilation
的監聽器不會被複制。 更多關於子編譯器的內容,將在其餘文章中討論。

這裏咱們來仔細看一會兒編譯器是如何建立的, Compiler 實例經過 createChildCompiler 的方法來建立:

上面的代碼看起來不少,但其實主要邏輯基本都是在拷貝父編譯器的屬性到子編譯器上面。值得注意的一點是第9行,子編譯器在拷貝父編譯器的任務點時,會過濾掉 make, compile, emit, after-emit, invalid, done, this-compilation這些任務點。

若是你閱讀過第一篇文章(若是沒有,推薦先看一下),應該會知道上面任務點在整個構建流程中的位置。從這裏咱們也能夠看出來,子編譯器跟父編譯器的一個差異在於,子編譯器並無完整的構建流程。 好比子編譯器沒有文件生成階段( emit任務點),它的文件生成必須掛靠在父編譯器下面來實現。

另外須要注意的是,子編譯器的運行入口並不是 run 方法 ,而是有單獨的 runAsChild 方法來運行,從代碼上面也可以直接看出來,它立刻調用了 compile 方法,跳過了 run, make等任務點:

那麼子編譯器有什麼做用呢?從上面功能和流程來看,子編譯器仍然擁有完整的模塊解析和chunk生成階段。也就是說咱們能夠利用子編譯器來獨立(於父編譯器)跑完一個核心構建流程,額外生成一些須要的模塊或者chunk。

事實上一些外部的 webpack 插件就是這麼作的,好比經常使用的插件 html-webpack-plugin 中,就是利用子編譯器來獨立完成 html 文件的構建,爲何不能直接讀取 html 文件?由於 html 文件中可能依賴其餘外部資源(好比 img 的src屬性),因此加載 html 文件時仍然須要一個額外的完整的構建流程來完成這個任務,子編譯器的做用在這裏就體現出來了:

在下一篇文章中咱們將親自實現一個插件,關於子編譯器的具體實踐到時再繼續討論。

Compilation

接下來咱們來看看最重要的 Compilation 對象,在上一篇文章中,咱們已經說明過部分屬性了,好比咱們簡單回顧一下

  • modules 記錄了全部解析後的模塊
  • chunks 記錄了全部chunk
  • assets記錄了全部要生成的文件

上面這三個屬性已經包含了 Compilation 對象中大部分的信息,可是咱們也只是有個大體的概念,特別是 modules 中每一個模塊實例究竟是什麼東西,咱們並不太清楚。因此下面的內容將會比較細地講解。

但若是你對這部份內容不感興趣也能夠直接跳過,由於能真正使用的場景不會太多,但它能加深對 webpack 構建的理解。

所謂的模塊
Compilation 在解析過程當中,會將解析後的模塊記錄在 modules 屬性中,那麼每個模塊實例又是什麼呢?

首先咱們先回顧一下最開始的類圖,咱們會發現跟模塊相關的類很是多,看起來類之間的關係也十分複雜,但其實只要記住下面的公式就很好理解:

這個公式的解讀是: 一個依賴對象(Dependency)通過對應的工廠對象(Factory)建立以後,就可以生成對應的模塊實例(Module)。

首先什麼是 Dependency?我我的的理解是,還未被解析成模塊實例的依賴對象。好比咱們運行 webpack 時傳入的入口模塊,或者一個模塊依賴的其餘模塊,都會先生成一個 Dependency 對象。做爲基類的 Dependency 十分簡單,內部只有一個 module 屬性來記錄最終生成的模塊實例。可是它的派生類很是多,webpack 中有單獨的文件夾( webpack/lib/dependencies)來存放全部的派生類,這裏的每個派生類都對應着一種依賴的場景。好比從 CommonJS 中require一個模塊,那麼會先生成 CommonJSRequireDependency。

有了 Dependency 以後,如何找到對應的工廠對象呢? Dependecy 的每個派生類在使用前,都會先肯定對應的工廠對象,好比 SingleEntryDependency 對應的工廠對象是 NormalModuleFactory。這些信息所有是記錄在 Compilation 對象的 dependencyFactories 屬性中,這個屬性是 ES6 中的 Map 對象。直接看下面的代碼可能更容易理解:

一種工廠對象只會生成一種模塊,因此不一樣的模塊實例都會有不一樣的工廠對象來生成。模塊的生成過程咱們在第一篇文章有討論過,無非就是解析模塊的 request, loaders等信息而後實例化。

模塊對象有哪些特性呢?一樣在第一篇文章中,咱們知道一個模塊在實例化以後並不意味着構建就結束了,它還有一個內部構建的過程。全部的模塊實例都有一個 build 方法,這個方法的做用是開始加載模塊源碼(並應用loaders),而且經過 js 解析器來完成依賴解析。這裏要兩個點要注意:

  1. 模塊源碼最終是保存在 _source 屬性中,能夠經過 _source.source() 來獲得。注意在 build 以前 _source是不存在的。
  2. js 解析器解析以後會記錄全部的模塊依賴,這些依賴其實會分爲三種,分別記錄在 variables, dependencies, blocks屬性。模塊構建以後的遞歸構建過程,其實就是讀取這三個屬性來重複上面的過程:依賴 => 工廠 => 模塊

咱們再來看看這些模塊類,從前面的類圖看,它們是繼承於 Module 類。這個類實際上纔是咱們日常用來跟 chunk 打交道的類對象,它內部有 _chunks 屬性來記錄後續所在的 chunk 信息,而且提供了不少相關的方法來操做這個對象: addChunk, removeChunk, isInChunk, mapChunks等。後面咱們也會看到, Chunk 類與之對應。

Module 類往上還會繼承於 DependenciesBlock,這個是全部模塊的基類,它包含了處理依賴所須要的屬性和方法。上面所說的 variables, dependencies, blocks 也是這個基類擁有的三個屬性。它們分別是:

  • variables 對應須要對應的外部變量,好比 __filename, __dirname, process 等node環境下特有的變量
  • dependencies 對應須要解析的其餘普通模塊,好比 require("./a") 中的 a 模塊會先生成一個 CommonJSRequireDependency
  • blocks 對應須要解析的代碼塊(最終會對應成一個 chunk),好比 require.ensure("./b"),這裏的 b 會生成一個 DependenciesBlock 對象

通過上面的討論以後,咱們基本將 webpack 中於模塊相關的對象、概念都涉及到了,剩下還有模塊渲染相關的模板,會在下面描述 Template 時繼續討論。

Chunk
討論完 webpack 的模塊以後,下面須要說明的是 Chunk 對象。關於 chunk 的生成,在第一篇文章中有涉及,這裏再也不贅述。 chunk 只有一個相關類,並且並不複雜。 Chunk 類內部的主要屬性是 _modules,用來記錄包含的全部模塊對象,而且提供了不少方法來操做: addModule, removeModule, mapModules 等。 另外有幾個方法可能比較實用,這裏也列出來:

  • integrate 用來合併其餘chunk
  • split 用來生成新的子 chunk
  • hasRuntime 判斷是不是入口 chunk 其餘關於 chunk 的內容,有興趣的同窗能夠直接查看源碼。

Template
Compilation 實例在生成最終文件時,須要將全部的 chunk 渲染(生成代碼)出來,這個時候須要用到下面幾個屬性:

  • mainTemplate 對應 MainTemplate 類,用來渲染入口 chunk
  • chunkTemplate 對應 ChunkTemplate 類,用來傳染非入口 chunk
  • moduleTemplate 對應 ModuleTemplate,用來渲染 chunk 中的模塊
  • dependencyTemplates 記錄每個依賴類對應的模板

在第一篇文章時,有略微描述過 chunk 渲染的過程,這裏再仔細地過一遍,看看這幾個屬性是如何應用在渲染過程當中的:

首先 chunk 的渲染入口是 mainTemplate 和 chunkTemplate 的 render 方法。根據 chunk 是不是入口 chunk 來區分使用哪個:

兩個類的 render 方法將生成不一樣的"包裝代碼", MainTemplate 對應的入口 chunk 須要帶有 webpack 的啓動代碼,因此會有一些函數的聲明和啓動。 這兩個類都只負責這些"包裝代碼"的生成,包裝代碼中間的每一個模塊代碼,是經過調用 renderChunkModules 方法來生成的。這裏的 renderChunkModules 是由他們的基類 Template 類提供,方法會遍歷 chunk 中的模塊,而後使用 ModuleTemplate 來渲染。

ModuleTemplate 作的事情跟 MainTemplate 相似,它一樣只是生成"包裝代碼"來封裝真正的模塊代碼,而真正的模塊代碼,是經過模塊實例的 source 方法來提供。該方法會先讀取 _source 屬性,即模塊內部構建時應用loaders以後生成的代碼,而後使用 dependencyTemplates 來更新模塊源碼。

dependencyTemplates 是 Compilation 對象的一個屬性,它跟 dependencyFactories 一樣是個 Map 對象,記錄了全部的依賴類對應的模板類。

上面用文字來描述這個過程可能十分難懂,因此咱們直接看實際的例子。好比下面這個文件:

let a = require("./a")複製代碼

咱們來看看使用 webpack 構建後最終的文件:

其中,從 1-113 行都是 MainTemplate 生成的啓動代碼,剩餘的代碼生成以下圖所示:

總結

經過這篇文章,咱們將 webpack 中的一些核心概念和對象都進行了不一樣程度的討論,這裏再總結一下他們主要的做用和意義:

  • Tapable 爲 webpack 的總體構建流程提供了基礎,利用事件機制來分離龐大的構建任務,保證了 webpack 強大的配置能力。
  • Compiler 對象做爲構建入口對象,負責解析全局的 webpack 配置,再將配置應用到 Compilation 對象中。
  • Compilation 對象是每一次構建的核心對象,包含了一次構建過程的所有信息。理清楚 Compilation 對象核心的任務點和相關數據,是理解 webpack 構建過程的關鍵。

以上內容,但願可以幫助你們進一步瞭解 webpack ,感謝你們閱讀~

相關閱讀

玩轉webpack(一)上篇:webpack的基本架構和構建流程
玩轉webpack(一)下篇:webpack的基本架構和構建流程
利用神經網絡算法的C#手寫數字識別

此文已由做者受權雲加社區發佈,轉載請註明文章出處

相關文章
相關標籤/搜索