happypack 原理解析


tb15zejofxxxxcyxvxxxxxxxxxx-900-500

說起 happypack 可能很多同學還比較陌生,其實 happypack 是 webpack 的一個插件,目的是通過多進程模型,來加速代碼構建,目前我們的線上服務器已經上線這個插件功能,並做了一定適配,效果顯著。這里有一些大致參考:

tb1apiaofxxxxcnxvxxxxxxxxxx-549-451

這張圖是 happypack 九月逐步全量上線后構建時間的的參考數據,線上構建服務器 16 核環境。

在上這個插件的過程中,我們也發現了這個單人維護的社區插件有一些問題,我們在解決這些問題的同時,也去修改了內部的代碼,發布了自己維護的版本 @ali/happypack,那么內部是怎么跑起來的,這里做一個總結記錄。

webpack 加載配置

 

這個示例只單獨抽取了配置 happypack 的部分。可以看到,類似 extract-text-webpack-plugin 插件,happypack 也是通過 webpack 中 loader 與 plugin 的相互調用協作的方式來運作。

loader 配置直接指向 happypack 提供的 loader, 對於文件實際匹配的處理 loader ,則是通過配置在 plugin 屬性來傳遞說明,這里 happypack 提供的 loader 與 plugin 的銜接匹配,則是通過 id=less 來完成。

happypack 文件解析

HappyPlugin.js

tb1svtkofxxxxcaxpxxxxxxxxxx-767-269

對於 webpack 來講,plugin 是貫穿在整個構建流程,同樣對於 happypack 配置的構建流程,首先進入邏輯的是 plugin 的部分,從初始化的部分查看 happypack 中與 plugin 關聯的文件。

1. 基礎參數設置

對於基礎參數的初始化,對應上文提到的配置,可以看到插件設置了兩個標識

  • id: 在配置文件中設置的與 loader 關聯的 id 首先會設置到實例上,為了后續 loader 與 plugin 能進行一對一匹配
  • name: 標識插件類型為 HappyPack,方便快速在 loader 中定位對應 plugin,同時也可以避免其他插件中存在 id 屬性引起錯誤的風險

對於這兩個屬性的應用,可以看到 loader 文件中有這樣一段代碼

其次聲明 state 對象標識插件的運行狀態之后,開始配置信息的處理。

調用 OptionParser 函數來進行插件過程中使用到的參數合並,在合並函數的參數對象中,提供了作為數據合並依據的一些屬性,例如合並類型 type、默認值 default 以及還有設置校驗函數的校驗屬性 validate 完成屬性檢查。

這里對一些運行過車中的重要屬性進行解釋:

  • tmpDir: 存放打包緩存文件的位置
  • cache: 是否開啟緩存,目前緩存如果開啟,(注: 會以數量級的差異來縮短構建時間,很方便日常開發)
  • cachePath: 存放緩存文件映射配置的位置
  • verbose: 是否輸出過程日志
  • loaders: 因為配置中文件的處理 loader 都指向了 happypack 提供的 loadr ,這里配置的對應文件實際需要運行的 loader

2. 線程池初始化

這里的 thread 其實嚴格意義說是 process,應該是進程,猜測只是套用的傳統軟件的一個主進程多個線程的模型。這里不管是在配置中,配置的是 threads 屬性還是 threadPool 屬性,都會生成一個 HappyThreadPool 對象來管理生成的子進程對象。

2.1. HappyThreadPool.js

在返回 HappyThreadPool 對象之前,會有兩個操作:

2.1.1. HappyRPCHandler.js

對於 HappyRPCHandler 實例,可以從構造函數看到,會綁定當前運行的 loader 與 compiler ,同時在文件中,針對 loader 與 compiler 定義調用接口:

  • 對應 compiler 會綁定查找解析路徑的 reolve 方法:
  • 對應 loader 其中一些綁定:

通過定義調用 webpack 流程過程中的 loader、compiler 的能力來完成功能,類似傳統服務中的 RPC 過程。

2.1.2. 創建子進程 (HappyThread.js)

傳遞子進程數參數 config.size 以及之前生成的 HappyRPCHandler 對象,調用createThreads 方法生成實際的子進程。

fullThreadId 生成之后,傳入 HappyThread 方法,生成對應的子進程,然后放在 set 集合中返回。調用 HappyThread 返回的對象就是 Happypack 的編譯 worker 的上層控制。

對象中包含了對應的進程狀態控制 open 、close,以及通過子進程來實現編譯的流程控制configurecompile

2.1.2.1 子進程執行文件 HappyWorkerChannel.js

上面還可以看到一個信息是,fd 子進程的運行文件路徑變量 WORKER_BIN,這里對應的是相同目錄下的 HappyWorkerChannel.js 。

精簡之后的代碼可以看到 fork 子進程之后,最終執行的是 HappyWorkerChannel 函數,這里的 stream 參數對應的是子進程的 process 對象,用來與主進程進行通信。

函數的邏輯是通過 stream.on('messgae') 訂閱消息,控制層 HappyThread 對象來傳遞消息進入子進程,通過 accept() 方法來路由消息進行對應編譯操作。

對於不同的上層消息進行不通的子進程處理。

2.1.2.1.1 子進程編譯邏輯文件 HappyWorker.js

這里的核心方法 compile ,對應了一層 worker 抽象,包含 Happypack 的實際編譯邏輯,這個對象的構造函數對應 HappyWorker.js 的代碼。

 applyLoaders 的參數看到,這里會把 webpack 編輯過程中的 loadersloaderContext通過最上層的 HappyPlugin 進行傳遞,來模擬實現 loader 的編譯操作。

從回調函數中看到當編譯完成時, fs.writeFileSync(compiledPath, source); 會將編譯結果寫入 compilePath 這個編譯路徑,並通過 done 回調返回編譯結果給主進程。

3. 編譯緩存初始化

happypack 會將每一個文件的編譯進行緩存,這里通過

這里的 cachePath 默認會將 plugin 的 tmpDir 的目錄作為生成緩存映射配置文件的目錄路徑。同時創建好 config.tempDir 目錄。

3.1 happypack 緩存控制 HappyFSCache.js HappyFSCache 函數這里返回對應的 cache 對象,在編譯的開始和 worker 編譯完成時進行緩存加載、設置等操作。

對於編譯過程中的單個文件,會通過 getCompiledSourceCodePath 函數來獲取對應的緩存內容的文件物理路徑,同時在新文件編譯完整之后,會通過 updateMTimeFor 來進行緩存設置的更新。

HappyLoader.js

在 happypack 流程中,配置的對應 loader 都指向了 happypack/loader.js ,文件對應導出的是 HappyLoader.js 導出的對象 ,對應的 bundle 文件處理都通過 happypack 提供的 loader 來進行編譯流程。

省略了部分代碼,HappyLoader 首先拿到配置 id ,然后對所有的 webpack plugin 進行遍歷

找到 id 匹配的 happypackPlugin。傳遞原有 webpack 編譯提供的 loaderContext (loader 處理函數中的 this 對象)中的參數,調用 happypackPlugin 的 compile 進行編譯。

上面是 happypack 的主要文件,作者在項目介紹中也提供了一張圖來進行結構化描述:

tb12ji-opxxxxclaxxxxxxxxxxx-916-556

實際運行

從前面的文件解析,已經把 happypack 的工程文件關聯結構大致說明了一下,這下結合日常在構建工程的一個例子,將整個流程串起來說明。

啟動入口

tb1px8jofxxxxbdaxxxxxxxxxxx-1022-487

在 webpack 編譯流程中,在完成了基礎的配置之后,就開始進行編譯流程,這里 webpack 中的 compiler 對象會去觸發 run 事件,這邊 HappypackPlugin 以這個事件作為流程入口,進行初始化。

 run 事件觸發時,開始進行 start 整個流程

start函數通過 async.series 將整個過程串聯起來。

1. registerCompilerForRPCs: RPCHandler 綁定 compiler

通過調用 plugin 初始化時生成的 handler 上的方法,完成對 compiler 對象的調用綁定。

2. normalizeLoaders: loader 解析

對應中的 webpack 中的 happypackPlugin 的 loaders 配置的處理:

對應配置的 loaders ,經過 normalizeLoader 的處理后,例如 [css!less] 會返回成一個loader 數組 [{path: 'css'},{path: 'less'}],復制到 plugin 的 this.state 屬性上。

3.resolveLoaders: loader 對應文件路徑查詢

為了實際執行 loader 過程,這里將上一步 loader 解析 處理過后的 loaders 數組傳遞到resolveLoaders 方法中,進行解析

 resolveLoaders 方法采用的是借用原有 webpack 的 compiler 對象上的對應resolvers.loader 這個 Resolver 實例的 resolve 方法進行解析,構造好解析參數后,通過async.parallel 並行解析 loader 的路徑

4.loadCache: cache 加載

cache 加載通過調用 cache.load 方法來加載上一次構建的緩存,快速提高構建速度。

load 方法會去讀取 cachePath 這個路徑的緩存配置文件,然后將內容設置到當前 cache對象上的 mtimes 上。

在 happypack 設計的構建緩存中,存在一個上述的一個緩存映射文件,里面的配置會映射到一份編譯生成的緩存文件。

5.launchAndConfigureThreads: 線程池啟動

上面有提到,在加載完 HappyPlugin 時,會創建對應的 HappyThreadPool 對象以及設置數量的 HappyThread。但實際上一直沒有創建真正的子進程實例,這里通過調用threadPool.start 來進行子進程創建。

start 方法通過 send 、notget 這三個方法來進行過濾、啟動的串聯。

傳遞 'isOpen' 到 send 返回函數中,receiver 對象綁定調用 isOpen 方法;再傳遞給 not返回函數中,返回前面函數結構取反。傳遞給 threads 的 filter 方法進行篩選;最后通過 get 傳遞返回的 open 屬性。

 HappyThread 對象中 isOpen 通過判斷 fd 變量來判斷是否創建子進程。

HappyThread 對象的 open 方法首先將 async.parallel 傳遞過來的 callback 鈎子通過Once 方法封裝,避免多次觸發,返回成 emitReady 函數。

然后調用 childProcess.fork 傳遞 HappyWorkerChannel.js 作為子進程執行文件來創建一個子進程,綁定對應的 error 、exit 異常情況的處理,同時綁定最為重要的 message 事件,來接受子進程發來的處理消息。而這里 COMPILED 消息就是對應的子進程完成編譯之后會發出的消息。

在子進程完成創建之后,會向主進程發送一個 READY 消息,表明已經完成創建,在主進程接受到 READY 消息后,會調用前面封裝的 emitReady ,來反饋給 async.parallel 表示完成open 流程。

6.markStarted: 標記啟動

最后一步,在完成之前的步驟后,修改狀態屬性 started 為 true,完成整個插件的啟動過程。

編譯運行

tb1qd5uofxxxxxqxxxxxxxxxxxx-1141-720

1. loader 傳遞 在 webpack 流程中,在源碼文件完成內容讀取之后,開始進入到 loader 的編譯執行階段,這時 HappyLoader 作為編譯邏輯入口,開始進行編譯流程。

loader 中將 webpack 原本的 loaderContext(this指向) 對象的一些參數例如this.resourcethis.resourcePath等透傳到 HappyPlugin.compile 方法進行編譯。

2. plugin 編譯邏輯運行

HappyPlugin 中的 compile 方法對應 build 過程,通過調用 compileInBackground 方法來完成調用。

2.1 構建緩存判斷

 compileInBackground 中,首先會代用 cache 的 hasChanged 和 hasErrored 方法來判斷是否可以從緩存中讀取構建文件。

hasError 判斷的是更新緩存的時候的 error 屬性是否存在。

hasChanged 中會去比較 nowMTime 與 lastMTime 兩個是否相等。實際上這里 nowMTime 通過調用 generateSignature(默認是 getMTime 函數) 返回的是文件目前的最后修改時間,lastMTime 返回的是編譯完成時的修改時間。

如果 nowMTimelastMTime 兩個的最后修改時間相同且不存在錯誤,那么說明構建可以利用緩存

2.1.1 緩存生效

如果緩存判斷生效,那么開始調用 readFromCache 方法,從緩存中讀取構建對應文件內容。

函數的意圖是通過 cache 對象的 getCompiledSourceCodePath 、getCompiledSourceMapPath方法獲取緩存的編譯文件及 sourcemap 文件的存儲路徑,然后讀取出來,完成從緩存中獲取構建內容。

獲取的路徑是通過在完成編譯時調用的 updateMTimeFor 進行存儲的對象中的 compiledPath編譯路徑屬性。

2.1.2 緩存失效

在緩存判斷失效的情況下,進入 _performCompilationRequest ,進行下一步 happypack 編譯流程。

在調用 _performCompilationRequest 前, 還有一步是從 ThreadPool 獲取對應的子進程封裝對象。

這里按照遞增返回的 round-robin,這種在服務器進程控制中經常使用的簡潔算法返回子進程封裝對象。

3. 編譯開始

首先對編譯的文件,調用 cache.invalidateEntryFor 設置該文件路徑的構建緩存失效。然后調用子進程封裝對象的 compile 方法,觸發子進程進行編譯。

同時會生成銜接主進程、子進程、緩存的 compiledPath,當子進程完成編譯后,會將編譯后的代碼寫入 compiledPath,之后發送完成編譯的消息回主進程,主進程也是通過compiledPath 獲取構建后的代碼,同時傳遞 compiledPath 以及對應的編譯前文件路徑filePath,更新緩存設置。

這里的 messageId 是個從 0 開始的遞增數字,完成回調方法的存儲注冊,方便完成編譯之后找到回調方法傳遞信息回主進程。同時在 thread 這一層,也是將參數透傳給子進程執行編譯。

 

子進程接到消息后,調用 worker.compile 方法 ,同時進一步傳遞構建參數。

在 HappyWorker.js 中的 compile 方法中,調用 applyLoaders 進行 loader 方法執行。applyLoaders 是 happypack 中對 webpack 中 loader 執行過程進行模擬,對應 NormalModuleMixin.js 中的 doBuild 方法。完成對文件的字符串處理編譯。

根據 err 判斷是否成功。如果判斷成功,則將對應文件的編譯后內容寫入之前傳遞進來的compiledPath,反之,則會把錯誤內容寫入。

在子進程完成編譯流程后,會調用傳遞進來的回調方法,在回調方法中將編譯信息返回到主進程,主進程根據 compiledPath 來獲取子進程的編譯內容。

獲取子進程的編譯內容 contents 后,根據 result.success 屬性來判斷是否編譯成功,如果失敗的話,會將 contents 作為錯誤傳遞進去。

在完成調用 updateMTimeFor 緩存更新后,最后將內容返回到 HappyLoader.js 中的回調中,返回到 webpack 的原本流程。

4. 編譯結束

當 webpack 整體編譯流程結束后, happypack 開始進行一些善后工作

4.1. 存儲緩存配置

首先調用 cache.save() 存儲下這個緩存的映射設置。

cache 對象的處理是會將這個文件直接寫入 cachePath ,這樣就能供下一次 cache.load 方法裝載配置,利用緩存。

4.2. 終止子進程

其次調用 threadPool.stop 來終止掉進程

類似前面提到的 start 方法,這里是篩選出來正在運行的 HappyThread 對象,調用 close方法。

 HappyThread 中,則是調用 kill 方法,完成子進程的釋放。

匯總

happypack 的處理思路是將原有的 webpack 對 loader 的執行過程從單一進程的形式擴展多進程模式,原本的流程保持不變。整個流程代碼結構上還是比較清晰,在使用過程中,也確實有明顯提升,有興趣的同學可以一起下來交流~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM