從Webpack原始碼探究打包流程,萌新也能看懂~

小美娜娜發表於2018-12-01

簡介

上一篇講述瞭如何理解tapable這個鉤子機制,因為這個是webpack程式的靈魂。雖然鉤子機制很靈活,而然卻變成了我們讀懂webpack道路上的阻礙。每當webpack執行起來的時候,我的心態都是佛繫心態,祈禱中間不要出問題,不然找問題都要找半天,還不如不打包。尤其是loader和plugin的執行機制,這兩個是在什麼時候觸發的,作用於webpack哪一個環節?這些都是需要熟悉webpack原始碼才能有答案的問題。

大家就跟著我一步步揭開webpack的神祕面紗吧。

如何除錯webpack

本小節主要描述了,如何除錯webpack,如果你有自成一派的除錯方法,或者更加主流的方法,可以留言討論討論。

簡易版webpack啟動

工欲善其事,必先利其器。我相信大家剛學習webpack的時候一定是跟著官方文件執行webpack打包網站。

webpack上手文件,->萌新指路

初級操作應該依賴webpack-cli,通過在小黑框中輸入npx webpack --config webpack.config.js,然後enter執行打包。雖然webpack-cli會幫助我們把大多數打包過程中會出現的問考慮進去,但是這樣會使我們對webpack的原始碼更加陌生,似乎配置就是一切。

這種尷尬的時候,我們就要另闢蹊徑來開發,並不用官方的入門方法。

我寫的一個簡易啟動webpack的除錯程式碼,如下方所示:

//載入webpack主體
let webpack=require('webpack');
//指定webpack配置檔案
let config=require("./webpack.config.js");
//執行webpack,返回一個compile的物件,這個時候編譯並未執行
let compile=webpack(config);
//執行compile,執行編譯
compile.run();
複製程式碼

如果大家想知道我這段程式碼的靈感來源於哪裡?我會告訴大家是來自webpack-cli。

挑出關鍵執行的部分,然後重組就可以做一個簡易的webpack啟動了。

話嘮筆者:我為什麼要這麼做?程式碼越少分析起來越簡單,“無關”程式碼越多,我們的視線就會被這些程式碼所困住而寸步難行。當然等到這部分掌握了,再去看cli的程式碼,也許收穫會更大一些。

配置的溫馨提醒

雖然我們都會配置Entry,但是我們可能會忽略Context的配置,如果我們的cmd在當下的目錄,那麼執行是OK的,但是如果我們不在當前目錄下,然後執行,那麼很有可能路徑會出現問題,為了防止遮掩的悲劇產生,我推薦機上context配置也就是context:你當前專案的絕對路徑

module.exports = {
  //...
  context: path.resolve(__dirname, 'app')
};
複製程式碼

打斷點!debugger

關鍵部分來了,寫一個簡易個webpack主要就是為了方便打斷點!增加程式的可讀性。

非vscode玩家入口

如果你是小黑框(termial)和chrome的愛好者,以下方法請收下!點選獲取參考文件,這裡有詳細的操作過程。

node --inspect-brk debugger1.js
複製程式碼

然後我們就可以愉快地像除錯網頁一樣在親切的chrome上玩耍了。但是問題來了,沒有斷點的除錯,太可怕了,雖然每一步都顯示非常地好,不過我並不想知道fs的讀取,timer的執行和模組的載入等node原生方法,next的點選了幾百下,webpack主流程並沒有走幾步,這極大的挑戰了我的耐心,如果有小夥伴一步步next到了最後一步,希望你能來和我們分享一下。為了防止過於細節,這個時候我們可以在適當的地方打斷點:

options = new WebpackOptionsDefaulter().process(options);
debugger//是他是他就是他,我們的救星
compiler = new Compiler(options.context);
複製程式碼

WebpackOptionsDefaulter執行之後,程式便會自動停下任君除錯。

vscode的玩家

如果是vscode的玩家,除了上述的debugger方法,我們還可以直接打紅點,作為斷點,這樣更加方便。最後還可以一鍵清除所有的斷點。

同時也可以在當前斷點的時候,在除錯控制檯,輸入自己想要了解的引數。

從Webpack原始碼探究打包流程,萌新也能看懂~

webpack主流程是什麼

對於webpack的主流程的解釋,我分為了以下三種:

簡介版本:webpack的過程就通過Compiler發號施令,Compilation專心解析,最後返回Compiler輸出檔案。

從Webpack原始碼探究打包流程,萌新也能看懂~

專業版本:webpack的過程是通過Compiler控制流程,Compilation專業解析,ModuleFactory生成模組,Parser解析原始碼,最後通過Template組合模組,輸出打包檔案的過程。

粗暴版本:webpack就是打散原始碼再重組的過程。

從Webpack原始碼探究打包流程,萌新也能看懂~

原始碼解讀

我們直接開始從專業版本來理解webpack吧。從上方的啟動程式碼我們可以看到webpack(config)是啟動webpack打包的關鍵程式碼,也就是webpack.js是我們第一個研究物件。

因為筆者各種除錯webpack,各種斷點,導致原始碼的行數和線上的行數不一致,所以這裡我會直接丟擲程式碼而不是行數,大家自行對著webpack的原始碼對照。

一切的源頭webpack.js

大家以為我會從第一步引入開始解析嗎?不存在的,我們直接從關鍵邏輯開始吧。

options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin().apply(compiler);
···省略自定義外掛的繫結
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
compiler.options = new WebpackOptionsApply().process(options, compiler);
複製程式碼

是不是覺得不知所云,不要慌,我們一行行看下來,這裡的每一行都很重要。

options = new WebpackOptionsDefaulter().process(options);這一行的關鍵字Default,通過關鍵字我們可以猜測到這個類的作用就是將我們webpack.config.js中自定義的部分,覆蓋webpack預設的配置。

挑一行這個類中的程式碼,便於大家理解。

this.set("entry", "./src");
複製程式碼

這個就是入口的預設配置,如果我們不配入口,程式就會自動找src下方的檔案打包。

話癆的筆者:webpack4.0有一個很大的特色就是零配置,無需webpack.config.js我們都可以打包。為什麼呢?難道是webpack真的不需要配置了嗎?做到人工智慧了?不!因為有預設配置,就像所有的程式都有初始化的預設配置。

new Compiler(options.context),非常重要的編譯器,基本上編譯的流程就是出自這個類。 options.context這個值是當前資料夾的絕對路徑,通過WebpackOptionsDefaulter.js預設配置的程式碼片段的程式碼片段既可以理解。這個類稍後分析。

this.set("context", process.cwd());
複製程式碼

然後就是一系列,對於compiler的配置以及將NodeEnvironmentPlugin的hooks以及自定義的外掛plugins也是鉤子分別掛入compiler之中,掛入之後觸發environment的一些鉤子。相當於開車前會啟動車子一樣。比如在解析檔案(resolver)時一定會用到的檔案系統,如何讀取檔案。這個就是將inputFileSystem輸入檔案系統掛載了compiler上,然後通過compiler來控制那些外掛需要這個功能,就派發給他。

class NodeEnvironmentPlugin {
	apply(compiler) {
		compiler.inputFileSystem = new CachedInputFileSystem(
			new NodeJsInputFileSystem(),
			60000
		);
		//....
		compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
			if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
		});
	}
}
module.exports = NodeEnvironmentPlugin;

複製程式碼

compiler.options = new WebpackOptionsApply().process(options, compiler);,這裡又對options做處理的,如果說第一步是格式化配置,那麼這邊就是將配置在compiler中啟用。這個類很重要,因為compiler中的啟用了許多鉤子,同時在一些鉤子上掛上(tap)了函式。

關鍵配置options啟用解析:

  • 這個是parse的一個解析器,如果檔案是js,就會使用到這個parse,也就是說這個是在loader的時候進行的。

    new JavascriptModulesPlugin().apply(compiler);
    複製程式碼
  • 這一行是用於解析也就是入口的解析,是SingleEntryPlugin還是MultiEntryPlugin。這個方法相當於入口程式已經就緒,就等後續的一聲令下就可以執行了。

    new EntryOptionPlugin().apply(compiler);
    compiler.hooks.entryOption.call(options.context, options.entry);
    複製程式碼
  • 當外掛鉤子都掛上後,執行的鉤子。

    compiler.hooks.afterPlugins.call(compiler);
    複製程式碼
  • 接著是各類路徑解析的鉤子,根據我們的自定義resolver來解析。

    compiler.resolverFactory.hooks.resolveOptions
    複製程式碼

關鍵點突破Compiler.js

從Webpack原始碼探究打包流程,萌新也能看懂~

可以說Compiler.js這個類才是真正得控制了webpack打包的流程,如果說webpack.js所做的事是準備,那麼Compiler就是擼起袖子就是幹。

constructor

我們從constructor開始解析Compiler

Compiler首先是定義了一堆鉤子,如果大家觀察仔細會發現這就是流程的各個階段(此處的程式碼可讀性很友好),也就是各個階段都有個鉤子,這意味著什麼?我們可以利用這些鉤子掛上我們的外掛,所以說Compiler很重要。

關鍵鉤子 鉤子型別 鉤子引數 作用
beforeRun AsyncSeriesHook Compiler 執行前的準備活動,主要啟用了檔案讀取的功能。
run AsyncSeriesHook Compiler “機器”已經跑起來了,在編譯之前有快取,則啟用快取,這樣可以提高效率。
beforeCompile AsyncSeriesHook params 開始編譯前的準備,建立的ModuleFactory,建立Compilation,並繫結ModuleFactory到Compilation上。同時處理一些不需要編譯的模組,比如ExternalModule(遠端模組)和DllModule(第三方模組)。
compile SyncHook params 編譯了
make AsyncParallelHook compilation 從Compilation的addEntry函式,開始構建模組
afterCompile AsyncSeriesHook compilation 編譯結束了
shouldEmit SyncBailHook compilation 獲取compilation發來的電報,確定編譯時候成功,是否可以開始輸出了。
emit AsyncSeriesHook compilation 輸出檔案了
afterEmit AsyncSeriesHook compilation 輸出完畢
done AsyncSeriesHook Stats 無論成功與否,一切已塵埃落定。

Compiler.run()

從函式的名稱我們大致可以猜出他的作用,不過還是從Compiler的執行流程來加深對Compiler的理解。Compiler.run()開跑!

首先觸發beforeRun這個async鉤子,在這個鉤子中繫結了讀取檔案的物件。接著是run這個async鉤子,在這個鉤子中主要是處理快取的模組,減少編譯的模組,加速編譯速度。之後才會進去入Compiler.compile()的編譯環節。

this.hooks.beforeRun.callAsync(this, err => {
    ....
	this.hooks.run.callAsync(this, err => {
	    ....
		this.compile(onCompiled);
		....
	});
	....
});
複製程式碼

等Compiler.compile執行結束之後會回撥run中名為onCompiled的函式,這個函式的作用就是將編譯後的內容生成檔案。我們可以看到首先是shouldEmit判斷是否編譯成功,未成功則結束done,列印相應資訊。成功則呼叫Compiler.emitAssets打包檔案。

if (this.hooks.shouldEmit.call(compilation) === false) {
	...
	this.hooks.done.callAsync(stats, err => {
		...
    }
    return
	
}
this.emitAssets(compilation, err => {
    ...
    if (compilation.hooks.needAdditionalPass.call()) {
    ...
	    this.hooks.done.callAsync(stats, err => {});
    };
})

複製程式碼

Compiler.compile()

上一節只討論了Compiler.run方法的整體流程,並未提及Compiler.compile,這個compiler顧名思義就是編譯的意思。那麼編譯的過程中究竟發生了寫什麼呢?

const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
    ...
	this.hooks.compile.call(params);
	const compilation = this.newCompilation(params);
    this.hooks.make.callAsync(compilation, err => {
    	...
        compilation.finish();
        ompilation.seal(err => {
			...	
			this.hooks.afterCompile.callAsync(compilation, err => {
			    ...
			    此處是回撥函式,這個函式主要用於將編譯成功的程式碼輸出
    			...
			});
		});
	});
});
複製程式碼

首先是定義了params並傳入了hooks.compile這個鉤子中,params就是模組工廠,其中最常用的就是normalModuleFactory,將這個工廠傳入鉤子中,方便之後的外掛或鉤子操作模組。

鉤子想要和程式產生聯絡,比如在compiler中加內容,就需要將Compiler傳入鉤子中,才可行,否則並無介面暴露給外掛。

然後是beforeCompile預備一下,接著就是啟動compile這個鉤子。

這裡新建了Compilation,一個很重要的專注於編譯的類。

hooks.make這個鉤子就是正式啟動編譯了,所以這個鉤子執行完畢就意味這編譯結束了,可以進行封裝seal了。那麼make這個鉤子觸發的時候,執行了那些步驟呢?

大家是否還記得在webpack.js中提到過的EntryOptionPlugin

    new EntryOptionPlugin().apply(compiler);
    compiler.hooks.entryOption.call(options.context, options.entry);
複製程式碼

從Webpack原始碼探究打包流程,萌新也能看懂~

來自筆者的話癆:webpack的模組構建其實是通過entry,也就是入口檔案開始分析,開始構建。也就是說一個入口檔案會觸發一次Compliation.addEntry,然後觸發之後就是Compilation開始構建模組了。

EntryOptionPlugin是幫助我們處理入口型別的外掛,他會webpack.config.js中entry的不同配置幫助我們搭配不同的EntryPlugin。通過entry配置進入的一共有3種型別,SingleEntryPlugin,MultiEntryPlugin和DynamicEntryPlugin,根據名字就能夠輕易區分他們的型別。一般一個compiler只會觸發一個EntryPlugin,然後在這個EntryPlugin中,會有我們構建模組的入口,也就是compilation的入口。

compiler.hooks.make.tapAsync("SingleEntryPlugin|MultiEntryPlugin|DynamicEntryPlugin",(compilation, callback) => { 
    ...
	compilation.addEntry(context, dep, name, callback);
	...
});
複製程式碼

除了幫助我們開啟compilation的大門之外,???EntryPlugin還繫結了一個事件就是,當前入口的模組工廠型別。

compiler.hooks.compilation.tap("SingleEntryPlugin",(compilation, { normalModuleFactory }) => {
	compilation.dependencyFactories.set(
		SingleEntryDependency,
		normalModuleFactory
	);
});
複製程式碼

這個鉤子函式幫我們定義了SingleEntry的模組型別,那麼之後compliation編譯的時候就會使用normalModuleFactory來創造模組。

make這個鉤子相當於一個轉折點,我們從主流程中跳轉到正真編譯的流程之中——compilation,一個專注於編譯優化的類。

等compilation編譯成功之後,再回到compiler主戰場,我們將編譯成功的內容emitAssest到硬碟上。

專業編譯100年——Compilation.js

從Webpack原始碼探究打包流程,萌新也能看懂~

如果說Compiler是流程,那麼Compilation就是編譯主場了。也就是原始碼經過他加工之後才得到了昇華變成了規規矩矩的模樣。

Compilation的工作總結起來就是,新增入口entry,通過entry分析模組,分析模組之間的依賴關係,就像圖表一樣。構建完成之後就開始seal,封裝了這個階段Compilation幹了一系列的優化措施以及將解析後的模組轉化為標準的webpack模組,輸出備用,前提是你將優化plugin掛到了各個優化的hooks上面,觸發了優化的鉤子,但是鉤子上也要註冊了函式才能生效。

好了我們從Compile得到的資訊來按照出場順序分析Compilation.js

addEntry——一切開始的地方

上一節提到的SingleEntryPlugin(還有其他的EntryPlugin),就是一個啟動口,等到觸發compile.hooks.make的時候,就會啟動SingleEntryPlugin中的compilation.addEntry這個方法,這個方法就是啟動構建入口模組,成功後將入口模組新增到程式之中。

//context,entry,name都是options中的值。
addEntry(context, entry, name, callback) {
	this._addModuleChain(context,entry,module => {
			this.entries.push(module);
		},(err, module) => {
			...
			if (module) {
				slot.module = module;
			} else {
				const idx = this._preparedEntrypoints.indexOf(slot);
				if (idx >= 0) {
					this._preparedEntrypoints.splice(idx, 1);
				}
			}
			...
			return callback(null, module);
		}
	);
}
複製程式碼

新增模組的依賴_addModuleChain

這個方法是模組構建的主要方法,由addEntry呼叫,等模組構建完成之後返回。

  • _addModuleChain,構建模組,同時儲存模組間之間的依賴。
    • const moduleFactory = this.dependencyFactories.get(Dep);moduleFactory.create(...),這裡的moduleFactory其實就是當前模組的型別的創造工廠,create就是從這個工廠中創造除了新產品(新模組)。
      • this.addModule(module)->this.modules.push(module);,將模組加入compilation.modules之中。
      • onModule(module);, 這個方法呼叫了addEntry中this.entries.push(module),也就是將入口模組加入compilation.entries。
      • this.buildModule->this.hooks.buildModule.call(module);module.build(...),這個方法就是給出了一個可以對module進行操作的hooks,大家可以自行定義plugin對此進行操作。之後便是模組自行的一個建立,這個建立的方法更具模組型別而定,比如normalModuleFactor建立的模組就來自NormalModule這個類。
        • _addModuleChain的內建方法afterBuild(),這個方法就是獲取模組和模組依賴的建立所耗費的時間,然後如果有回撥函式就執行回撥函式。

構建結束之後,回到Compiler,finish我們的構建

這裡finish幹了兩件事,一件就是出發了結束構建的鉤子,然後就是收集了每個模組構建是產生的問題。

一切就緒,開始封裝seal(callback)

產品已經準備好,準備打包出口。

開始逐個執行優化的鉤子,如果大家有寫優化的鉤子的化。

開始優化:

此處是優化依賴的hook

此處是優化module的hook

此處是優化Chunk的hook

。。。。。

太多優化了,筆者已經開溜了。

優化結束之後開始執行來自Compiler的回撥函式,也就是將生成檔案。

除了各類鉤子的call之外,seal還幹了一件很重要時就是將格式化的js,通過Template模版,重新聚合在一起,然後回撥Compiler生成檔案。這一塊會在之後Template的時候具體分析。

筆者有話說,其實主流程就是Compiler和Compliation,這兩個類互相合作。接下來還有幾個比較關鍵的類,不過從我的角度看來,不屬於主要流程,但是很重要,因為是模組建立的類。就像是流水線上的產品一樣,產品本身和流水線的流程無關。

模組的發源地—moduleFactory

從Webpack原始碼探究打包流程,萌新也能看懂~

moduleFactory是模組的例項,不過並不屬於主流程,就像是樂高的零件一樣,沒有它,我們會拼又如何?巧婦難為無米之炊!需要編譯的moduleFactory分為兩類context和normal,我基本上遇到的都是normal型別的,所以這裡以noraml類為主解釋moduleFactory。

他的使命

既然他是工廠,那麼他的使命就是製作產品。這裡模組就是產品,因此工廠只需要一個就夠了。我們的工廠是在Compiler.compile中建立的,並將此作為引數傳入了compile.hooks.beforeCompilecompile.hooks.compile這兩個鉤子之中,這意味著我們在寫這兩個鉤子的掛載函式的時候,就可以呼叫這個工廠幫我們建立處理模組了。

const NormalModule = require("./NormalModule");
const RuleSet = require("./RuleSet");
複製程式碼

這兩個引數很重要,一個是產品本身,也就是通過NormalModule建立的例項就是模組。RuleSet就是loaders,其中包括自帶的loader和自定義的loader。也就是說Factory幹了兩件事,第一件是匹配了相對應的parser,將parser配置成了專門用於當前模組的解析器將原始碼解析成AST模式,第二件是建立generator用於生成程式碼也就是還原AST(這一塊是模版生成的時候會用到),第三件是建立模組,構建模組的時候給他找到相映的loader,替換原始碼,新增相映的依賴模組,然後在模組解析的時候提供相應的parser解析器,在生成模版的時候提供相應的generator。

normalModule類

Fatory提供了原料(options)和工具(parser),就等於將引數輸給了自動化的機器,這個normalModule就是創造的機器,由他來build模組,並將原始碼變為AST語法樹。

build(options, compilation, resolver, fs, callback) {
    //...
	return this.doBuild(options, compilation, resolver, fs, err => {
	    //...
		this._cachedSources.clear();
		//...
		try {
			const result = this.parser.parse(//重點在這裡。
				//....
			);
		    //...

	});
}
複製程式碼

在Compilation中模組建立好之後,開始觸發module的build方法,開始生成模組,他的邏輯很簡單,就是輸入source原始檔,然後通過reslover解析檔案loader和依賴的檔案,並返回結果。然後通過loader將此轉化為標準的webpack模組,儲存source,等待生成模版的時候備用。

等到需要打包的時候,就將編譯過的原始碼在重組成JS程式碼,主要通過Facotry給模組配備的generator。

source(dependencyTemplates, runtimeTemplate, type = "javascript") {
	//...獲取快取
	const source = this.generator.generate(
		this,
		dependencyTemplates,
		runtimeTemplate,
		type
	);
    //...存到快取中
	return cachedSource;
}

複製程式碼

loader進行曲

loader究竟在哪裡執行,如何執行

對於初學者來說,loader和plugin可能會傻傻地分不清(沒錯,我就是那個傻子)。深入瞭解原始碼之後,我才明明白白瞭解兩者的不同。

懵懂的我 瞭解套路的我
區別1: plugin範圍廣,嗯,含義真的很廣 區別1: plugin可以在任何一個流程節點出現,loader有特定的活動範圍
區別2: 配置地方不一致,loader的配置很奇怪,居然不是module.loaders,而是module.ruleset 區別2: plugin可以做和原始碼無關的事,比如監控,loader只能解析原始碼變成標準模組。

那麼loader究竟在哪裡執行的呢?瞭解了CompilationNormalModuleFactoryNormalModule的功能之後,聽我娓娓道來loader是如何進入module的!

首先是Compilation._addModuleChain開始新增模組時,觸發了Compilation.buildModule這個方法,然後呼叫了NormalModule.build,開始建立模組了。建立模組之時,會呼叫runLoaders去執行loaders,但是對於loader所在的位置,程式還是迷茫的,所以這個時候需要請求NormalModuleFactory.resolveRequestArray,幫我們讀取loader所在的地址,執行並返回。就這樣一個個模組生成,一個個loader生成,直到最後一個模組建立完畢,然後就到了Compilation.seal的流程了。

靈魂Parser

等到當前模組處理完loaders之後,將匯入模組變成標準的JS模組之後,就要開始分解原始碼了,讓它變成標準的AST語法樹,這個時候就要依靠Parser。Parser很強大,他幫助我們將不規範的內容轉化為標準的模組,方便打包活著其他操作。Parser相當於一個機器,原始檔進入,然後處理,然後輸出,原始檔並未於Parser產生化學作用。Parser不是按照normalModule建立的個數存在的,而是按照模組的型別給匹配的。想想如果工廠中給每一個產品都配一個解析器,那麼效率成功地biubiubiu下降了了。

javascript型別的Parser一共有3個型別,"auto"、"script"和"module",根據模組的需求,Factoy幫我們匹配不同型別的Parser。

normalModuleFactory.hooks.createParser.for("javascript/auto").tap("JavascriptModulesPlugin", options => {
	return new Parser(options, "auto");
});
normalModuleFactory.hooks.createParser.for("javascript/dynamic").tap("JavascriptModulesPlugin", options => {
	return new Parser(options, "script");
});
normalModuleFactory.hooks.createParser.for("javascript/esm").tap("JavascriptModulesPlugin", options => {
	return new Parser(options, "module");
});
複製程式碼

Parser實則呢麼解析我們的原始碼的呢?

首先先變成一個AST——標準的語法樹,結構化的程式碼,方便後期解析,如果傳入的source不是ast,也會被強制ast再進行處理。

這個解析庫,webpack用的是acorn。

static parse(code, options) {
	.....
	ast = acorn.parse(code, parserOptions);
	.....
	return ast;
}
parse(source, initialState) {
    //...
	ast = Parser.parse(source, {
		sourceType: this.sourceType,
		onComment: comments
	});
	//...
}
複製程式碼

叮咚——你的打包模版Template

從Webpack原始碼探究打包流程,萌新也能看懂~

終於到了收尾的時候了,不過這個部分也不及簡單呢。

Template是在compilation.seal的時候觸發的們也就是模組構建完成之後。我們要將好不容易構建完成的模組再次重組成js程式碼,也就是我們在bundle中見到的程式碼。

我們打包出來的js,總是用著相同的套路?這是為什麼?很明顯有個標準的模版。等到我們的原始檔變成ast之後,準備輸出的處理需要依靠Template操作如何輸出,以及webpack-source幫助我們合併替換還是ast格式的模組。最後按照chunk合併一起輸出。

Template的類一共有5個:

  • Template.js
  • MainTemplate.js
  • ModuleTemplate.js
  • RuntimeTemplate
  • ChunkTemplate.js

當然!模版替換是在Compilation中執行的,畢竟Compilation就像一個指揮者,指揮者大家如何按順序一個個編譯。

Compilation.seal觸發了MainTemplate.getRenderManifest,獲取需要渲染的資訊,接著通過中的鉤子觸發了mainTemplate.hooks.renderManifest這個鉤子,呼叫了JavascriptModulePlugin中相應的函式,建立了一個含有打包資訊的fileManifest返回備用。

result.push({
	render: () =>
		compilation.mainTemplate.render(
			hash,
			chunk,
			moduleTemplates.javascript,
			dependencyTemplates
		),
	filenameTemplate,
	pathOptions: {
		noChunkHash: !useChunkHash,
		contentHashType: "javascript",
		chunk
	},
	identifier: `chunk${chunk.id}`,
	hash: useChunkHash ? chunk.hash : fullHash
});
複製程式碼

從Webpack原始碼探究打包流程,萌新也能看懂~

createChunkAssets(){
    //...
    const manifest = template.getRenderManifest(...)//獲取渲染列表
    //...
    for (const fileManifest of manifest) {
        //...
        source = fileManifest.render();
        //...
    }
    //...
}

複製程式碼

準備工作做完之後就要開始渲染了,呼叫了fileManifest的render函式,其實就是mainTemplate.rendermainTemplate.render觸發了hooks.render這個鉤子,返回了一個ConcatSource的資源。其中有固定的模板,也有呼叫的模組。

//...
this.hooks.render.tap("MainTemplate",(bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
		const source = new ConcatSource();
		source.add("/******/ (function(modules) { // webpackBootstrap\n");
	    //...
		source.add(
			this.hooks.modules.call(//獲取模組的資源
				new RawSource(""),
				chunk,
				hash,
				moduleTemplate,
				dependencyTemplates
			)
		);
		source.add(")");
		return source;
	}
);
//..
render(hash, chunk, moduleTemplate, dependencyTemplates) {
	//...
	let source = this.hooks.render.call(
		new OriginalSource(
			Template.prefix(buf, " \t") + "\n",
			"webpack/bootstrap"
		),
		chunk,
		hash,
		moduleTemplate,
		dependencyTemplates
	);
	//...
	return new ConcatSource(source, ";");
}
複製程式碼

各個模組的模板替換MainTemplate將任務分配給了Template,讓他去處理模組們的問題,於是呼叫了Template.renderChunkModules這個方法。這個方法首先是獲取所有模組的替換資源。

static renderChunkModules(chunk,filterFn,moduleTemplate,dependencyTemplates,prefix = ""){
	const source = new ConcatSource();
	const modules = chunk.getModules().filter(filterFn);
	//...
	const allModules = modules.map(module => {
		return {
			id: module.id,
			source: moduleTemplate.render(module, dependencyTemplates, {
				chunk
			})
		};
	});
	//...
	//...
}
複製程式碼

然後ModuleTemplate再去請求NormalModule.source這個方法。這裡的module便使用了Factory給他配備的generator,生成了替換程式碼,generate階段的時候會請求RuntimeTemplate,根據名字可以得知,是用於替換成執行時的程式碼。

source(dependencyTemplates, runtimeTemplate, type = "javascript") {
	//...
	const source = this.generator.generate(
		this,
		dependencyTemplates,
		runtimeTemplate,
		type
	);
	const cachedSource = new CachedSource(source);
	//..
	return cachedSource;
}
複製程式碼

NormalModule.generator.generate產生的資源

然後丟入NormalModule將此變為cachedSource,返回給ModuleTemplate進一步處理。ModuleTemplate在對這個模組進行打包,最後出來的效果是這樣的:

ModuleTemplate最終處理結果

我們再回到Template,繼續處理,經過ModuleTemplate的處理之後,我們返回的資料長這樣。

ModuleTemplate返回給Template的效果

革命尚未結束!替換仍在進行!我們回到Template.renderChunkModules,繼續替換。

static renderChunkModules(chunk,filterFn,moduleTemplate,dependencyTemplates,prefix = ""){
	const source = new ConcatSource();
	const modules = chunk.getModules().filter(filterFn);
	//...如果沒有模組,則返回"[]"
		source.add("[]");
		return source;
	//...如果有模組則獲取所有模組
	const allModules = modules.map(//...);
	//...開始新增模組
		source.add("[\n");
	//...
	    source.add(`/* ${idx} */`);
		source.add("\n");
		source.add(module.source);
		source.add("\n" + prefix + "]");
	//...
	return source;
}
複製程式碼

Template的最終返回

我們將ConcatSource返回至MainTemplate.render(),再加個;,然後組合返回至Compliation.createChunkAssets

到此seal中template就告一段落啦。至於生成檔案,那就是通過webpack-source這個包,將我們的餓陣列變成字串然後拼接,最後輸出。

所有圖片素材均出自筆者之手,歡迎大家轉載,請標明出處。畢竟搗鼓了一個多月,感覺自己都要禿了。

在醞釀下一篇研究什麼了。感覺loader還需要多扒扒。(笑~)

相關文章