淺談Webpack的AMD插件開發和運行機制

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

怎麼寫webpack插件

  在寫webpack的插件時,內部必需要實現一個apply方法,傳入compiler參數,就能夠經過調用plugin方法,在對應的事件鉤子訂閱事件,而後取出源碼進行自定義開發後,return回去便可。webpack

  這是由於webpack的內部機制是經過tapable來實現的,經過plugin方法訂閱事件,咱們能夠根據不一樣事件鉤子類型,執行對應的同步代碼,或者觸發對應的異步回調--串行、並行、瀑布流(上一個操做的輸出是下一個操做的輸入,有點相似pipeLine),在異步鉤子的回調函數裏,要返回callback參數。git

簡單的同步插件示例以下:github

class MyPlugin {
	apply(compiler) {
		compiler.plugin(「compilation」, compilation => {
			compilation.plugin(「optimize-modules」, modules => {
				modules.forEach(…);
			}
		}
	}
}
複製代碼

Webpack運行機制

  咱們能夠看到,遵循對應的同步、異步規則編寫簡單的webpack插件實際上是不難的,可是最難的怎麼找到合適的事件鉤子執行對應事件,也就是怎麼去理解webpack運行機制,找到合適突破口。web

  理解webpack運行機制有幾個概念是須要注意一下:segmentfault

  1. compiler-編譯器對象數組

      compiler對象表明了完整的webpack環境配置。該對象在啓動webpack時就被一次性建立,由webpack組合全部的配置項(包括原始配置,加載器和插件)構建生成。緩存

      當在webpack環境中應用一個插件時,插件會收到compiler的引用,經過使用compiler,插件就能夠訪問到整個webpack的環境(包括原始配置,加載器和插件)。架構

  2. compilation-構建過程app

      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. 監測依賴若是發生改動,從新回到第一步

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/上下文模塊工廠,能夠在事件鉤子的回調函數裏直接讀取。

Compilation事件鉤子

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

addEntry開始構建模塊

在compiler的make方法裏開始構建模塊,對應了compilation的addEntry方法開始添加模塊該方法其實是調用了_addModuleChain/私有方法,根據模塊的類型獲取對應的模塊工廠並建立模塊、構建模塊。

首先,這裏用了工廠模式來建立module實例,compilation經過讀取每一個module/模塊的dependency/依賴信息,在依賴裏讀取對應的模塊工廠,而後在用工廠的create事件建立出module實例。

建立出module/模塊後,再添加至compilation對象裏,而後由compilation來統籌模塊的構建和module的依賴處理。

在處理module的依賴時,每個依賴模塊仍是按照第一個步驟進行操做,迭代循環。直至模塊及依賴徹底處理完。

addModule添加模塊

咱們能夠看看addModule事件裏發生了什麼事情,整個addModule裏分爲兩塊:讀取依賴工廠,把模塊添加至compilation。在後面一個步驟裏webpack專門作了一系列的優化邏輯。

  在addModule裏,compilation會經過identifier判斷是否已經有當前module,若是有則跳過;

  • 若是沒有的話,會在cache緩存裏判斷是否有這個模塊,若是也沒有那麼直接添加至compilation裏;
  • 若是有那麼就須要判斷緩存裏的模塊是否已過時,是否須要重構(經過timeStamps判斷),若是須要重構那麼就觸發disconnect事件,若是不須要重構,那麼觸發unbuild事件;
  • 不論是否須要重構,只要緩存裏有這個模塊,都要添加至compilation裏。

buildModule構建模塊

通過上面的步驟,已經把全部的模塊添加至compilation裏,接下來是由compilation統籌進行構建

構建模塊是整個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資源。

針對每個Entry入口構建chunk的過程有點相似上面的buildModule,具體細節能夠查看流程圖。在這裏咱們須要重點關注createChunkAssets生成最終的資源並輸出至指定文件路徑。

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

經過這兩個template的render處理輸出source源碼,都會彙總到moduleTemplate進行render處理輸出module。接着module調用source抽象方法輸出assets,最終由compiler統一調用emitAssets,輸出至指定文件路徑。

咱們須要注意一下mainTemplate的render(render-with-entry)方法,整個webpack經過該方法找到整個入口開始構建,引用依賴。若是咱們想自定義AMD插件,把整個webpack編譯結果包裹起來,那麼咱們能夠經過mainTemplate的render-with-entry方法,進行自定義開發。

關於webpack運行機制

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

編寫AMD插件

假設咱們如今要寫一個插件使得其可以生成 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
    ]
}
複製代碼
相關文章
相關標籤/搜索