淺談Webpack的AMD外掛開發和執行機制

wind4gis發表於2018-01-30
參考:
  1. 細說 webpack 之流程篇
  2. webpack之plugin內部執行機制
  3. How webpack works
  4. github.com/webpack/tap…
  5. 玩轉webpack(一)上篇:webpack的基本架構和構建流程

怎麼寫webpack外掛

  在寫webpack的外掛時,內部必須要實現一個apply方法,傳入compiler引數,就可以通過呼叫plugin方法,在對應的事件鉤子訂閱事件,然後取出原始碼進行自定義開發後,return回去即可。

  這是因為webpack的內部機制是通過tapable來實現的,通過plugin方法訂閱事件,我們可以根據不同事件鉤子型別,執行對應的同步程式碼,或者觸發對應的非同步回撥--序列、並行、瀑布流(上一個操作的輸出是下一個操作的輸入,有點類似pipeLine),在非同步鉤子的回撥函式裡,要返回callback引數。

簡單的同步外掛示例如下:

class MyPlugin {
	apply(compiler) {
		compiler.plugin(“compilation”, compilation => {
			compilation.plugin(“optimize-modules”, modules => {
				modules.forEach(…);
			}
		}
	}
}
複製程式碼

Webpack執行機制

  我們可以看到,遵循對應的同步、非同步規則編寫簡單的webpack外掛其實是不難的,但是最難的怎麼找到合適的事件鉤子執行對應事件,也就是怎麼去理解webpack執行機制,找到合適突破口。

  理解webpack執行機制有幾個概念是需要注意一下:

  1. compiler-編譯器物件

      compiler物件代表了完整的webpack環境配置。該物件在啟動webpack時就被一次性建立,由webpack組合所有的配置項(包括原始配置,載入器和外掛)構建生成。

      當在webpack環境中應用一個外掛時,外掛會收到compiler的引用,通過使用compiler,外掛就可以訪問到整個webpack的環境(包括原始配置,載入器和外掛)。

  2. compilation-構建過程

      compilation物件在compiler的compile方法裡建立,它代表了一次單一的版本構建以及構建生成資源的彙總:

    1. compilation物件負責組織整個打包過程,包含了每個構建環節及輸出環節所對應的方法
    2. 該物件內部存放著所有module、chunk、生成的assets以及用來生成最後打包檔案的template的資訊

    當執行webpack-dev-server時,每當檢測到一個檔案變化,就會建立一次新的編譯,從而生成一組新的編譯資源。

  3. 外掛

      外掛本質上是被例項化的帶有apply原型方法的物件,其apply方法在安裝外掛時將被webpack編譯器呼叫一次,apply入參提供了一個compiler(編譯器物件)的引用,從而可以在外掛內部訪問到webpack的環境。

      具體使用方式,在webpack.config.js裡require對應的外掛,然後在module.exports物件的plugins陣列裡例項化對應外掛即可。

Compiler執行模式

Compiler有兩種執行模式,一種是run執行模式,另一種是watch監測模式(熱載入模式webpack-dev-server)。在執行模式下,compiler有三個主要的事件鉤子:

  1. compile/編譯

    開始編譯,建立對應的compilation物件。

  2. make/構建

    根據配置的不同Entry入口構建對應的chunk塊,並輸出assets資源。

  3. emit/提交

    將最終的assets資源寫入硬碟,輸出至指定的檔案路徑。

相比較,監測模式則分為兩部分:

  1. run執行模式
  2. 監測依賴如果發生改動,重新回到第一步

淺談Webpack的AMD外掛開發和執行機制

Compiler事件鉤子

在執行模式下,Compiler的事件鉤子如下:

  1. entry-option:生成不同的外掛應用

    解析傳給 webpack 的配置中的 entry 屬性,然後生成不同的外掛應用到 Compiler 例項上。這些外掛可能是 SingleEntryPlugin, MultiEntryPlugin 或者 DynamicEntryPlugin。但不管是哪個外掛,內部都會監聽 Compiler 例項物件的 make 任務點

  2. (before-)run:開始執行

    開始執行,啟動構建

  3. (before/after-)compile:編譯原始碼,建立對應的Compilation物件

    Compiler 例項將會開始建立 Compilation 物件,這個物件是後續構建流程中最核心最重要的物件,它包含了一次構建過程中所有的資料。也就是說一次構建過程對應一個 Compilation 例項。

  4. make:構建

  5. (after-)emit:輸出結果

  6. done:結束

在監測模式下則對應下面三個鉤子

  1. watch-run
  2. invalid
  3. watch-close

同時,在Compiler裡內嵌了compilation/編譯物件、normal-module-factory/常規模組工廠、context-module-factory/上下文模組工廠,可以在事件鉤子的回撥函式裡直接讀取。

淺談Webpack的AMD外掛開發和執行機制

Compilation事件鉤子

compiler的幾個事件鉤子貫穿了webpack的整個生命週期,但是具體到實際版本構建等操作,卻是由compilation來具體執行。compilation物件是指單一的版本構建以及構建生成資源的彙總,它是在compiler的compile/編譯事件裡被建立,重點負責後續的新增模組、構建模組,打包並輸出資源的具體操作(對應了compiler的make構建、emit輸出)。

addEntry開始構建模組

在compiler的make方法裡開始構建模組,對應了compilation的addEntry方法開始新增模組該方法實際上是呼叫了_addModuleChain/私有方法,根據模組的型別獲取對應的模組工廠並建立模組、構建模組。

淺談Webpack的AMD外掛開發和執行機制

首先,這裡用了工廠模式來建立module例項,compilation通過讀取每個module/模組的dependency/依賴資訊,在依賴裡讀取對應的模組工廠,然後在用工廠的create事件建立出module例項。

建立出module/模組後,再新增至compilation物件裡,然後由compilation來統籌模組的構建和module的依賴處理。

在處理module的依賴時,每一個依賴模組還是按照第一個步驟進行操作,迭代迴圈。直至模組及依賴完全處理完。

addModule新增模組

我們可以看看addModule事件裡發生了什麼事情,整個addModule裡分為兩塊:讀取依賴工廠,把模組新增至compilation。在後面一個步驟裡webpack專門做了一系列的優化邏輯。

淺談Webpack的AMD外掛開發和執行機制

  在addModule裡,compilation會通過identifier判斷是否已經有當前module,如果有則跳過;

  • 如果沒有的話,會在cache快取裡判斷是否有這個模組,如果也沒有那麼直接新增至compilation裡;
  • 如果有那麼就需要判斷快取裡的模組是否已過期,是否需要重構(通過timeStamps判斷),如果需要重構那麼就觸發disconnect事件,如果不需要重構,那麼觸發unbuild事件;
  • 不管是否需要重構,只要快取裡有這個模組,都要新增至compilation裡。

buildModule構建模組

經過上面的步驟,已經把所有的模組新增至compilation裡,接下來是由compilation統籌進行構建

淺談Webpack的AMD外掛開發和執行機制

構建模組是整個webpack最耗時的操作,在這裡分為幾個步驟:

  1. 讀取module/模組,呼叫對應Loader載入器進行處理,並輸出對應原始碼

    1. 遇到依賴時,遞迴地處理依賴的module/模組
    2. 把處理完的module/模組依賴新增至當前模組
  2. 呼叫acorn解析載入器輸出的原始檔,並生成抽象語法樹 AST

  3. 遍歷AST樹,構建該模組以及所依賴的模組

  4. 整合模組和所有對應的依賴,輸出整體的module

seal打包輸出模組

在構建完模組後,就會在回撥函式裡呼叫compilation的seal方法。Compilation的seal事件裡,會根據webpack裡配置的每個Entry入口開始打包,每個Entry對應地打包出一個chunk,逐次對每個module和chunk進行整理,生成編譯後的原始碼chunk,合併、拆分、生成hash,最終輸出Assets資源。

淺談Webpack的AMD外掛開發和執行機制

針對每一個Entry入口構建chunk的過程有點類似上面的buildModule,具體細節可以檢視流程圖。在這裡我們需要重點關注createChunkAssets生成最終的資源並輸出至指定檔案路徑。

淺談Webpack的AMD外掛開發和執行機制

在整個createChunkAssets過程裡,有個有意思的地方需要注意,就是mainTemplate、chunkTemplate和moduleTemplate。template是用來處理上面輸出的chunk,打包成最終的Assets資源。但是mainTemplate是用來處理入口模組,chunkTemplate是處理非入口模組,即引用的依賴模組。

通過這兩個template的render處理輸出source原始碼,都會彙總到moduleTemplate進行render處理輸出module。接著module呼叫source抽象方法輸出assets,最終由compiler統一呼叫emitAssets,輸出至指定檔案路徑。

淺談Webpack的AMD外掛開發和執行機制

我們需要注意一下mainTemplate的render(render-with-entry)方法,整個webpack通過該方法找到整個入口開始構建,引用依賴。如果我們想自定義AMD外掛,把整個webpack編譯結果包裹起來,那麼我們可以通過mainTemplate的render-with-entry方法,進行自定義開發。

關於webpack執行機制

關於webpack執行機制的理解,建議大家有空再看看下面兩個流程圖,講得很仔細,有助於理解。

淺談Webpack的AMD外掛開發和執行機制

淺談Webpack的AMD外掛開發和執行機制

編寫AMD外掛

假設我們現在要寫一個外掛使得其能夠生成 AMD 模組,那麼我們要怎麼操作?我們可能會有下面這些疑惑。

淺談Webpack的AMD外掛開發和執行機制

具體開發的思路,是通過在webpack打包輸出最終資源時,從入口模組構建的節點入手,訂閱compilation.mainTemplate的render-with-entry事件,在裡邊用AMD宣告包裹住原始碼,然後返回即可。這樣子後續展開依賴模組時也會統一被AMD宣告所包裹。

具體外掛原始碼如下:

/** plugin.js **/
const ConcatSource = require('webpack-sources').ConcatSource,
      path = require('path')

class DefPlugin {
  constructor(name) {
  }

  apply(compiler) {
    compiler.plugin('compilation', (compilation)=>{
      compilation.mainTemplate.plugin('render-with-entry', function(source, chunk, hash){
        return new ConcatSource(`global.define(['require','module','exports'],function(require, module, exports) { 
          ${source.source()} 
        })`);
      }) 
    })
  }
}

module.exports = DefPlugin
複製程式碼

實際呼叫時,只需要在webpack.config.js的plugins陣列裡例項化該外掛即可。

/** webpack.config.js **/
const path = require('path')
const Plugin = require('./plugin')

module.exports = {
    context: path.join(__dirname, 'src'),
    entry: './index.js',
    output: {
        libraryTarget: 'commonjs2',
        path: path.join(__dirname, 'dist'),
        filename: './bundle.js'
    },
    plugins: [
        new Plugin
    ]
}
複製程式碼

相關文章