webpack 源碼探索之插件機制

最近在一直在爲面試作準備,搜了不少大佬記錄的面試經驗和麪試內容,對本身不太熟悉和已經記憶模糊的知識點內容進行復習鞏固,爭取可以有一個好的狀態。這篇文章以個人經驗講述了我是如何從源碼的角度瞭解到 webpack 插件機制,也簡單描述了 webpack 編譯構建的機制。css

使用 vscode 調試功能,運行項目打包程序,一步一步走 webpack(version: 3.10.0)執行代碼。node

先來看看 webpack 函數源碼webpack

function webpack(options, callback) {
	const webpackOptionsValidationErrors = validateSchema(webpackOptionsSchema, options);
	if(webpackOptionsValidationErrors.length) {
		throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
	}
	let compiler;
	if(Array.isArray(options)) {
		compiler = new MultiCompiler(options.map(options => webpack(options)));
	} else if(typeof options === "object") {
		// TODO webpack 4: process returns options
		new WebpackOptionsDefaulter().process(options);

		compiler = new Compiler();
		compiler.context = options.context;
		compiler.options = options;
		new NodeEnvironmentPlugin().apply(compiler);
		if(options.plugins && Array.isArray(options.plugins)) {
			compiler.apply.apply(compiler, options.plugins);
		}
		compiler.applyPlugins("environment");
		compiler.applyPlugins("after-environment");
		compiler.options = new WebpackOptionsApply().process(options, compiler);
	} else {
		throw new Error("Invalid argument: options");
	}
	if(callback) {
		if(typeof callback !== "function") throw new Error("Invalid argument: callback");
		if(options.watch === true || (Array.isArray(options) && options.some(o => o.watch))) {
			const watchOptions = Array.isArray(options) ? options.map(o => o.watchOptions || {}) : (options.watchOptions || {});
			return compiler.watch(watchOptions, callback);
		}
		compiler.run(callback);
	}
	return compiler;
}
複製代碼

源碼分析

首先檢查 webpack 配置是否有符合要求

const webpackOptionsValidationErrors = validateSchema(webpackOptionsSchema, options);
if(webpackOptionsValidationErrors.length) {
  throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
}
複製代碼

validateSchema 是一個依賴 ajv(JSON 模式驗證器) 插件的對 webpack 配置進行驗證的函數,須要提供一份 webpack 配置的 JSON 格式的驗證描述 webpackOptionsSchema 和咱們項目的配置信息 options,先用 ajv 的 compile 方法編譯 webpackOptionsSchema 獲得驗證器,再用驗證器驗證 options 並返回驗證結果。相似於 React 中使用的 prop-types 驗證父組件傳給子組件的屬性和 Vue 中的 props 自定義驗證。git

根據項目配置 options 肯定 compiler 對象

這裏須要先着重介紹下 compiler 對象,它對咱們瞭解 webpack 的構建機制和接下來的講解相當重要,須要理解它究竟是什麼,有什麼做用。先借用官網的一些介紹:github

compiler 對象表明了完整的 webpack 環境配置。這個對象在啓動 webpack 時被一次性創建,並在全部可操做的設置中被配置,包括原始配置,loader 和插件。當在 webpack 環境中應用一個插件時,插件將收到一個編譯器對象的引用。可使用它來訪問 webpack 的主環境。web

compiler 對象在 webpack 構建過程當中表明着整個 webpack 環境,包含上下文、項目配置信息、執行、監聽、統計等等一系列的信息,提供給 loader 和插件使用。它繼承於 Tapable(Tapable 是 webpack 的一個底層庫,相似於 NodeJS 的 EventEmitter 類),使用事件的發佈 compiler.applyPlugins('eventName') 訂閱compiler.plugin('eventName', callback) 模式註冊 new WebpackPlugin().apply(compiler) 全部插件,插件必須提供 apply 方法給 webpack 完成註冊流程,插件在 apply 方法內作一些初始化操做並監聽 webpack 構建過程當中的生命週期事件,等待構建時生命週期事件的發佈。面試

全部插件都會在構建方法 compiler.run(callback) 以前註冊,當 webpack 構建到某個階段就會發佈一個生命週期事件,此時全部訂閱了當前發佈的生命週期事件的插件會按照註冊順序一個一個執行訂閱時提供的回調函數,回調函數的參數是與發佈的生命週期事件相對應的參數,好比經常使用的 compilation 生命週期事件回調函數參數就包含 compilation 對象(此對象也是 webpack 構建機制的重要成員),entry-option 生命週期事件回調函數參數是 context(項目上下文路徑)和 entry(項目配置的入口對象)。另外,插件若是須要異步執行編譯,則還會提供一個回調函數做爲監聽回調函數的參數,異步編譯完成必須調用回調函數。json

簡單點說,webpack 的構建包含不少個階段,每一個階段都會發布對應的生命週期事件,插件須要提供 apply 方法註冊並在此方法內監聽指定的生命週期事件,事件發佈後會順序執行監聽的回調函數並提供相對應的參數。數組

OK,瞭解完 compiler 對象後繼續看代碼。app

if(Array.isArray(options)) {
  compiler = new MultiCompiler(options.map(options => webpack(options)));
} else if(typeof options === "object") {
  // ...
  compiler = new Compiler();
  // ...
}
複製代碼

這裏有一層判斷決定 compiler 對象是 MultiCompiler 類仍是 Compiler 類的實例,兩者的關係實際上是一層包裝關係,從代碼能夠看出,MultiCompiler 類的參數是多個 Compiler 實例成員組成的數組,再進入到 MultiCompiler.js 查看源碼會發現,在 MultiCompiler 的構造函數中,實例有個 compilers 屬性指向這個由多個 Compiler 實例成員組成的數組,以及遍歷 compilers 數組給每一個 compiler 成員註冊(監聽) doneinvalid 兩個生命週期事件。

對於 outputPath、inputFileSystem、outputFileSystem 這三個屬性,會使用取值函數(getter)和存值函數(setter)進行攔截(outputPath 只有取值函數),查看 Compiler 源碼 Compiler 類的構造函數,會發現這三個屬性都是 Compiler 實例上的屬性,再看 MultiCompiler 中這三個屬性的存取值函數會發現,都是在遍歷 MultiCompiler 實例的 compilers 屬性對每個 compiler 成員作相應的存取值操做(其中 inputFileSystem 和 outputFileSystem 的取值函數是拋出錯誤),也就是說對 MultiCompiler 實例的 inputFileSystem 和 outputFileSystem 屬性賦值其實就是對全部 Compiler 實例的 inputFileSystem 和 outputFileSystem 屬性賦值。MultiCompiler 類還覆寫了 Compiler 類中的 watch、run、purgeInputFileSystem 三個方法,無一例外也都是遍歷 compilers 讓每一個 compiler 成員執行與之相對應的方法。

通常狀況下,咱們項目配置的 options 是一個 object,至於什麼狀況下會使用到數組,我我的見解是一個大的項目中包含多個小的子項目,須要可以單獨打包小的子項目,也須要可以一次打包整個大的項目,這時候 Array.isArray(options) === true 才應用到 MultiCompiler。

new WebpackOptionsDefaulter().process(options);
複製代碼

咱們以 typeof options === "object" 常規項目爲例,代碼往下執行,首先建立了 WebpackOptionsDefaulter 實例,而後立刻執行 process 方法並傳入 options 做爲參數。查看 WebpackOptionsDefaulter 源碼 會發現,它自己只有一個構造函數,構造函數內大規模使用 this.set() 方法初始化 webpack options 默認配置,這個 set 方法是來自它所繼承的 OptionsDefaulter 類,執行的 process 方法也是出自 OptionsDefaulter 類,能夠說 WebpackOptionsDefaulter 類只是一層外殼,設置全部的 webpack 默認配置信息,藉由 procss 方法將 webpack 默認的配置信息與項目配置信息融合,提供出接下來須要使用的 options。

compiler = new Compiler();
compiler.context = options.context;
compiler.options = options;
複製代碼

初始化 webpack options 後,建立 compiler 實例並設置 context(項目上下文路徑)和 options(項目配置)。

new NodeEnvironmentPlugin().apply(compiler);
複製代碼

上文對 compiler 對象的介紹已經清楚的解釋了上面一行代碼的行爲,就是註冊一個 NodeEnvironmentPlugin 插件,查看源碼發現很是簡單,以下:

class NodeEnvironmentPlugin {
	apply(compiler) {
		compiler.inputFileSystem = new CachedInputFileSystem(new NodeJsInputFileSystem(), 60000);
		const inputFileSystem = compiler.inputFileSystem;
		compiler.outputFileSystem = new NodeOutputFileSystem();
		compiler.watchFileSystem = new NodeWatchFileSystem(compiler.inputFileSystem);
		compiler.plugin("before-run", (compiler, callback) => {
			if(compiler.inputFileSystem === inputFileSystem)
				inputFileSystem.purge();
			callback();
		});
	}
}
複製代碼

apply 方法執行插件初始化操做修改了 compiler 的 inputFileSystem、outputFileSystem、watchFileSystem 這三個屬性的值,而後監聽 before-run 事件,顧名思義,這個事件是在 compiler.run(callback) 函數執行會被髮布的。查看回調函數內部代碼,執行了 callback() 方法,馬上能想到這是一個異步編譯回調,以前有一層 inputFileSystem 引用的判斷,若是 before-run 事件發佈以前 compiler 的 inputFileSystem 被修改從新賦值,則不作任何操做直接執行 callback;若是沒有被修改從新賦值,則運行 purge 方法。大意就是若是有其它插件(指項目配置的插件)提供了 inputFileSystem 對象,就用其它插件的,若是沒有,那就由我來接管了。

if(options.plugins && Array.isArray(options.plugins)) {
  compiler.apply.apply(compiler, options.plugins);
}
複製代碼

首先判斷是否有 plugins 屬性而且是否爲數組,判斷語句內的語法稍微有一點繞,咋一眼看上去有些懵,但稍微想一下應該就能明白,就是執行 compiler 的 apply 方法而且綁定 this 爲 compiler 對象,再傳入 options.plugins(項目配置的全部插件)做爲參數。這裏使用 apply 方法的目的其實不是爲了綁定 this,由於自己 compiler.apply() 方法執行上下文中的 this 指向的就是 compiler,這裏主要目的是爲了把 options.plugins 解構爲一個一個的參數。

Tapable.prototype.apply = function apply() {
	for(var i = 0; i < arguments.length; i++) {
		arguments[i].apply(this);
	}
};
複製代碼

由於 Compiler 類繼承自 Tapable 類,Compiler 實例上的 apply 方法調用的是 Tapable.prototype.apply,經過上面代碼能夠清楚的看到,之因此要解構 options.plugins 是由於要遍歷 arguments 對象,讓 arguments 成員(插件實例)調用自身的 apply 方法(不是 Function.prototype.apply 方法)執行註冊流程,傳入 this 也就是 compiler 做爲參數。

至此,插件註冊的流程已經很是清晰明瞭,對於開始動手寫一個 webpack 插件應該沒有什麼畏懼啦。

compiler.applyPlugins("environment");
compiler.applyPlugins("after-environment");
複製代碼

若是已經理解 compiler 對象那上面兩行代碼是能夠略過的,就是發佈 compiler.applyPlugins 兩個生命週期事件:environment 和 after-environment,若是項目配置的插件中有監聽這兩個事件的插件則會執行監聽回調函數,只提供了默認的 compiler 做爲參數,沒有任何其餘參數。

compiler.options = new WebpackOptionsApply().process(options, compiler);
複製代碼

這裏是對 compiler.options 從新賦值操做,那意思就很明白了,又要對 options 參數作一系列的操做,爲何說又?由於以前執行 new WebpackOptionsDefaulter().process(options); 已經對 options 作過一次初始化操做,融合了 webpack 的默認配置與項目配置的結果,那此次操做 options 又是爲了什麼呢?查看 WebpackOptionsApply 源碼 發現 WebpackOptionsApply 繼承自 OptionsApply,再查看 OptionsApply 源碼 發現 OptionsApply 無關緊要...,不太清楚爲何要寫這麼一個空類,或許是爲 webpack v4 作準備,這兒咱們就先無論,意義不大。再回到 WebpackOptionsApply 類的 process 方法,嘩啦啦的一串,將近 300 行,這裏我就不貼代碼了,我大概描述一下 process 方法內作了哪些事情。

  1. 把一些 options 上的屬性賦值給 compiler 對象

  2. 根據 options.target 的值註冊相應的 webpack 內部插件

    options.target 配置的意思是告訴 webpack 構建應用於什麼環境的代碼,它的默認值是 web,另外還有 webworkernodeasync-nodenode-webkitatomelectronelectron-mainelectron-renderer

  3. 根據 options 的配置肯定是否要註冊一些內部插件

    好比若是配置了 externals 屬性須要則註冊 ExternalsPlugin 插件

  4. 肯定 compiler.resolvers 三個屬性 normalcontextloader 的值

  5. 發佈三個生命週期事件:entry-optionafter-pluginsafter-resolvers

  6. 返回 options

大體就是在完善 compiler 對象,根據當前項目配置應用一些相對應的內部插件,從這裏能夠看出,webpack 內部也大量運用插件機制來實現編譯構建,插件機制讓 webpack 變得靈活而強大。

if(callback) {
  if(typeof callback !== "function") throw new Error("Invalid argument: callback");
  if(options.watch === true || (Array.isArray(options) && options.some(o => o.watch))) {
    const watchOptions = Array.isArray(options) ? options.map(o => o.watchOptions || {}) : (options.watchOptions || {});
    return compiler.watch(watchOptions, callback);
  }
  compiler.run(callback);
}
return compiler;
複製代碼

接下來,判斷是否有 callback 參數,若是沒有則直接返回 compiler 對象,注意!此時 webpack 並無執行構建程序,也不會執行,由於構建程序 compiler.run(callback) 方法只有當有 callback 參數時纔會執行,而且 callback 必須爲函數。若是開啓 watch 模式,則 webpack 會監聽文件變化,當文件發生變更則會觸發從新編譯,像 webpack-dev-server 和 webpack-dev-middleware 裏 watch 模式是默認開啓的,方便進行開發。

最後執行 compiler.run(callback); 表示開始構建,至此,webpack 構建前的初始化操做已經所有完成,接下來要探索的就是 run 方法是如何執行 webpack 構建的。

本文篇幅已經很長,run 方法內的構建過程不是三言兩語可以描述清楚,這裏也不打算細說,大概羅列幾個關鍵事件節點:

  1. compile: 開始編譯

  2. make: 從入口點分析模塊及其依賴的模塊並建立這些模塊對象

  3. build-module: 構建模塊

  4. after-compile: 完成構建

  5. after-compile 完成構建

  6. emit: 把各個chunk輸出到結果文件

  7. after-emit: 完成輸出

另外 webpack 構建還有一個關鍵對象 compilation,上文介紹 compiler 時有提到,他們倆是理解和擴展 webpack 引擎的關鍵,是 webpack 插件必不可缺的組成部分。

compilation 對象表明了一次單一的版本構建和生成資源。當運行 webpack 開發環境中間件時,每當檢測到一個文件變化,一次新的編譯將被建立,從而生成一組新的編譯資源。一個編譯對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息。編譯對象也提供了不少關鍵點回調供插件作自定義處理時選擇使用。

compiler 是 webpack 環境的表明,compilation 則是 webpack 構建內容的表明,它包含了每一個構建環節及輸出環節所對應的方法,存放着全部 module、chunk、asset 以及用來生成最後打包文件的 template 的信息。

最後,附上一張淘寶 FED 團隊在《細說 webpack 之流程篇》 一文中的 webpack 總體流程圖:

webpack 總體流程圖
相關文章
相關標籤/搜索