在前面的文章webpack不適合多頁面應用?你寫的插件還不夠多中提到過,webpack核心使用了Tapable實現事件的發布訂閱處理的插件架構(Tapable中文文檔),今天就具體來分析下webpack基于Tapable的插件架構
找到代碼入口
- 想必你已經使用過
npm install webpack
命令下載過webpack,那么在你的node_modules目錄下找到webpack。 - npm模塊的入口文件可以通過package.json中的
"main": "lib/webpack.js"
找到,當你通過reqire引用模塊的時候,其實定位到的就是這個文件。一般情況下,我們會在命令行直接使用webpack命令去執行打包,這個時候執行的就是bin/webpack.js
了,這個命令只是在調用lib/webpack.js
之前處理一些命令行參數,殊途同歸。 - 打開lib/webpack.js。webpack.js除了使用exportPlugins導出很多插件類(方便外部調用),最重要的事情就是創建compiler對象(如果options是數組的話,每個元素創建一個)
//創建compiler對象
compiler = new Compiler();
//后面這幾句代碼,得看了compiler再回來看看了
compiler.options = options;
compiler.options = new WebpackOptionsApply().process(options, compiler);
new NodeEnvironmentPlugin().apply(compiler);
compiler.applyPlugins("environment");
compiler.applyPlugins("after-environment");
這個時候我們的視線得轉移到compiler上了
植入Tapable
打開lib/Compiler.js
function Compiler() {
//傳入作用域,調用Tapable的構造函數
Tapable.call(this);
this.outputPath = "";
this.outputFileSystem = null;
this.inputFileSystem = null;
this.recordsInputPath = null;
this.recordsOutputPath = null;
this.records = {};
this.fileTimestamps = {};
this.contextTimestamps = {};
this.resolvers = {
normal: new Resolver(null),
loader: new Resolver(null),
context: new Resolver(null)
};
this.parser = new Parser();
this.options = {};
}
module.exports = Compiler;
//復制一份Tapable的原型
Compiler.prototype = Object.create(Tapable.prototype);
Compiler.prototype.constructor = Compiler;
如果你閱讀過Tapable中文文檔,你應該對這個mix的方式不會陌生,tapable的原理其實也不復雜
聲明一個全局的變量
this._plugins = {}
,插件中使用plugin(name, fn)
方法給事件name
注冊處理方法fn
,多次注冊形成了事件name
的監聽鏈,當事件name
觸發的時候,執行這些處理方法。處理方法的執行順序和執行方式依據事件name
的觸發方式的不同而不同
這個時候compiler已經具備Tapable的所有屬性和方法了,我們再回到lib/webpack.js來看看創建了Compiler對象后的幾行代碼
說實話不太欣賞這種在對象外面初始化的設計模式,讀代碼的時候你得跳來跳去,我更傾向于通過構造函數傳入options,在對象內進行初始化工作。(僅代表個人的想法)
//給對象參數賦值
compiler.options = options;
//傳入options和compiler執行WebpackOptionsApply的process想法
//這個方法對參數進行了處理,并且注入了大量的插件
compiler.options = new WebpackOptionsApply().process(options, compiler);
//注冊nodeEveironmentPlugin插件
new NodeEnvironmentPlugin().apply(compiler);
//觸發environment和after-environment事件
compiler.applyPlugins("environment");
compiler.applyPlugins("after-environment");
webpack很多核心功能本身就是以插件的形式開發的,打開lib/WebpackOptionsApply就會發現,在這個方法中,除了處理參數,就是把一個個插件注冊到compiler中
compiler.applyPlugins("environment")
以一種最簡單的并行處理的方式去去觸發事件environment事件
,所有注冊的處理方法并行執行,相互獨立互不干擾,并且不需要給處理方法傳入參數。
而有些事件的觸發方式要復雜一些,例如complier觸發emit
的方式
this.applyPluginsAsync("emit", compilation, function(err) {
if(err) return callback(err);
outputPath = compilation.getPath(this.outputPath);
this.outputFileSystem.mkdirp(outputPath, emitFiles.bind(this));
}.bind(this));
這段代碼:
觸發事件emit
,傳入參數compilation對象,串行的調用注冊在事件emit
上的處理函數(先入先出),倘若某一個處理函數報錯,則執行傳入的function(err)
,后續的處理函數將不被執行,否則最后一個處理函數調用function()
。插件注冊此類事件,處理函數需要調用callback,這樣才能保證監聽鏈的正確執行。所以為了在寫自定義插件的時候能正確的監聽事件,非常有必要仔細讀Tapable中文文檔(雖然本文已經多次提到這個文檔,但是還是有必要再進行一次提醒)
webpack中另外一個重要的對象compilation使用了同樣的方式植入了tapable
總結
咱們分析webpack源碼主要有兩個原因:一是為了學習優秀的代碼的設計方法,二是為了編寫webpack自定義插件的時候能夠游刃有余。不管從哪一點來說,Tapable面向切面的插件思想,都是值得我們琢磨的(在我的一個項目中還真的從webpack把Tapable借鑒過來了)
后續會繼續更新對webpack源碼的進一步分析,歡迎關注,共同學習。有問題請評論或發簡信,如果你覺得文章對你有所幫助,請不要吝惜你的喜歡,當然,給我打賞我也不會客氣的~~。