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