細說 webpack 之流程篇

發表於2016-09-11

tb1_znhnxxxxxxbxpxxxxxxxxxx-900-500

引言

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

準備工作

1. webstorm 中配置 webpack-webstorm-debugger-script

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

先將 webpack-webstorm-debugger-script 中的 webstorm-debugger.js 置於webpack.config.js 的同一目錄下,搭建好你的腳手架後就可以直接 Debug 這個 webstorm-debugger.js 檔案了。

2. webpack.config.js 配置

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

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

  • loader:能轉換各類資源,並處理成對應模組的載入器。loader 間可以序列使用。
  • chunk:code splitting 後的產物,也就是按需載入的分塊,裝載了不同的 module。

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

tb1b0dxnxxxxxxdxfxxxxxxxxxx-368-522

  • plugin:webpack 的外掛實體,這裡以 UglifyJsPlugin 為例。

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

在具體流程學習前,可以先通過這幅 webpack 整體流程圖 瞭解一下大致流程(建議儲存下來檢視)。

tb1gvgfnxxxxxatapxxxxxxxxxx-4436-4244

shell 與 config 解析

每次在命令列輸入 webpack 後,作業系統都會去呼叫 ./node_modules/.bin/webpack 這個 shell 指令碼。這個指令碼會去呼叫 ./node_modules/webpack/bin/webpack.js 並追加輸入的引數,如 -p , -w 。(圖中 webpack.js 是 webpack 的啟動檔案,而 $@ 是字尾引數)

tb1kvfbnxxxxxarxpxxxxxxxxxx-500-111

在 webpack.js 這個檔案中 webpack 通過 optimist 將使用者配置的 webpack.config.js 和 shell 指令碼傳過來的引數整合成 options 物件傳到了下一個流程的控制物件中。

1. optimist

和 commander 一樣,optimist 實現了 node 命令列的解析,其 API 呼叫非常方便。

獲取到字尾引數後,optimist 分析引數並以鍵值對的形式把引數物件儲存在 optimist.argv 中,來看看 argv 究竟有什麼?

2. config 合併與外掛載入

在載入外掛之前,webpack 將 webpack.config.js 中的各個配置項拷貝到 options 物件中,並載入使用者配置在 webpack.config.js 的 plugins 。接著 optimist.argv 會被傳入到./node_modules/webpack/bin/convert-argv.js 中,通過判斷 argv 中引數的值決定是否去載入對應外掛。(至於 webpack 外掛執行機制,在之後的執行機制篇會提到)

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

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

編譯與構建流程

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

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

  • compile 開始編譯
  • make 從入口點分析模組及其依賴的模組,建立這些模組物件
  • build-module 構建模組
  • after-compile 完成構建
  • seal 封裝構建結果
  • emit 把各個chunk輸出到結果檔案
  • after-emit 完成輸出
1. 核心物件 Compilation

compiler.run 後首先會觸發 compile ,這一步會構建出 Compilation 物件:

tb1ugs4nxxxxxxzxvxxxxxxxxxx-693-940

這個物件有兩個作用,一是負責組織整個打包過程,包含了每個構建環節及輸出環節所對應的方法,可以從圖中看到比較關鍵的步驟,如 addEntry() , _addModuleChain() ,buildModule() , seal() , createChunkAssets() (在每一個節點都會觸發 webpack 事件去呼叫各外掛)。二是該物件內部存放著所有 module ,chunk,生成的 asset 以及用來生成最後打包檔案的 template 的資訊。

2. 編譯與構建主流程

在建立 module 之前,Compiler 會觸發 make,並呼叫 Compilation.addEntry 方法,通過 options 物件的 entry 欄位找到我們的入口js檔案。之後,在 addEntry 中呼叫私有方法_addModuleChain ,這個方法主要做了兩件事情。一是根據模組的型別獲取對應的模組工廠並建立模組,二是構建模組。

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

  • 呼叫各 loader 處理模組之間的依賴webpack 提供的一個很大的便利就是能將所有資源都整合成模組,不僅僅是 js 檔案。所以需要一些 loader ,比如 url-loaderjsx-loadercss-loader 等等來讓我們可以直接在原始檔中引用各類資源。webpack 呼叫 doBuild() ,對每一個 require() 用對應的 loader 進行加工,最後生成一個 js module。
  • 呼叫 acorn 解析經 loader 處理後的原始檔生成抽象語法樹 AST
  • 遍歷 AST,構建該模組所依賴的模組對於當前模組,或許存在著多個依賴模組。當前模組會開闢一個依賴模組的陣列,在遍歷 AST 時,將 require() 中的模組通過 addDependency() 新增到陣列中。當前模組構建完成後,webpack 呼叫 processModuleDependencies 開始遞迴處理依賴的 module,接著就會重複之前的構建步驟。
3. 構建細節

module 是 webpack 構建的核心實體,也是所有 module 的 父類,它有幾種不同子類:NormalModule , MultiModule , ContextModule , DelegatedModule 等。但這些核心實體都是在構建中都會去呼叫對應方法,也就是 build() 。來看看其中具體做了什麼:

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

tb1woirnxxxxxcjaxxxxxxxxxxx-445-1228

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

打包輸出

在所有模組及其依賴模組 build 完成後,webpack 會監聽 seal 事件呼叫各外掛對構建後的結果進行封裝,要逐次對每個 module 和 chunk 進行整理,生成編譯後的原始碼,合併,拆分,生成 hash 。 同時這是我們在開發時進行程式碼優化和功能新增的關鍵環節。

1. 生成最終 assets

在封裝過程中,webpack 會呼叫 Compilation 中的 createChunkAssets 方法進行打包後程式碼的生成。 createChunkAssets 流程如下:

tb1cz5-nxxxxxc7xpxxxxxxxxxx-959-807

  • 不同的 Template從上圖可以看出通過判斷是入口 js 還是需要非同步載入的 js 來選擇不同的模板物件進行封裝,入口 js 會採用 webpack 事件流的 render 事件來觸發 Template類 中的renderChunkModules() (非同步載入的 js 會呼叫 chunkTemplate 中的 render 方法)。

    在 webpack 中有四個 Template 的子類,分別是 MainTemplate.jsChunkTemplate.jsModuleTemplate.jsHotUpdateChunkTemplate.js ,前兩者先前已大致有介紹,而 ModuleTemplate 是對所有模組進行一個程式碼生成,HotUpdateChunkTemplate 是對熱替換模組的一個處理。
  • 模組封裝模組在封裝的時候和它在構建時一樣,都是呼叫各模組類中的方法。封裝通過呼叫module.source() 來進行各操作,比如說 require() 的替換。
  • 生成 assets各模組進行 doBlock 後,把 module 的最終程式碼迴圈新增到 source 中。一個 source 對應著一個 asset 物件,該物件儲存了單個檔案的檔名( name )和最終程式碼( value )。
2. 輸出

最後一步,webpack 呼叫 Compiler 中的 emitAssets() ,按照 output 中的配置項將檔案輸出到了對應的 path 中,從而 webpack 整個打包過程結束。要注意的是,若想對結果進行處理,則需要在 emit 觸發後對自定義外掛進行擴充套件。

總結

webpack 的整體流程主要還是依賴於 compilationmodule 這兩個物件,但其思想遠不止這麼簡單。最開始也說過,webpack 本質是個外掛集合,並且由 tapable 控制各外掛在 webpack 事件流上執行,至於具體的思想和細節,將會在後一篇文章中提到。同時,在業務開發中,無論是為了提升構建效率,或是減小打包檔案大小,我們都可以通過編寫 webpack 外掛來進行流程上的控制,這個也會在之後提到。

相關文章