歡迎你們前往雲+社區,獲取更多騰訊海量技術實踐乾貨哦~html
做者介紹:陳柏信,騰訊前端開發,目前主要負責手Q遊戲中心業務開發,以及項目相關的技術升級、架構優化等工做。
webpack 是一個強大的模塊打包工具,之因此強大的一個緣由在於它擁有靈活、豐富的插件機制。可是 webpack 的文檔不太友好,就我本身的學習經從來說,官方的文檔並不詳細,網上的學習資料又少有完整的概述和例子。因此,在研究了一段時間的 webpack 源碼以後,本身但願寫個系列文章,結合本身的實踐一塊兒來談談 webpack 插件這個主題,也但願可以幫助其餘人更全面地瞭解 webpack。前端
這篇文章是系列文章的第二篇,將會從對象的角度來說解 webpack。若是你想從總體角度瞭解 webpack,能夠先閱讀系列文章的第一篇:node
P.S. 如下的分析都基於 webpack 3.6.0
P.S. 本文將繼續沿用第一篇文章的名詞,任務點表示經過 plugin 方法註冊的名稱webpack
跟第一篇文章相似,咱們不會將全部 webpack 中的對象都拿出來說解,而是整理了一些比較核心的概念。咱們能夠先看看下面的類圖:web
下面的論述將會逐一講述類圖中的對象,首先咱們先來看一下最頂層的類 Tapable。算法
Tapable 提供了 webpack 中基於任務點的架構基礎,它將提供任務點註冊的方法以及觸發的方法。緩存
一個簡單的例子,使用 plugin 方法來註冊一個任務點,而後使用 applyPlugins 方法觸發:bash
Tapable 裏面註冊任務點只有 plugin 方法,可是觸發任務點的方法是提供了不少,能夠分爲同步和異步執行兩類:網絡
同步執行:架構
異步執行:
雖然上面的方法看起來不少,但從函數名就聯想到函數的實際功能:
最後 Tapable 類還提供了一個方法 apply,它的做用是提供了外部插件註冊任務點的統一接口,要求都在 apply 方法內部進行任務點註冊邏輯:
webpack 中自定義插件就是調用 Compiler 實例對象(繼承於 Tapable)的 apply 方法:
webpack 源碼中隨處能夠見 Tapable 的身影,在瞭解其工做原理對理解源碼頗有幫助。 Compiler 繼承了 Tapable,同時也做爲構建的入口對象,下面咱們來看一下。
Compiler 是一個編譯器實例,在 webpack 的每一個進程中只會建立一個對象,它用來建立構建對象 Compilation,自己須要注意的屬性和方法並非不少。下面咱們找幾個主要的屬性來講一下。
當 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 中每一個模塊實例究竟是什麼東西,咱們並不太清楚。因此下面的內容將會比較細地講解。
但若是你對這部份內容不感興趣也能夠直接跳過,由於能真正使用的場景不會太多,但它能加深對 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 解析器來完成依賴解析。這裏要兩個點要注意:
咱們再來看看這些模塊類,從前面的類圖看,它們是繼承於 Module 類。這個類實際上纔是咱們日常用來跟 chunk 打交道的類對象,它內部有 _chunks 屬性來記錄後續所在的 chunk 信息,而且提供了不少相關的方法來操做這個對象: addChunk, removeChunk, isInChunk, mapChunks等。後面咱們也會看到, Chunk 類與之對應。
Module 類往上還會繼承於 DependenciesBlock,這個是全部模塊的基類,它包含了處理依賴所須要的屬性和方法。上面所說的 variables, dependencies, blocks 也是這個基類擁有的三個屬性。它們分別是:
通過上面的討論以後,咱們基本將 webpack 中於模塊相關的對象、概念都涉及到了,剩下還有模塊渲染相關的模板,會在下面描述 Template 時繼續討論。
Chunk
討論完 webpack 的模塊以後,下面須要說明的是 Chunk 對象。關於 chunk 的生成,在第一篇文章中有涉及,這裏再也不贅述。 chunk 只有一個相關類,並且並不複雜。 Chunk 類內部的主要屬性是 _modules,用來記錄包含的全部模塊對象,而且提供了不少方法來操做: addModule, removeModule, mapModules 等。 另外有幾個方法可能比較實用,這裏也列出來:
Template
Compilation 實例在生成最終文件時,須要將全部的 chunk 渲染(生成代碼)出來,這個時候須要用到下面幾個屬性:
在第一篇文章時,有略微描述過 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")複製代碼
其中,從 1-113 行都是 MainTemplate 生成的啓動代碼,剩餘的代碼生成以下圖所示:
經過這篇文章,咱們將 webpack 中的一些核心概念和對象都進行了不一樣程度的討論,這裏再總結一下他們主要的做用和意義:
以上內容,但願可以幫助你們進一步瞭解 webpack ,感謝你們閱讀~
玩轉webpack(一)上篇:webpack的基本架構和構建流程
玩轉webpack(一)下篇:webpack的基本架構和構建流程
利用神經網絡算法的C#手寫數字識別
此文已由做者受權雲加社區發佈,轉載請註明文章出處