天天看點

細說 webpack 之流程篇

細說 webpack 之流程篇

目前,幾乎所有業務的開發建構都會用到 webpack 。的确,作為子產品加載和打包神器,隻需配置幾個檔案,加載各種 loader 就可以享受無痛流程化開發。但對于 webpack 這樣一個複雜度較高的插件集合,它的整體流程及思想對我們來說還是很透明的。那麼接下來我會帶你了解 webpack 這樣一個建構黑盒,首先來談談它的流程。

在開始了解之前,必須要能對 webpack 整個流程進行 debug ,配置過程比較簡單。

估計大家對 webpack.config.js 的配置也嘗試過不少次了,這裡就大緻對這個配置檔案進行個分析。

除此之外再大緻介紹下 webpack 的一些核心概念:

loader:能轉換各類資源,并處理成對應子產品的加載器。loader 間可以串行使用。

chunk:code splitting 後的産物,也就是按需加載的分塊,裝載了不同的 module。

對于 module 和 chunk 的關系可以參照 webpack 官方的這張圖:

細說 webpack 之流程篇

plugin:webpack 的插件實體,這裡以 uglifyjsplugin 為例。

在 webpack 中你經常可以看到 compilation.plugin(‘xxx’, callback) ,你可以把它當作是一個事件的綁定,這些事件在打包時由 webpack 來觸發。

細說 webpack 之流程篇

每次在指令行輸入 webpack 後,作業系統都會去調用 <code>./node_modules/.bin/webpack</code> 這個 shell 腳本。這個腳本會去調用 <code>./node_modules/webpack/bin/webpack.js</code> 并追加輸入的參數,如 -p , -w 。(圖中 webpack.js 是 webpack 的啟動檔案,而 $@ 是字尾參數)

細說 webpack 之流程篇

在 webpack.js 這個檔案中 webpack 通過 optimist 将使用者配置的 webpack.config.js 和 shell 腳本傳過來的參數整合成 options 對象傳到了下一個流程的控制對象中。

擷取到字尾參數後,optimist 分析參數并以鍵值對的形式把參數對象儲存在 optimist.argv 中,來看看 argv 究竟有什麼?

在加載插件之前,webpack 将 webpack.config.js 中的各個配置項拷貝到 options 對象中,并加載使用者配置在 webpack.config.js 的 plugins 。接着 optimist.argv 會被傳入到<code>./node_modules/webpack/bin/convert-argv.js</code> 中,通過判斷 argv 中參數的值決定是否去加載對應插件。(至于 webpack 插件運作機制,在之後的運作機制篇會提到)

<code>options</code> 作為最後傳回結果,包含了之後建構階段所需的重要資訊。

這和 webpack.config.js 的配置非常相似,隻是多了一些經 shell 傳入的插件對象。插件對象一初始化完畢, options 也就傳入到了下個流程中。

在加載配置檔案和 shell 字尾參數申明的插件,并傳入建構資訊 options 對象後,開始整個 webpack 打包最漫長的一步。而這個時候,真正的 webpack 對象才剛被初始化,具體的初始化邏輯在 <code>lib/webpack.js</code> 中,如下:

webpack 的實際入口是 compiler 中的 run 方法,run 一旦執行後,就開始了編譯和建構流程 ,其中有幾個比較關鍵的 webpack 事件節點。

<code>compile</code> 開始編譯

<code>make</code> 從入口點分析子產品及其依賴的子產品,建立這些子產品對象

<code>build-module</code> 構模組化塊

<code>after-compile</code> 完成建構

<code>seal</code> 封裝建構結果

<code>emit</code> 把各個chunk輸出到結果檔案

<code>after-emit</code> 完成輸出

compiler.run 後首先會觸發 compile ,這一步會建構出 compilation 對象:

細說 webpack 之流程篇

這個對象有兩個作用,一是負責組織整個打包過程,包含了每個建構環節及輸出環節所對應的方法,可以從圖中看到比較關鍵的步驟,如 <code>addentry()</code> , <code>_addmodulechain()</code> ,<code>buildmodule()</code> , <code>seal()</code> , <code>createchunkassets()</code> (在每一個節點都會觸發 webpack 事件去調用各插件)。二是該對象内部存放着所有 module ,chunk,生成的 asset 以及用來生成最後打封包件的 template 的資訊。

在建立 module 之前,compiler 會觸發 make,并調用 <code>compilation.addentry</code> 方法,通過 options 對象的 entry 字段找到我們的入口js檔案。之後,在 addentry 中調用私有方法<code>_addmodulechain</code> ,這個方法主要做了兩件事情。一是根據子產品的類型擷取對應的子產品工廠并建立子產品,二是構模組化塊。

而構模組化塊作為最耗時的一步,又可細化為三步:

調用各 loader 處理子產品之間的依賴

webpack 提供的一個很大的便利就是能将所有資源都整合成子產品,不僅僅是 js 檔案。是以需要一些 loader ,比如 <code>url-loader</code> , <code>jsx-loader</code> , <code>css-loader</code> 等等來讓我們可以直接在源檔案中引用各類資源。webpack 調用 <code>dobuild()</code> ,對每一個 require() 用對應的 loader 進行加工,最後生成一個 js module。

周遊 ast,建構該子產品所依賴的子產品

對于目前子產品,或許存在着多個依賴子產品。目前子產品會開辟一個依賴子產品的數組,在周遊 ast 時,将 require() 中的子產品通過 <code>adddependency()</code> 添加到數組中。目前子產品建構完成後,webpack 調用 <code>processmoduledependencies</code> 開始遞歸處理依賴的 module,接着就會重複之前的建構步驟。

module 是 webpack 建構的核心實體,也是所有 module 的 父類,它有幾種不同子類:<code>normalmodule</code> , <code>multimodule</code> , <code>contextmodule</code> , <code>delegatedmodule</code> 等。但這些核心實體都是在建構中都會去調用對應方法,也就是 <code>build()</code> 。來看看其中具體做了什麼:

對于每一個 module ,它都會有這樣一個建構方法。當然,它還包括了從建構到輸出的一系列的有關 module 生命周期的函數,我們通過 module 父類類圖其子類類圖(這裡以 normalmodule 為例)來觀察其真實形态:

細說 webpack 之流程篇

可以看到無論是建構流程,處理依賴流程,包括後面的封裝流程都是與 module 密切相關的。

在所有子產品及其依賴子產品 build 完成後,webpack 會監聽 <code>seal</code> 事件調用各插件對建構後的結果進行封裝,要逐次對每個 module 和 chunk 進行整理,生成編譯後的源碼,合并,拆分,生成 hash 。 同時這是我們在開發時進行代碼優化和功能添加的關鍵環節。

在封裝過程中,webpack 會調用 compilation 中的 <code>createchunkassets</code> 方法進行打包後代碼的生成。 createchunkassets 流程如下:

細說 webpack 之流程篇

不同的 template

從上圖可以看出通過判斷是入口 js 還是需要異步加載的 js 來選擇不同的模闆對象進行封裝,入口 js 會采用 webpack 事件流的 render 事件來觸發 <code>template類</code> 中的<code>renderchunkmodules()</code> (異步加載的 js 會調用 chunktemplate 中的 render 方法)。

在 webpack 中有四個 template 的子類,分别是 <code>maintemplate.js</code> , <code>chunktemplate.js</code>,<code>moduletemplate.js</code> , <code>hotupdatechunktemplate.js</code> ,前兩者先前已大緻有介紹,而 moduletemplate 是對所有子產品進行一個代碼生成,hotupdatechunktemplate 是對熱替換子產品的一個處理。

子產品封裝

子產品在封裝的時候和它在建構時一樣,都是調用各子產品類中的方法。封裝通過調用<code>module.source()</code> 來進行各操作,比如說 require() 的替換。

生成 assets

各子產品進行 doblock 後,把 module 的最終代碼循環添加到 source 中。一個 source 對應着一個 asset 對象,該對象儲存了單個檔案的檔案名( name )和最終代碼( value )。

最後一步,webpack 調用 compiler 中的 <code>emitassets()</code> ,按照 output 中的配置項将檔案輸出到了對應的 path 中,進而 webpack 整個打包過程結束。要注意的是,若想對結果進行處理,則需要在 <code>emit</code> 觸發後對自定義插件進行擴充。

webpack 的整體流程主要還是依賴于 <code>compilation</code> 和 <code>module</code> 這兩個對象,但其思想遠不止這麼簡單。最開始也說過,webpack 本質是個插件集合,并且由 <code>tapable</code> 控制各插件在 webpack 事件流上運作,至于具體的思想和細節,将會在後一篇文章中提到。同時,在業務開發中,無論是為了提升建構效率,或是減小打封包件大小,我們都可以通過編寫 webpack 插件來進行流程上的控制,這個也會在之後提到。

轉載自:http://taobaofed.org/blog/2016/08/24/react-key/

作者:葉齋

繼續閱讀