從Webpack原始碼探究打包流程,萌新也能看懂~
簡介
上一篇講述瞭如何理解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的過程就通過Compiler發號施令,Compilation專心解析,最後返回Compiler輸出檔案。
專業版本:webpack的過程是通過Compiler控制流程,Compilation專業解析,ModuleFactory生成模組,Parser解析原始碼,最後通過Template組合模組,輸出打包檔案的過程。
粗暴版本: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
可以說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的模組構建其實是通過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
如果說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()
,這個方法就是獲取模組和模組依賴的建立所耗費的時間,然後如果有回撥函式就執行回撥函式。
- _addModuleChain的內建方法
構建結束之後,回到Compiler,finish我們的構建
這裡finish幹了兩件事,一件就是出發了結束構建的鉤子,然後就是收集了每個模組構建是產生的問題。
一切就緒,開始封裝seal(callback)
產品已經準備好,準備打包出口。
開始逐個執行優化的鉤子,如果大家有寫優化的鉤子的化。
開始優化:
此處是優化依賴的hook
此處是優化module的hook
此處是優化Chunk的hook
。。。。。
太多優化了,筆者已經開溜了。
優化結束之後開始執行來自Compiler的回撥函式,也就是將生成檔案。
除了各類鉤子的call之外,seal還幹了一件很重要時就是將格式化的js,通過Template模版,重新聚合在一起,然後回撥Compiler生成檔案。這一塊會在之後Template的時候具體分析。
筆者有話說,其實主流程就是Compiler和Compliation,這兩個類互相合作。接下來還有幾個比較關鍵的類,不過從我的角度看來,不屬於主要流程,但是很重要,因為是模組建立的類。就像是流水線上的產品一樣,產品本身和流水線的流程無關。
模組的發源地—moduleFactory
moduleFactory是模組的例項,不過並不屬於主流程,就像是樂高的零件一樣,沒有它,我們會拼又如何?巧婦難為無米之炊!需要編譯的moduleFactory分為兩類context和normal,我基本上遇到的都是normal型別的,所以這裡以noraml類為主解釋moduleFactory。
他的使命
既然他是工廠,那麼他的使命就是製作產品。這裡模組就是產品,因此工廠只需要一個就夠了。我們的工廠是在Compiler.compile中建立的,並將此作為引數傳入了compile.hooks.beforeCompile
和compile.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究竟在哪裡執行的呢?瞭解了Compilation
、NormalModuleFactory
、NormalModule
的功能之後,聽我娓娓道來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
終於到了收尾的時候了,不過這個部分也不及簡單呢。
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
});
複製程式碼
createChunkAssets(){
//...
const manifest = template.getRenderManifest(...)//獲取渲染列表
//...
for (const fileManifest of manifest) {
//...
source = fileManifest.render();
//...
}
//...
}
複製程式碼
準備工作做完之後就要開始渲染了,呼叫了fileManifest的render函式,其實就是mainTemplate.render
。mainTemplate.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將此變為cachedSource,返回給ModuleTemplate
進一步處理。ModuleTemplate
在對這個模組進行打包,最後出來的效果是這樣的:
我們再回到Template
,繼續處理,經過ModuleTemplate
的處理之後,我們返回的資料長這樣。
革命尚未結束!替換仍在進行!我們回到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;
}
複製程式碼
我們將ConcatSource返回至MainTemplate.render()
,再加個;
,然後組合返回至Compliation.createChunkAssets
。
到此seal中template就告一段落啦。至於生成檔案,那就是通過webpack-source這個包,將我們的餓陣列變成字串然後拼接,最後輸出。
所有圖片素材均出自筆者之手,歡迎大家轉載,請標明出處。畢竟搗鼓了一個多月,感覺自己都要禿了。
在醞釀下一篇研究什麼了。感覺loader還需要多扒扒。(笑~)