webpack構建流程及梳理

餓了麼新餐飲前端團隊發表於2019-11-27

摘要

webpack的核心功能是通過抽離出很多外掛來實現的,因此係統內功能的劃分粒度很細,這樣做到了完美解偶同時又分工明確,程式碼容易維護。可以說外掛就是webpack的基石,這些基石又影響著流程的走向。這些鉤子是通過Tapable串起來的,可以類比Vue框架的生命週期,webpack也有自己的生命週期,在週期裡邊會順序地觸發一些鉤子,掛載在這些鉤子上的外掛得以執行,從而進行一些特定的邏輯處理。在外掛裡邊,構建的實體或構建出來的資料結果都是可觸達的,這樣做實現了webpack的高度可擴充套件。瞭解了這些之後,我們就不再懷疑webpack是如何擁有如此豐富的生態體系及社群、如何達到了今天的高度。

關於Compiler

Compiler物件就是webpack的實體(是Tapable的例項),掌控者整個webpack的生命週期,他不執行具體的任務,只是進行一些排程工作(調兵遣將)。他建立了Compilation物件,Compilation任務執行完畢後會將最終的處理結果返回給Compiler。官網列出了該物件暴露出的所有鉤子。

關於Compilation

Compilation是編譯階段的主要執行者,(是Tapable的例項),執行模組建立、依賴收集、分塊、打包等主要任務的物件。官網列出了該物件暴露出的所有鉤子。

在第二章節,我們瞭解到獲取到配置資料之後,啟動了compiler.run方法。其實在compiler啟動run之前,在webpack.js我們會發現還做了很多初始化操作,比如增加預設操作,比如針對不同的配置項(如target、devtool)初始化相應的外掛等。

webpack支援傳入多個配置物件,比如一個library有多個構建目標,就需要傳入多個配置物件,每個配置物件都會執行。如果傳入一個陣列,初始化的就不是Compiler,而是MultiCompiler,最後會運作MultiCompiler上的run方法,在裡邊遍歷compilers物件,存放著Compiler陣列,然後會依次呼叫Conmpiler的run方法。

Compiler

compiler的啟動是run方法,run方法裡邊主要關注兩個動作:呼叫了compile方法;宣告瞭呼叫compile傳入的回撥函式onCompiled。
複製程式碼
  1. compile()

    涉及webpack構建生命週期的幾個重要鉤子:

    1. compile上的鉤子:beforeCompile、compile、make(關鍵鉤子)、afterCompile、thisCompilation、compilation
    2. compilation上的鉤子:finish、seal
      在這個方法裡邊,主要是構建建立Compilation所需要的引數並建立了Compilation,這個引數就是後邊解析module需要用的工廠函式。
      make鉤子是一個關鍵的鉤子,呼叫make鉤子時傳入的是新建的compilation物件,在這個鉤子上掛載了一些入口外掛的處理邏輯,這些入口外掛裡邊呼叫compilation.addEntry(),此後,控制權就由Compiler轉移到了Compilation。在make鉤子的回撥函式裡邊呼叫了compilation的finish、seal鉤子。
  2. onCompiled()

    涉及webpack構建生命週期的最後幾個重要鉤子:emit、done。該方法相當於將Compilation的許可權又收取回來。此時拿到的compilation物件是彙集了經過module解析、loader處理、template編譯後的所有資原始檔。
    該方法裡邊主要呼叫了emitAssets方法,該方法呼叫了emit鉤子(這一步我們可以獲取完整的構建資料),獲取compilation構建出來的所有的assets資源資料,裡邊遞迴的呼叫writeOut寫入最終的chunk檔案,並呼叫done鉤子。

Compilation

compilation開始於addEntry方法並結束於addEntry。
複製程式碼
  1. addEntry(): 根據入口的配置模式,分為單入口和多入口。該方法裡邊主要呼叫了_addModuleChain()。
  2. _addModuleChain(): 建立module。根據入口的不同,使用不同的模組工廠(從建立Compilation傳入的引數中獲取,包括ContextModuleFactory或NormalModuleFactory)的create方法建立模組,獲取模組相關的parser、loader、hash等資料資訊。
  3. buildModule(): 呼叫module.build()進行module構建。
  4. addModuleDependencies(): 構建成功之後,遞迴地獲取依賴模組並構建(邏輯同_addModuleChain)。
  5. successEntry: 執行完上述的操作之後,在_addModuleChain的回撥函式裡邊呼叫succeedEntry鉤子,在這個鉤子裡邊可以獲取剛建立的module。然後將控制權返回給Compiler。

上述階段完成後,cimpiler呼叫了compilation的finish()、seal(),這裡我們重點關注seal方法。

  1. seal(): 該方法主要完成了chunk的構建。主要是收集modules、chunks,使用template(在Compilation的構建函裡初始化了幾種Template:MainTemplate、ChunkTemplate等)對chunk進行編譯。entry屬性配置的入口模組使用的是MainTemplate,裡邊會加入啟動webpack多需要的程式碼,建立並更新hash資訊等,並呼叫emitAsset()會將最終的資原始檔全部收集在assets物件裡邊。
  2. emitAsset(): 收集assets資源資料,多個外掛都有呼叫該方法。

Tapable改造

為方便原始碼的學習,想獲取webpack執行過程中鉤子的掛載及觸發情況,改造了Tapable。主要是修改Hook.js檔案。頂部需要引入fs庫。

const fs = require('fs')
複製程式碼
  1. 在call方法裡插入邏輯
_fnCp(fn, name, type) {
      const _fn = (...arg) => {
            // console.log('hahaha:', arg)
            fs.writeFileSync('/Users/eleme/Documents/my/test-webpack/calls.js', `${type}: ${name} \n`, { 'flag': 'a' },  () => {})
                  return fn(...arg)
            }
      return _fn
}
// 改造tap、tapAsync、tapPromise方法
tap(options, fn) {
      // ...
      // options = Object.assign({ type: "sync", fn: fn }, options);
      options = Object.assign({ type: "sync", fn: this._fnCp(fn, options.name, "sync") }, options);
      // ...
}
tapAsync(options, fn) {
      // ...
      // options = Object.assign({ type: "async", fn: fn }, options);
      options = Object.assign({ type: "async", fn: this._fnCp(fn, options.name, "async") }, options);
      // ...
}
tapPromise(options, fn) {
      // ...
      // options = Object.assign({ type: "promise", fn: fn }, options);
      options = Object.assign({ type: "promise", fn: this._fnCp(fn, options.name, "promise") }, options);
      // ...
}
複製程式碼
  1. 在tap方法插入邏輯
// 改造insert方法,在方法最後插入一條語句
_insert(item){
      fs.writeFileSync('/Users/eleme/Documents/my/test-webpack/taps.js', `${item.type}: ${item.name} \n`, { 'flag': 'a' },  () => {})
}
複製程式碼

總結

至此,我們大致理了一下webpack構建的脈絡。webpack體系非常龐大,內部封裝了很多webpack自己的庫。學習webpack原始碼的目的一個是學習好的構建思想,一個是方便自己在業務中開發外掛,這裡有原始碼註釋版及其他資料可供擴充。

相關文章