Webpack 5 核心打包原理全流程解析,看這一篇就夠了

Webpack在前端前端構(gòu)建工具中可以堪稱中流砥柱般的存在,日常業(yè)務(wù)開發(fā)、前端基建工具、高級(jí)前端面試...任何場景都會(huì)出現(xiàn)它的身影。
也許對(duì)于它的內(nèi)部實(shí)現(xiàn)機(jī)制你也許會(huì)感到疑惑,日常工作中基于Webpack Plugin/Loader之類查閱API仍然不明白各個(gè)參數(shù)的含義和應(yīng)用方式。
其實(shí)這一切原因本質(zhì)上都是基于Webpack工作流沒有一個(gè)清晰的認(rèn)知導(dǎo)致了所謂的“面對(duì)API無從下手”開發(fā)。
文章中我們會(huì)從如何實(shí)現(xiàn)模塊分析項(xiàng)目打包的角度出發(fā),使用最通俗,最簡潔,最明了的代碼帶你揭開Webpack背后的神秘面紗,帶你實(shí)現(xiàn)一個(gè)簡易版Webpack,從此對(duì)于任何webpack相關(guān)底層開發(fā)了然于胸。
這里我們只講「干貨」,用最通俗易懂的代碼帶你走進(jìn)webpack的工作流。
- Tapable[2]
Tapable[3]包本質(zhì)上是為我們更方面創(chuàng)建自定義事件和觸發(fā)自定義事件的庫,類似于Nodejs中的EventEmitter Api。
Webpack中的插件機(jī)制就是基于Tapable實(shí)現(xiàn)與打包流程解耦,插件的所有形式都是基于Tapable實(shí)現(xiàn)。
- Webpack Node Api[4]
基于學(xué)習(xí)目的我們會(huì)著重于Webpack Node Api流程去講解,實(shí)際上我們?cè)谇岸巳粘J褂玫?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">npm run build命令也是通過環(huán)境變量調(diào)用bin腳本去調(diào)用Node Api去執(zhí)行編譯打包。
- Babel[5]
Webpack內(nèi)部的AST分析同樣依賴于Babel進(jìn)行處理,如果你對(duì)Babel不是很熟悉。我建議你可以先去閱讀下這兩篇文章「前端基建」帶你在Babel的世界中暢游[6]、\# 從Tree Shaking來走進(jìn)Babel插件開發(fā)者的世界[7]。
流程梳理當(dāng)然后續(xù)我也會(huì)去詳解這些內(nèi)容在
Webpack中的應(yīng)用,但是我更加希望在閱讀文章之前你可以去點(diǎn)一點(diǎn)上方的文檔稍微了解一下前置知識(shí)。
在開始之前我們先對(duì)于整個(gè)打包流程進(jìn)行一次梳理。
這里僅僅是一個(gè)全流程的梳理,現(xiàn)在你沒有必要非常詳細(xì)的去思考每一個(gè)步驟發(fā)生了什么,我們會(huì)在接下來的步驟中去一步一步帶你串聯(lián)它們。
image.png整體我們將會(huì)從上邊5個(gè)方面來分析Webpack打包流程:
初始化參數(shù)階段。
這一步會(huì)從我們配置的
webpack.config.js中讀取到對(duì)應(yīng)的配置參數(shù)和shell命令中傳入的參數(shù)進(jìn)行合并得到最終打包配置參數(shù)。開始編譯準(zhǔn)備階段
這一步我們會(huì)通過調(diào)用
webpack()方法返回一個(gè)compiler方法,創(chuàng)建我們的compiler對(duì)象,并且注冊(cè)各個(gè)Webpack Plugin。找到配置入口中的entry代碼,調(diào)用compiler.run()方法進(jìn)行編譯。模塊編譯階段
從入口模塊進(jìn)行分析,調(diào)用匹配文件的
loaders對(duì)文件進(jìn)行處理。同時(shí)分析模塊依賴的模塊,遞歸進(jìn)行模塊編譯工作。完成編譯階段
在遞歸完成后,每個(gè)引用模塊通過
loaders處理完成同時(shí)得到模塊之間的相互依賴關(guān)系。輸出文件階段
整理模塊依賴關(guān)系,同時(shí)將處理后的文件輸出到
ouput的磁盤目錄中。
接下來讓我們?cè)敿?xì)的去探索每一步究竟發(fā)生了什么。
創(chuàng)建目錄工欲善其事,必先利其器。首先讓我們創(chuàng)建一個(gè)良好的目錄來管理我們需要實(shí)現(xiàn)的Packing tool吧!
讓我們來創(chuàng)建這樣一個(gè)目錄:
image.pngwebpack/core存放我們自己將要實(shí)現(xiàn)的webpack核心代碼。webpack/example存放我們將用來打包的實(shí)例項(xiàng)目。webpack/example/webpak.config.js配置文件.webpack/example/src/entry1第一個(gè)入口文件webpack/example/src/entry1第二個(gè)入口文件webpack/example/src/index.js模塊文件
webpack/loaders存放我們的自定義loader。webpack/plugins存放我們的自定義plugin。
往往,我們?cè)谌粘J褂秒A段有兩種方式去給webpack傳遞打包參數(shù),讓我們先來看看如何傳遞參數(shù):
Cli命令行傳遞參數(shù)
通常,我們?cè)谑褂谜{(diào)用webpack命令時(shí),有時(shí)會(huì)傳入一定命令行參數(shù),比如:
webpack?--mode=production
#?調(diào)用webpack命令執(zhí)行打包?同時(shí)傳入mode為production
復(fù)制代碼
webpack.config.js傳遞參數(shù)
另一種方式,我相信就更加老生常談了。
我們?cè)陧?xiàng)目根目錄下使用webpack.config.js導(dǎo)出一個(gè)對(duì)象進(jìn)行webpack配置:
const?path?=?require('path')
//?引入loader和plugin?...
module.exports?=?{
??mode:?'development',
??entry:?{
????main:?path.resolve(__dirname,?'./src/entry1.js'),
????second:?path.resolve(__dirname,?'./src/entry2.js'),
??},
??devtool:?false,
??//?基礎(chǔ)目錄,絕對(duì)路徑,用于從配置中解析入口點(diǎn)(entry point)和?加載器(loader)。
??//?換而言之entry和loader的所有相對(duì)路徑都是相對(duì)于這個(gè)路徑而言的
??context:?process.cwd(),
??output:?{
????path:?path.resolve(__dirname,?'./build'),
????filename:?'[name].js',
??},
??plugins:?[new?PluginA(),?new?PluginB()],
??resolve:?{
????extensions:?['.js',?'.ts'],
??},
??module:?{
????rules:?[
??????{
????????test:?/\.js/,
????????use:?[
??????????//?使用自己loader有三種方式?這里僅僅是一種
??????????path.resolve(__dirname,?'../loaders/loader-1.js'),
??????????path.resolve(__dirname,?'../loaders/loader-2.js'),
????????],
??????},
????],
??},
};
復(fù)制代碼
同時(shí)這份配置文件也是我們需要作為實(shí)例項(xiàng)目example下的實(shí)例配置,接下來讓我們修改example/webpack.config.js中的內(nèi)容為上述配置吧。
當(dāng)然這里的
loader和plugin目前你可以不用理解,接下來我們會(huì)逐步實(shí)現(xiàn)這些東西并且添加到我們的打包流程中去。
實(shí)現(xiàn)合并參數(shù)階段
這一步,讓我們真正開始動(dòng)手實(shí)現(xiàn)我們的webpack吧!
首先讓我們?cè)?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">webpack/core下新建一個(gè)index.js文件作為核心入口文件。
同時(shí)建立一個(gè)webpack/core下新建一個(gè)webpack.js文件作為webpack()方法的實(shí)現(xiàn)文件。
首先,我們清楚在NodeJs Api中是通過webpack()方法去得到compiler對(duì)象的。
image.png此時(shí)讓我們按照原本的webpack接口格式來補(bǔ)充一下index.js中的邏輯:
- 我們需要一個(gè)
webpack方法去執(zhí)行調(diào)用命令。 - 同時(shí)我們引入
webpack.config.js配置文件傳入webpack方法。
//?index.js
const?webpack?=?require('./webpack');
const?config?=?require('../example/webpack.config');
//?步驟1:?初始化參數(shù)?根據(jù)配置文件和shell參數(shù)合成參數(shù)
const?compiler?=?webpack(config);
復(fù)制代碼
嗯,看起來還不錯(cuò)。接下來讓我們?nèi)?shí)現(xiàn)一下webpack.js:
function?webpack(options)?{
??//?合并參數(shù)?得到合并后的參數(shù)?mergeOptions
??const?mergeOptions?=?_mergeOptions(options);
}
//?合并參數(shù)
function?_mergeOptions(options)?{
??const?shellOptions?=?process.argv.slice(2).reduce((option,?argv)?=>?{
????//?argv?->?--mode=production
????const?[key,?value]?=?argv.split('=');
????if?(key?&&?value)?{
??????const?parseKey?=?key.slice(2);
??????option[parseKey]?=?value;
????}
????return?option;
??},?{});
??return?{?...options,?...shellOptions?};
}
module.export?=?webpack;
復(fù)制代碼
這里我們需要額外說明的是
webpack文件中需要導(dǎo)出一個(gè)名為webpack的方法,同時(shí)接受外部傳入的配置對(duì)象。這個(gè)是我們?cè)谏鲜鲋v述過的。
當(dāng)然關(guān)于我們合并參數(shù)的邏輯,是將外部傳入的對(duì)象和執(zhí)行shell時(shí)的傳入?yún)?shù)進(jìn)行最終合并。
在Node Js中我們可以通過process.argv.slice(2)來獲得shell命令中傳入的參數(shù),比如:
image.png當(dāng)然_mergeOptions方法就是一個(gè)簡單的合并配置參數(shù)的方法,相信對(duì)于大家來說就是小菜一碟。
恭喜大家??,千里之行始于足下。這一步我們已經(jīng)完成了打包流程中的第一步:合并配置參數(shù)。
編譯階段在得到最終的配置參數(shù)之后,我們需要在webpack()函數(shù)中做以下幾件事情:
通過參數(shù)創(chuàng)建
compiler對(duì)象。我們看到官方案例中通過調(diào)用webpack(options)方法返回的是一個(gè)compiler對(duì)象。并且同時(shí)調(diào)用compiler.run()方法啟動(dòng)的代碼進(jìn)行打包。注冊(cè)我們定義的
webpack plugin插件。根據(jù)傳入的配置對(duì)象尋找對(duì)應(yīng)的打包入口文件。
創(chuàng)建compiler對(duì)象
讓我們先來完成index.js中的邏輯代碼補(bǔ)全:
//?index.js
const?webpack?=?require('./webpack');
const?config?=?require('../example/webpack.config');
//?步驟1:?初始化參數(shù)?根據(jù)配置文件和shell參數(shù)合成參數(shù)
//?步驟2:?調(diào)用Webpack(options)?初始化compiler對(duì)象??
//?webpack()方法會(huì)返回一個(gè)compiler對(duì)象
const?compiler?=?webpack(config);
//?調(diào)用run方法進(jìn)行打包
compiler.run((err,?stats)?=>?{
??if?(err)?{
????console.log(err,?'err');
??}
??//?...
});
復(fù)制代碼
可以看到,核心編譯實(shí)現(xiàn)在于webpack()方法返回的compiler.run()方法上。
一步一步讓我們來完善這個(gè)webpack()方法:
//?webpack.js
function?webpack(options)?{
??//?合并參數(shù)?得到合并后的參數(shù)?mergeOptions
??const?mergeOptions?=?_mergeOptions(options);
??//?創(chuàng)建compiler對(duì)象
??const?compiler?=?new?Compiler(mergeOptions)
??
??return?compiler
}
//?...
復(fù)制代碼
讓我們?cè)?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">webpack/core目錄下同樣新建一個(gè)compiler.js文件,作為compiler的核心實(shí)現(xiàn)文件:
//?compiler.js
//?Compiler類進(jìn)行核心編譯實(shí)現(xiàn)
class?Compiler?{
??constructor(options)?{
????this.options?=?options;
??}
??//?run方法啟動(dòng)編譯?
??//?同時(shí)run方法接受外部傳遞的callback
??run(callback)?{
??}
}
module.exports?=?Compiler
復(fù)制代碼
此時(shí)我們的Compiler類就先搭建一個(gè)基礎(chǔ)的骨架代碼。
目前,我們擁有了:
webpack/core/index.js作為打包命令的入口文件,這個(gè)文件引用了我們自己實(shí)現(xiàn)的webpack同時(shí)引用了外部的webpack.config.js(options)。調(diào)用webpack(options).run()開始編譯。webpack/core/webpack.js這個(gè)文件目前處理了參數(shù)的合并以及傳入合并后的參數(shù)new Compiler(mergeOptions),同時(shí)返回創(chuàng)建的Compiler實(shí)力對(duì)象。webpack/core/compiler,此時(shí)我們的compiler僅僅是作為一個(gè)基礎(chǔ)的骨架,存在一個(gè)run()啟動(dòng)方法。
編寫Plugin
還記得我們?cè)?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">webpack.config.js中使用了兩個(gè)plugin---pluginA、pluginB插件嗎。接下來讓我們來依次實(shí)現(xiàn)它們:
在實(shí)現(xiàn)Plugin前,我們需要先來完善一下compiler方法:
const?{?SyncHook?}?=?require('tapable');
class?Compiler?{
??constructor(options)?{
????this.options?=?options;
????//?創(chuàng)建plugin?hooks
????this.hooks?=?{
??????//?開始編譯時(shí)的鉤子
??????run:?new?SyncHook(),
??????//?輸出?asset?到?output?目錄之前執(zhí)行?(寫入文件之前)
??????emit:?new?SyncHook(),
??????//?在?compilation?完成時(shí)執(zhí)行?全部完成編譯執(zhí)行
??????done:?new?SyncHook(),
????};
??}
??//?run方法啟動(dòng)編譯
??//?同時(shí)run方法接受外部傳遞的callback
??run(callback)?{}
}
module.exports?=?Compiler;
復(fù)制代碼
這里,我們?cè)?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">Compiler這個(gè)類的構(gòu)造函數(shù)中創(chuàng)建了一個(gè)屬性hooks,它的值是三個(gè)屬性run、emit、done。
關(guān)于這三個(gè)屬性的值就是我們上文提到前置知識(shí)的tapable的SyncHook方法,本質(zhì)上你可以簡單將SyncHook()方法理解稱為一個(gè)Emitter Event類。
當(dāng)我們通過new SyncHook()返回一個(gè)對(duì)象實(shí)例后,我們可以通過this.hook.run.tap('name',callback)方法為這個(gè)對(duì)象上添加事件監(jiān)聽,然后在通過this.hook.run.call()執(zhí)行所有tap注冊(cè)的事件。
當(dāng)然
webpack真實(shí)源碼中,這里有非常多的hook。以及分別存在同步/異步鉤子,我們這里更多的是為大家講解清楚流程,所以僅列舉了三個(gè)常見且簡單的同步鉤子。
此時(shí),我們需要明白,我們可以通過Compiler類返回的實(shí)例對(duì)象上compiler.hooks.run.tap注冊(cè)鉤子。
接下來讓我們切回到webpack.js中,讓我們來填充關(guān)于插件注冊(cè)的邏輯:
const?Compiler?=?require('./compiler');
function?webpack(options)?{
??//?合并參數(shù)
??const?mergeOptions?=?_mergeOptions(options);
??//?創(chuàng)建compiler對(duì)象
??const?compiler?=?new?Compiler(mergeOptions);
??//?加載插件
??_loadPlugin(options.plugins,?compiler);
??return?compiler;
}
//?合并參數(shù)
function?_mergeOptions(options)?{
??const?shellOptions?=?process.argv.slice(2).reduce((option,?argv)?=>?{
????//?argv?->?--mode=production
????const?[key,?value]?=?argv.split('=');
????if?(key?&&?value)?{
??????const?parseKey?=?key.slice(2);
??????option[parseKey]?=?value;
????}
????return?option;
??},?{});
??return?{?...options,?...shellOptions?};
}
//?加載插件函數(shù)
function?_loadPlugin(plugins,?compiler)?{
??if?(plugins?&&?Array.isArray(plugins))?{
????plugins.forEach((plugin)?=>?{
??????plugin.apply(compiler);
????});
??}
}
module.exports?=?webpack;
復(fù)制代碼
這里我們?cè)趧?chuàng)建完成compiler對(duì)象后,調(diào)用了_loadPlugin方法進(jìn)行注冊(cè)插件。
有接觸過webpack插件開發(fā)的同學(xué),或多或少可能都有了解過。任何一個(gè)webpack插件都是一個(gè)類(當(dāng)然類本質(zhì)上都是funciton的語法糖),每個(gè)插件都必須存在一個(gè)apply方法。
這個(gè)apply方法會(huì)接受一個(gè)compiler對(duì)象。我們上邊做的就是依次調(diào)用傳入的plugin的apply方法并且傳入我們的compiler對(duì)象。
這里我請(qǐng)你記住上邊的流程,日常我們編寫
webpack plugin時(shí)本質(zhì)上就是操作compiler對(duì)象從而影響打包結(jié)果進(jìn)行。
也許此時(shí)你并不是很理解這句話的含義,在我們串聯(lián)完成整個(gè)流程之后我會(huì)為大家揭曉這個(gè)答案。
接下來讓我們?nèi)ゾ帉戇@些個(gè)插件:
不了解插件開發(fā)的同學(xué)可以去稍微看一下官方的介紹[8],其實(shí)不是很難,我個(gè)人強(qiáng)烈建議如果不了解可以先去看看再回來結(jié)合上變講的內(nèi)容你一定會(huì)有所收獲的。
首先讓我們先創(chuàng)建文件:
image.png//?plugin-a.js
//?插件A
class?PluginA?{
??apply(compiler)?{
????//?注冊(cè)同步鉤子
????//?這里的compiler對(duì)象就是我們new?Compiler()創(chuàng)建的實(shí)例哦
????compiler.hooks.run.tap('Plugin?A',?()?=>?{
??????//?調(diào)用
??????console.log('PluginA');
????});
??}
}
module.exports?=?PluginA;
復(fù)制代碼
//?plugin-b.js
class?PluginB?{
??apply(compiler)?{
????compiler.hooks.done.tap('Plugin?B',?()?=>?{
??????console.log('PluginB');
????});
??}
}
module.exports?=?PluginB;
復(fù)制代碼
看到這里我相信大部分同學(xué)都已經(jīng)反應(yīng)過來了,compiler.hooks.done.tap不就是我們上邊講到的通過tapable創(chuàng)建一個(gè)SyncHook實(shí)例然后通過tap方法注冊(cè)事件嗎?
沒錯(cuò)!的確是這樣,關(guān)于webpack插件本質(zhì)上就是通過發(fā)布訂閱的模式,通過compiler上監(jiān)聽事件。然后再打包編譯過程中觸發(fā)監(jiān)聽的事件從而添加一定的邏輯影響打包結(jié)果。
我們?cè)诿總€(gè)插件的apply方法上通過tap在編譯準(zhǔn)備階段(也就是調(diào)用webpack()函數(shù)時(shí))進(jìn)行訂閱對(duì)應(yīng)的事件,當(dāng)我們的編譯執(zhí)行到一定階段時(shí)發(fā)布對(duì)應(yīng)的事件告訴訂閱者去執(zhí)行監(jiān)聽的事件,從而達(dá)到在編譯階段的不同生命周期內(nèi)去觸發(fā)對(duì)應(yīng)的plugin。
所以這里你應(yīng)該清楚,我們?cè)谶M(jìn)行
webpack插件開發(fā)時(shí),compiler對(duì)象上存放著本次打包的所有相關(guān)屬性,比如options打包的配置,以及我們會(huì)在之后講到的各種屬性。
尋找entry入口
這之后,我們的絕大多數(shù)內(nèi)容都會(huì)放在compiler.js中去實(shí)現(xiàn)Compiler這個(gè)類實(shí)現(xiàn)打包的核心流程。
任何一次打包都需要入口文件,接下來讓我們就從真正進(jìn)入打包編譯階段。首當(dāng)其沖的事情就是,我們需要根據(jù)入口配置文件路徑尋找到對(duì)應(yīng)入口文件。
//?compiler.js
const?{?SyncHook?}?=?require('tapable');
const?{?toUnixPath?}?=?require('./utils');
class?Compiler?{
??constructor(options)?{
????this.options?=?options;
????//?相對(duì)路徑跟路徑?Context參數(shù)
????this.rootPath?=?this.options.context?||?toUnixPath(process.cwd());
????//?創(chuàng)建plugin?hooks
????this.hooks?=?{
??????//?開始編譯時(shí)的鉤子
??????run:?new?SyncHook(),
??????//?輸出?asset?到?output?目錄之前執(zhí)行?(寫入文件之前)
??????emit:?new?SyncHook(),
??????//?在?compilation?完成時(shí)執(zhí)行?全部完成編譯執(zhí)行
??????done:?new?SyncHook(),
????};
??}
??//?run方法啟動(dòng)編譯
??//?同時(shí)run方法接受外部傳遞的callback
??run(callback)?{
????//?當(dāng)調(diào)用run方式時(shí)?觸發(fā)開始編譯的plugin
????this.hooks.run.call();
????//?獲取入口配置對(duì)象
????const?entry?=?this.getEntry();
??}
??//?獲取入口文件路徑
??getEntry()?{
????let?entry?=?Object.create(null);
????const?{?entry:?optionsEntry?}?=?this.options;
????if?(typeof?optionsEntry?===?'string')?{
??????entry['main']?=?optionsEntry;
????}?else?{
??????entry?=?optionsEntry;
????}
????//?將entry變成絕對(duì)路徑
????Object.keys(entry).forEach((key)?=>?{
??????const?value?=?entry[key];
??????if?(!path.isAbsolute(value))?{
????????//?轉(zhuǎn)化為絕對(duì)路徑的同時(shí)統(tǒng)一路徑分隔符為?/
????????entry[key]?=?toUnixPath(path.join(this.rootPath,?value));
??????}
????});
????return?entry;
??}
}
module.exports?=?Compiler;
復(fù)制代碼
//?utils/index.js
/**
?*
?*?統(tǒng)一路徑分隔符?主要是為了后續(xù)生成模塊ID方便
?*?@param?{*}?path
?*?@returns
?*/
function?toUnixPath(path)?{
??return?path.replace(/\\/g,?'/');
}
復(fù)制代碼
這一步我們通過options.entry處理獲得入口文件的絕對(duì)路徑。
這里有幾個(gè)需要注意的小點(diǎn):
this.hooks.run.call()
在我們_loadePlugins函數(shù)中對(duì)于每一個(gè)傳入的插件在compiler實(shí)例對(duì)象中進(jìn)行了訂閱,那么當(dāng)我們調(diào)用run方法時(shí),等于真正開始執(zhí)行編譯。這個(gè)階段相當(dāng)于我們需要告訴訂閱者,發(fā)布開始執(zhí)行的訂閱。此時(shí)我們通過this.hooks.run.call()執(zhí)行關(guān)于run的所有tap監(jiān)聽方法,從而觸發(fā)對(duì)應(yīng)的plugin邏輯。
this.rootPath:
在上述的外部webpack.config.js中我們配置了一個(gè) context: process.cwd(),其實(shí)真實(shí)webpack中這個(gè)context值默認(rèn)也是process.cwd()。
關(guān)于它的詳細(xì)解釋你可以在這里看到Context[9]。
簡而言之,這個(gè)路徑就是我們項(xiàng)目啟動(dòng)的目錄路徑,任何entry和loader中的相對(duì)路徑都是針對(duì)于context這個(gè)參數(shù)的相對(duì)路徑。
這里我們使用this.rootPath在構(gòu)造函數(shù)中來保存這個(gè)變量。
toUnixPath工具方法:
因?yàn)椴煌僮飨到y(tǒng)下,文件分隔路徑是不同的。這里我們統(tǒng)一使用\來替換路徑中的//來替換模塊路徑。后續(xù)我們會(huì)使用模塊相對(duì)于rootPath的路徑作為每一個(gè)文件的唯一ID,所以這里統(tǒng)一處理下路徑分隔符。
entry的處理方法:
關(guān)于entry配置,webpack中其實(shí)有很多種。我們這里考慮了比較常見的兩種配置方式:
entry:'entry1.js'
//?本質(zhì)上這段代碼在webpack中會(huì)被轉(zhuǎn)化為
entry:?{
????main:'entry1.js
}
復(fù)制代碼
entry:?{
???'entry1':'./entry1.js',
???'entry2':'/user/wepback/example/src/entry2.js'
}
復(fù)制代碼
這兩種方式任何方式都會(huì)經(jīng)過getEntry方法最終轉(zhuǎn)化稱為{ [模塊名]:[模塊絕對(duì)路徑]... }的形式,關(guān)于geEntry()方法其實(shí)非常簡單,這里我就不過于累贅這個(gè)方法的實(shí)現(xiàn)過程了。
這一步,我們就通過getEntry方法獲得了一個(gè)key為entryName,value為entryAbsolutePath的對(duì)象了,接來下就讓我們從入口文件出發(fā)進(jìn)行編譯流程吧。
上邊我們講述了關(guān)于編譯階段的準(zhǔn)備工作:
- 目錄/文件基礎(chǔ)邏輯補(bǔ)充。
- 通過
hooks.tap注冊(cè)webpack插件。 getEntry方法獲得各個(gè)入口的對(duì)象。
接下來讓我們繼續(xù)完善compiler.js。
在模塊編譯階段,我們需要做的事件:
- 根據(jù)入口文件路徑分析入口文件,對(duì)于入口文件進(jìn)行匹配對(duì)應(yīng)的
loader進(jìn)行處理入口文件。 - 將
loader處理完成的入口文件使用webpack進(jìn)行編譯。 - 分析入口文件依賴,重復(fù)上邊兩個(gè)步驟編譯對(duì)應(yīng)依賴。
- 如果嵌套文件存在依賴文件,遞歸調(diào)用依賴模塊進(jìn)行編譯。
- 遞歸編譯完成后,組裝一個(gè)個(gè)包含多個(gè)模塊的
chunk
首先,我們先來給compiler.js的構(gòu)造函數(shù)中補(bǔ)充一下對(duì)應(yīng)的邏輯:
class?Compiler?{
??constructor(options)?{
????this.options?=?options;
????//?創(chuàng)建plugin?hooks
????this.hooks?=?{
??????//?開始編譯時(shí)的鉤子
??????run:?new?SyncHook(),
??????//?輸出?asset?到?output?目錄之前執(zhí)行?(寫入文件之前)
??????emit:?new?SyncHook(),
??????//?在?compilation?完成時(shí)執(zhí)行?全部完成編譯執(zhí)行
??????done:?new?SyncHook(),
????};
????//?保存所有入口模塊對(duì)象
????this.entries?=?new?Set();
????//?保存所有依賴模塊對(duì)象
????this.modules?=?new?Set();
????//?所有的代碼塊對(duì)象
????this.chunks?=?new?Set();
????//?存放本次產(chǎn)出的文件對(duì)象
????this.assets?=?new?Set();
????//?存放本次編譯所有產(chǎn)出的文件名
????this.files?=?new?Set();
??}
??//?...
?}
復(fù)制代碼
這里我們通過給compiler構(gòu)造函數(shù)中添加一些列屬性來保存關(guān)于編譯階段生成的對(duì)應(yīng)資源/模塊對(duì)象。
關(guān)于
entries\modules\chunks\assets\files這幾個(gè)Set對(duì)象是貫穿我們核心打包流程的屬性,它們各自用來儲(chǔ)存編譯階段不同的資源從而最終通過對(duì)應(yīng)的屬性進(jìn)行生成編譯后的文件。
根據(jù)入口文件路徑分析入口文件
上邊說到我們?cè)?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">run方法中已經(jīng)可以通過this.getEntry();獲得對(duì)應(yīng)的入口對(duì)象了~
接下來就讓我們從入口文件開始去分析入口文件吧!
class?Compiler?{
????//?run方法啟動(dòng)編譯
??//?同時(shí)run方法接受外部傳遞的callback
??run(callback)?{
????//?當(dāng)調(diào)用run方式時(shí)?觸發(fā)開始編譯的plugin
????this.hooks.run.call();
????//?獲取入口配置對(duì)象
????const?entry?=?this.getEntry();
????//?編譯入口文件
????this.buildEntryModule(entry);
??}
??buildEntryModule(entry)?{
????Object.keys(entry).forEach((entryName)?=>?{
??????const?entryPath?=?entry[entryName];
??????const?entryObj?=?this.buildModule(entryName,?entryPath);
??????this.entries.add(entryObj);
????});
??}
??
??
??//?模塊編譯方法
??buildModule(moduleName,modulePath)?{
????//?...
????return?{}
??}
}
復(fù)制代碼
這里我們添加了一個(gè)名為buildEntryModule方法作為入口模塊編譯方法。循環(huán)入口對(duì)象,得到每一個(gè)入口對(duì)象的名稱和路徑。
比如如假使我們?cè)陂_頭傳入
entry:{ main:'./src/main.js' }的話,buildEntryModule獲得的形參entry為{ main: "/src...[你的絕對(duì)路徑]" }, 此時(shí)我們buildModule方法接受的entryName為main,entryPath為入口文件main對(duì)應(yīng)的的絕對(duì)路徑。
單個(gè)入口編譯完成后,我們會(huì)在
buildModule方法中返回一個(gè)對(duì)象。這個(gè)對(duì)象就是我們編譯入口文件后的對(duì)象。
buildModule模塊編譯方法
在進(jìn)行代碼編寫之前,我們先來梳理一下buildModule方法它需要做哪些事情:
buildModule接受兩個(gè)參數(shù)進(jìn)行模塊編譯,第一個(gè)為模塊所屬的入口文件名稱,第二個(gè)為需要編譯的模塊路徑。buildModule方法要進(jìn)行代碼編譯的前提就是,通過fs模塊根據(jù)入口文件路徑讀取文件源代碼。讀取文件內(nèi)容之后,調(diào)用所有匹配的loader對(duì)模塊進(jìn)行處理得到返回后的結(jié)果。
得到
loader處理后的結(jié)果后,通過babel分析loader處理后的代碼,進(jìn)行代碼編譯。(這一步編譯主要是針對(duì)require語句,修改源代碼中require語句的路徑)。如果該入口文件沒有依賴與任何模塊(
require語句),那么返回編譯后的模塊對(duì)象。如果該入口文件存在依賴的模塊,遞歸
buildModule方法進(jìn)行模塊編譯。
讀取文件內(nèi)容
我們先調(diào)用`fs`模塊讀取文件內(nèi)容。
const?fs?=?require('fs');
//?...
class?Compiler?{
??????//...
??????//?模塊編譯方法
??????buildModule(moduleName,?modulePath)?{
????????//?1.?讀取文件原始代碼
????????const?originSourceCode?=
??????????((this.originSourceCode?=?fs.readFileSync(modulePath,?'utf-8'));
????????//?moduleCode為修改后的代碼
????????this.moduleCode?=?originSourceCode;
??????}
??????
??????//?...
?}
復(fù)制代碼
調(diào)用loader處理匹配后綴文件
- 接下來我們獲得了文件的具體內(nèi)容之后,就需要匹配對(duì)應(yīng)
loader對(duì)我們的源代碼進(jìn)行編譯了。
實(shí)現(xiàn)簡單自定義loader
在進(jìn)行loader編譯前,我們先來實(shí)現(xiàn)一下我們上方傳入的自定義loader吧。
image.pngwebpack/loader目錄下新建loader-1.js,loader-2.js:
首先我們需要清楚簡單來說loader本質(zhì)上就是一個(gè)函數(shù),接受我們的源代碼作為入?yún)⑼瑫r(shí)返回處理后的結(jié)果。
關(guān)于
loader的特性,更加詳細(xì)你可以在這里看到[10],因?yàn)槲恼轮饕v述打包流程所以loader我們簡單的作為倒序處理。更加具體的loader/plugin開發(fā)我會(huì)在后續(xù)的文章詳細(xì)補(bǔ)充。
// loader本質(zhì)上就是一個(gè)函數(shù),接受原始內(nèi)容,返回轉(zhuǎn)換后的內(nèi)容。
function?loader1(sourceCode)?{
??console.log('join?loader1');
??return?sourceCode?+?`\n?const?loader1?=?'https://github.com/19Qingfeng'`;
}
module.exports?=?loader1;
復(fù)制代碼
function?loader2(sourceCode)?{
??console.log('join?loader2');
??return?sourceCode?+?`\n?const?loader2?=?'19Qingfeng'`;
}
module.exports?=?loader2;
復(fù)制代碼
使用loader處理文件
在搞清楚了loader就是一個(gè)單純的函數(shù)之后,讓我們?cè)谶M(jìn)行模塊分析之前將內(nèi)容先交給匹配的loader去處理下吧。
//?模塊編譯方法
??buildModule(moduleName,?modulePath)?{
????//?1.?讀取文件原始代碼
????const?originSourceCode?=
??????((this.originSourceCode?=?fs.readFileSync(modulePath)),?'utf-8');
????//?moduleCode為修改后的代碼
????this.moduleCode?=?originSourceCode;
????//??2.?調(diào)用loader進(jìn)行處理
????this.handleLoader(modulePath);
??}
??//?匹配loader處理
??handleLoader(modulePath)?{
????const?matchLoaders?=?[];
????//?1.?獲取所有傳入的loader規(guī)則
????const?rules?=?this.options.module.rules;
????rules.forEach((loader)?=>?{
??????const?testRule?=?loader.test;
??????if?(testRule.test(modulePath))?{
????????if?(loader.loader)?{
??????????//?僅考慮loader?{?test:/\.js$/g,?use:['babel-loader']?},?{?test:/\.js$/,?loader:'babel-loader'?}
??????????matchLoaders.push(loader.loader);
????????}?else?{
??????????matchLoaders.push(...loader.use);
????????}
??????}
??????//?2.?倒序執(zhí)行l(wèi)oader傳入源代碼
??????for?(let?i?=?matchLoaders.length?-?1;?i?>=?0;?i--)?{
????????//?目前我們外部僅支持傳入絕對(duì)路徑的loader模式
????????//?require引入對(duì)應(yīng)loader
????????const?loaderFn?=?require(matchLoaders[i]);
????????//?通過loader同步處理我的每一次編譯的moduleCode
????????this.moduleCode?=?loaderFn(this.moduleCode);
??????}
????});
??}
復(fù)制代碼
這里我們通過handleLoader函數(shù),對(duì)于傳入的文件路徑匹配到對(duì)應(yīng)后綴的loader后,依次倒序執(zhí)行l(wèi)oader處理我們的代碼this.moduleCode并且同步更新每次moduleCode。
最終,在每一個(gè)模塊編譯中this.moduleCode都會(huì)經(jīng)過對(duì)應(yīng)的loader處理。
webpack模塊編譯階段
上一步我們經(jīng)歷過loader處理了我們的入口文件代碼,并且得到了處理后的代碼保存在了this.moduleCode中。
此時(shí),經(jīng)過loader處理后我們就要進(jìn)入webpack內(nèi)部的編譯階段了。
這里我們需要做的是:針對(duì)當(dāng)前模塊進(jìn)行編譯,將當(dāng)前模塊所有依賴的模塊(require())語句引入的路徑變?yōu)橄鄬?duì)于跟路徑(this.rootPath)的相對(duì)路徑。
總之你需要搞明白的是,我們這里編譯的結(jié)果是期望將源代碼中的依賴模塊路徑變?yōu)橄鄬?duì)跟路徑的路徑,同時(shí)建立基礎(chǔ)的模塊依賴關(guān)系。后續(xù)我會(huì)告訴你為什么針對(duì)路徑進(jìn)行編譯。
讓我們繼續(xù)來完善buildModule方法吧:
const?parser?=?require('@babel/parser');
const?traverse?=?require('@babel/traverse').default;
const?generator?=?require('@babel/generator').default;
const?t?=?require('@babel/types');
const?tryExtensions?=?require('./utils/index')
//?...
??class?Compiler?{
?????//?...
??????
?????//?模塊編譯方法
??????buildModule(moduleName,?modulePath)?{
????????//?1.?讀取文件原始代碼
????????const?originSourceCode?=
??????????((this.originSourceCode?=?fs.readFileSync(modulePath)),?'utf-8');
????????//?moduleCode為修改后的代碼
????????this.moduleCode?=?originSourceCode;
????????//??2.?調(diào)用loader進(jìn)行處理
????????this.handleLoader(modulePath);
????????//?3.?調(diào)用webpack?進(jìn)行模塊編譯?獲得最終的module對(duì)象
????????const?module?=?this.handleWebpackCompiler(moduleName,?modulePath);
????????//?4.?返回對(duì)應(yīng)module
????????return?module
??????}
??????//?調(diào)用webpack進(jìn)行模塊編譯
??????handleWebpackCompiler(moduleName,?modulePath)?{
????????//?將當(dāng)前模塊相對(duì)于項(xiàng)目啟動(dòng)根目錄計(jì)算出相對(duì)路徑?作為模塊ID
????????const?moduleId?=?'./'?+?path.posix.relative(this.rootPath,?modulePath);
????????//?創(chuàng)建模塊對(duì)象
????????const?module?=?{
??????????id:?moduleId,
??????????dependencies:?new?Set(),?//?該模塊所依賴模塊絕對(duì)路徑地址
??????????name:?[moduleName],?//?該模塊所屬的入口文件
????????};
????????//?調(diào)用babel分析我們的代碼
????????const?ast?=?parser.parse(this.moduleCode,?{
??????????sourceType:?'module',
????????});
????????//?深度優(yōu)先?遍歷語法Tree
????????traverse(ast,?{
??????????//?當(dāng)遇到require語句時(shí)
??????????CallExpression:(nodePath)?=>?{
????????????const?node?=?nodePath.node;
????????????if?(node.callee.name?===?'require')?{
??????????????//?獲得源代碼中引入模塊相對(duì)路徑
??????????????const?moduleName?=?node.arguments[0].value;
??????????????//?尋找模塊絕對(duì)路徑?當(dāng)前模塊路徑+require()對(duì)應(yīng)相對(duì)路徑
??????????????const?moduleDirName?=?path.posix.dirname(modulePath);
??????????????const?absolutePath?=?tryExtensions(
????????????????path.posix.join(moduleDirName,?moduleName),
????????????????this.options.resolve.extensions,
????????????????moduleName,
????????????????moduleDirName
??????????????);
??????????????//?生成moduleId?-?針對(duì)于跟路徑的模塊ID?添加進(jìn)入新的依賴模塊路徑
??????????????const?moduleId?=
????????????????'./'?+?path.posix.relative(this.rootPath,?absolutePath);
??????????????//?通過babel修改源代碼中的require變成__webpack_require__語句
??????????????node.callee?=?t.identifier('__webpack_require__');
??????????????//?修改源代碼中require語句引入的模塊?全部修改變?yōu)橄鄬?duì)于跟路徑來處理
??????????????node.arguments?=?[t.stringLiteral(moduleId)];
??????????????//?為當(dāng)前模塊添加require語句造成的依賴(內(nèi)容為相對(duì)于根路徑的模塊ID)
??????????????module.dependencies.add(moduleId);
????????????}
??????????},
????????});
????????//?遍歷結(jié)束根據(jù)AST生成新的代碼
????????const?{?code?}?=?generator(ast);
????????//?為當(dāng)前模塊掛載新的生成的代碼
????????module._source?=?code;
????????//?返回當(dāng)前模塊對(duì)象
????????return?module
??????}
??}
復(fù)制代碼
這一步我們關(guān)于webpack編譯的階段就完成了。
需要注意的是:
這里我們使用
babel相關(guān)的API針對(duì)于require語句進(jìn)行了編譯,如果對(duì)于babel相關(guān)的api不太了解的朋友可以在前置知識(shí)中查看我的另兩篇文章。這里我就不在累贅了同時(shí)我們代碼中引用了一個(gè)
tryExtensions()工具方法,這個(gè)方法是針對(duì)于后綴名不全的工具方法,稍后你就可以看到這個(gè)方法的具體內(nèi)容。針對(duì)于每一次文件編譯,我們都會(huì)返回一個(gè)module對(duì)象,這個(gè)對(duì)象是重中之重。
id屬性,表示當(dāng)前模塊針對(duì)于this.rootPath的相對(duì)目錄。dependencies屬性,它是一個(gè)Set內(nèi)部保存了該模塊依賴的所有模塊的模塊ID。name屬性,它表示該模塊屬于哪個(gè)入口文件。_source屬性,它存放模塊自身經(jīng)過babel編譯后的字符串代碼。
tryExtensions方法實(shí)現(xiàn)
我們?cè)谏衔牡?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">webpack.config.js有這么一個(gè)配置:
image.png熟悉webpack配置的同學(xué)可能清楚,resolve.extensions是針對(duì)于引入依賴時(shí),在沒有書寫文件后綴的情況下,webpack會(huì)自動(dòng)幫我們按照傳入的規(guī)則為文件添加后綴。
在清楚了原理后我們來一起看看utils/tryExtensions方法的實(shí)現(xiàn):
/**
?*
?*
?*?@param?{*}?modulePath?模塊絕對(duì)路徑
?*?@param?{*}?extensions?擴(kuò)展名數(shù)組
?*?@param?{*}?originModulePath?原始引入模塊路徑
?*?@param?{*}?moduleContext?模塊上下文(當(dāng)前模塊所在目錄)
?*/
function?tryExtensions(
??modulePath,
??extensions,
??originModulePath,
??moduleContext
)?{
??//?優(yōu)先嘗試不需要擴(kuò)展名選項(xiàng)
??extensions.unshift('');
??for?(let?extension?of?extensions)?{
????if?(fs.existsSync(modulePath?+?extension))?{
??????return?modulePath?+?extension;
????}
??}
??//?未匹配對(duì)應(yīng)文件
??throw?new?Error(
????`No?module,?Error:?Can't?resolve?${originModulePath}?in??${moduleContext}`
??);
}
復(fù)制代碼
這個(gè)方法很簡單,我們通過fs.existsSync檢查傳入文件結(jié)合extensions依次遍歷尋找對(duì)應(yīng)匹配的路徑是否存在,如果找到則直接返回。如果未找到則給予用于一個(gè)友好的提示錯(cuò)誤。
需要注意
extensions.unshift('');是防止用戶如果已經(jīng)傳入了后綴時(shí),我們優(yōu)先嘗試直接尋找,如果可以找到文件那么就直接返回。找不到的情況下才會(huì)依次嘗試。
遞歸處理
經(jīng)過上一步處理,針對(duì)入口文件我們調(diào)用buildModule可以得到這樣的返回對(duì)象。
我們先來看看運(yùn)行webpack/core/index.js得到的返回結(jié)果吧。
image.png我在buildEntryModule中打印了處理完成后的entries對(duì)象??梢钥吹秸缥覀冎八诖?
id為每個(gè)模塊相對(duì)于跟路徑的模塊.(這里我們配置的context:process.cwd())為webpack目錄。dependencies為該模塊內(nèi)部依賴的模塊,這里目前還沒有添加。name為該模塊所屬的入口文件名稱。_source為該模塊編譯后的源代碼。
目前
_source中的內(nèi)容是基于
此時(shí)讓我們打開src目錄為我們的兩個(gè)入口文件添加一些依賴和內(nèi)容吧:
//?webpack/example/entry1.js
const?depModule?=?require('./module');
console.log(depModule,?'dep');
console.log('This?is?entry?1?!');
//?webpack/example/entry2.js
const?depModule?=?require('./module');
console.log(depModule,?'dep');
console.log('This?is?entry?2?!');
//?webpack/example/module.js
const?name?=?'19Qingfeng';
module.exports?=?{
??name,
};
復(fù)制代碼
此時(shí)讓我們重新運(yùn)行webpack/core/index.js:
image.pngOK,目前為止我們針對(duì)于entry的編譯可以暫時(shí)告一段落了。
總之也就是,這一步我們通過``方法將entry進(jìn)行分析編譯后得到一個(gè)對(duì)象。將這個(gè)對(duì)象添加到this.entries中去。
接下來讓我們?nèi)ヌ幚硪蕾嚨哪K吧。
其實(shí)對(duì)于依賴的模塊無非也是相同的步驟:
- 檢查入口文件中是否存在依賴。
- 存在依賴的話,遞歸調(diào)用
buildModule方法編譯模塊。傳入moduleName為當(dāng)前模塊所屬的入口文件。modulePath為當(dāng)前被依賴模塊的絕對(duì)路徑。 - 同理檢查遞歸檢查被依賴的模塊內(nèi)部是否仍然存在依賴,存在的話遞歸依賴進(jìn)行模塊編譯。這是一個(gè)深度優(yōu)先的過程。
- 將每一個(gè)編譯后的模塊保存進(jìn)入
this.modules中去。
接下來我們只要稍稍在handleWebpackCompiler方法中稍稍改動(dòng)就可以了:
?//?調(diào)用webpack進(jìn)行模塊編譯
??handleWebpackCompiler(moduleName,?modulePath)?{
????//?將當(dāng)前模塊相對(duì)于項(xiàng)目啟動(dòng)根目錄計(jì)算出相對(duì)路徑?作為模塊ID
????const?moduleId?=?'./'?+?path.posix.relative(this.rootPath,?modulePath);
????//?創(chuàng)建模塊對(duì)象
????const?module?=?{
??????id:?moduleId,
??????dependencies:?new?Set(),?//?該模塊所依賴模塊絕對(duì)路徑地址
??????name:?[moduleName],?//?該模塊所屬的入口文件
????};
????//?調(diào)用babel分析我們的代碼
????const?ast?=?parser.parse(this.moduleCode,?{
??????sourceType:?'module',
????});
????//?深度優(yōu)先?遍歷語法Tree
????traverse(ast,?{
??????//?當(dāng)遇到require語句時(shí)
??????CallExpression:?(nodePath)?=>?{
????????const?node?=?nodePath.node;
????????if?(node.callee.name?===?'require')?{
??????????//?獲得源代碼中引入模塊相對(duì)路徑
??????????const?moduleName?=?node.arguments[0].value;
??????????//?尋找模塊絕對(duì)路徑?當(dāng)前模塊路徑+require()對(duì)應(yīng)相對(duì)路徑
??????????const?moduleDirName?=?path.posix.dirname(modulePath);
??????????const?absolutePath?=?tryExtensions(
????????????path.posix.join(moduleDirName,?moduleName),
????????????this.options.resolve.extensions,
????????????moduleName,
????????????moduleDirName
??????????);
??????????//?生成moduleId?-?針對(duì)于跟路徑的模塊ID?添加進(jìn)入新的依賴模塊路徑
??????????const?moduleId?=
????????????'./'?+?path.posix.relative(this.rootPath,?absolutePath);
??????????//?通過babel修改源代碼中的require變成__webpack_require__語句
??????????node.callee?=?t.identifier('__webpack_require__');
??????????//?修改源代碼中require語句引入的模塊?全部修改變?yōu)橄鄬?duì)于跟路徑來處理
??????????node.arguments?=?[t.stringLiteral(moduleId)];
??????????//?為當(dāng)前模塊添加require語句造成的依賴(內(nèi)容為相對(duì)于根路徑的模塊ID)
??????????module.dependencies.add(moduleId);
????????}
??????},
????});
????//?遍歷結(jié)束根據(jù)AST生成新的代碼
????const?{?code?}?=?generator(ast);
????//?為當(dāng)前模塊掛載新的生成的代碼
????module._source?=?code;
????//?遞歸依賴深度遍歷?存在依賴模塊則加入
????module.dependencies.forEach((dependency)?=>?{
??????const?depModule?=?this.buildModule(moduleName,?dependency);
??????//?將編譯后的任何依賴模塊對(duì)象加入到modules對(duì)象中去
??????this.modules.add(depModule);
????});
????//?返回當(dāng)前模塊對(duì)象
????return?module;
??}
復(fù)制代碼
這里我們添加了這樣一段代碼:
????//?遞歸依賴深度遍歷?存在依賴模塊則加入
????module.dependencies.forEach((dependency)?=>?{
??????const?depModule?=?this.buildModule(moduleName,?dependency);
??????//?將編譯后的任何依賴模塊對(duì)象加入到modules對(duì)象中去
??????this.modules.add(depModule);
????});
復(fù)制代碼
這里我們對(duì)于依賴的模塊進(jìn)行了遞歸調(diào)用buildModule,將輸出的模塊對(duì)象添加進(jìn)入了this.modules中去。
此時(shí)讓我們重新運(yùn)行webpack/core/index.js進(jìn)行編譯,這里我在buildEntryModule編譯結(jié)束后打印了assets和modules:
image.pngSet?{
??{
????id:?'./example/src/entry1.js',
????dependencies:?Set?{?'./example/src/module.js'?},
????name:?[?'main'?],
????_source:?'const?depModule?=?__webpack_require__("./example/src/module.js");\n'?+
??????'\n'?+
??????"console.log(depModule,?'dep');\n"?+
??????"console.log('This?is?entry?1?!');\n"?+
??????"const?loader2?=?'19Qingfeng';\n"?+
??????"const?loader1?=?'https://github.com/19Qingfeng';"
??},
??{
????id:?'./example/src/entry2.js',
????dependencies:?Set?{?'./example/src/module.js'?},
????name:?[?'second'?],
????_source:?'const?depModule?=?__webpack_require__("./example/src/module.js");\n'?+
??????'\n'?+
??????"console.log(depModule,?'dep');\n"?+
??????"console.log('This?is?entry?2?!');\n"?+
??????"const?loader2?=?'19Qingfeng';\n"?+
??????"const?loader1?=?'https://github.com/19Qingfeng';"
??}
}?entries
Set?{
??{
????id:?'./example/src/module.js',
????dependencies:?Set?{},
????name:?[?'main'?],
????_source:?"const?name?=?'19Qingfeng';\n"?+
??????'module.exports?=?{\n'?+
??????'??name\n'?+
??????'};\n'?+
??????"const?loader2?=?'19Qingfeng';\n"?+
??????"const?loader1?=?'https://github.com/19Qingfeng';"
??},
??{
????id:?'./example/src/module.js',
????dependencies:?Set?{},
????name:?[?'second'?],
????_source:?"const?name?=?'19Qingfeng';\n"?+
??????'module.exports?=?{\n'?+
??????'??name\n'?+
??????'};\n'?+
??????"const?loader2?=?'19Qingfeng';\n"?+
??????"const?loader1?=?'https://github.com/19Qingfeng';"
??}
}?modules
復(fù)制代碼
可以看到我們已經(jīng)將module.js這個(gè)依賴如愿以償加入到modules中了,同時(shí)它也經(jīng)過loader的處理。但是我們發(fā)現(xiàn)它被重復(fù)加入了兩次。
這是因?yàn)?strong style="color:#000000;">module.js這個(gè)模塊被引用了兩次,它被entry1和entry2都已進(jìn)行了依賴,在進(jìn)行遞歸編譯時(shí)我們進(jìn)行了兩次buildModule相同模塊。
讓我們來處理下這個(gè)問題:
????handleWebpackCompiler(moduleName,?modulePath)?{
???????...
????????//?通過babel修改源代碼中的require變成__webpack_require__語句
??????????node.callee?=?t.identifier('__webpack_require__');
??????????//?修改源代碼中require語句引入的模塊?全部修改變?yōu)橄鄬?duì)于跟路徑來處理
??????????node.arguments?=?[t.stringLiteral(moduleId)];
??????????//?轉(zhuǎn)化為ids的數(shù)組?好處理
??????????const?alreadyModules?=?Array.from(this.modules).map((i)?=>?i.id);
??????????if?(!alreadyModules.includes(moduleId))?{
????????????//?為當(dāng)前模塊添加require語句造成的依賴(內(nèi)容為相對(duì)于根路徑的模塊ID)
????????????module.dependencies.add(moduleId);
??????????}?else?{
????????????//?已經(jīng)存在的話?雖然不進(jìn)行添加進(jìn)入模塊編譯?但是仍要更新這個(gè)模塊依賴的入口
????????????this.modules.forEach((value)?=>?{
??????????????if?(value.id?===?moduleId)?{
????????????????value.name.push(moduleName);
??????????????}
????????????});
??????????}
????????}
??????},
????});
????...
????}
復(fù)制代碼
這里在每一次代碼分析的依賴轉(zhuǎn)化中,首先判斷this.module對(duì)象是否已經(jīng)存在當(dāng)前模塊了(通過唯一的模塊id路徑判斷)。
如果不存在則添加進(jìn)入依賴中進(jìn)行編譯,如果該模塊已經(jīng)存在過了就證明這個(gè)模塊已經(jīng)被編譯過了。所以此時(shí)我們不需要將它再次進(jìn)行編譯,我們僅僅需要更新這個(gè)模塊所屬的chunk,為它的name屬性添加當(dāng)前所屬的chunk名稱。
重新運(yùn)行,讓我們?cè)趤砜纯创蛴〗Y(jié)果:
Set?{
??{
????id:?'./example/src/entry1.js',
????dependencies:?Set?{?'./example/src/module.js'?},
????name:?[?'main'?],
????_source:?'const?depModule?=?__webpack_require__("./example/src/module.js");\n'?+
??????'\n'?+
??????"console.log(depModule,?'dep');\n"?+
??????"console.log('This?is?entry?1?!');\n"?+
??????"const?loader2?=?'19Qingfeng';\n"?+
??????"const?loader1?=?'https://github.com/19Qingfeng';"
??},
??{
????id:?'./example/src/entry2.js',
????dependencies:?Set?{},
????name:?[?'second'?],
????_source:?'const?depModule?=?__webpack_require__("./example/src/module.js");\n'?+
??????'\n'?+
??????"console.log(depModule,?'dep');\n"?+
??????"console.log('This?is?entry?2?!');\n"?+
??????"const?loader2?=?'19Qingfeng';\n"?+
??????"const?loader1?=?'https://github.com/19Qingfeng';"
??}
}?entries
Set?{
??{
????id:?'./example/src/module.js',
????dependencies:?Set?{},
????name:?[?'main',?'./module'?],
????_source:?"const?name?=?'19Qingfeng';\n"?+
??????'module.exports?=?{\n'?+
??????'??name\n'?+
??????'};\n'?+
??????"const?loader2?=?'19Qingfeng';\n"?+
??????"const?loader1?=?'https://github.com/19Qingfeng';"
??}
}?modules
復(fù)制代碼
此時(shí)針對(duì)我們的“模塊編譯階段”基本已經(jīng)結(jié)束了,這一步我們對(duì)于所有模塊從入口文件開始進(jìn)行分析。
- 從入口出發(fā),讀取入口文件內(nèi)容調(diào)用匹配
loader處理入口文件。 - 通過
babel分析依賴,并且同時(shí)將所有依賴的路徑更換為相對(duì)于項(xiàng)目啟動(dòng)目錄options.context的路徑。 - 入口文件中如果存在依賴的話,遞歸上述步驟編譯依賴模塊。
- 將每個(gè)依賴的模塊編譯后的對(duì)象加入
this.modules。 - 將每個(gè)入口文件編譯后的對(duì)象加入
this.entries。
在上一步我們完成了模塊之間的編譯,并且為module和entry分別填充了內(nèi)容。
在將所有模塊遞歸編譯完成后,我們需要根據(jù)上述的依賴關(guān)系,組合最終輸出的chunk模塊。
讓我們來繼續(xù)改造我們的Compiler吧:
class?Compiler?{
????//?...
????buildEntryModule(entry)?{
????????Object.keys(entry).forEach((entryName)?=>?{
??????????const?entryPath?=?entry[entryName];
??????????//?調(diào)用buildModule實(shí)現(xiàn)真正的模塊編譯邏輯
??????????const?entryObj?=?this.buildModule(entryName,?entryPath);
??????????this.entries.add(entryObj);
??????????//?根據(jù)當(dāng)前入口文件和模塊的相互依賴關(guān)系,組裝成為一個(gè)個(gè)包含當(dāng)前入口所有依賴模塊的chunk
??????????this.buildUpChunk(entryName,?entryObj);
????????});
????????console.log(this.chunks,?'chunks');
????}
????
?????//?根據(jù)入口文件和依賴模塊組裝chunks
??????buildUpChunk(entryName,?entryObj)?{
????????const?chunk?=?{
??????????name:?entryName,?//?每一個(gè)入口文件作為一個(gè)chunk
??????????entryModule:?entryObj,?//?entry編譯后的對(duì)象
??????????modules:?Array.from(this.modules).filter((i)?=>
????????????i.name.includes(entryName)
??????????),?//?尋找與當(dāng)前entry有關(guān)的所有module
????????};
????????//?將chunk添加到this.chunks中去
????????this.chunks.add(chunk);
??????}
??????
??????//?...
}
復(fù)制代碼
這里,我們根據(jù)對(duì)應(yīng)的入口文件通過每一個(gè)模塊(module)的name屬性查找對(duì)應(yīng)入口的所有依賴文件。
我們先來看看this.chunks最終會(huì)輸出什么:
Set?{
??{
????name:?'main',
????entryModule:?{
??????id:?'./example/src/entry1.js',
??????dependencies:?[Set],
??????name:?[Array],
??????_source:?'const?depModule?=?__webpack_require__("./example/src/module.js");\n'?+
????????'\n'?+
????????"console.log(depModule,?'dep');\n"?+
????????"console.log('This?is?entry?1?!');\n"?+
????????"const?loader2?=?'19Qingfeng';\n"?+
????????"const?loader1?=?'https://github.com/19Qingfeng';"
????},
????modules:?[?[Object]?]
??},
??{
????name:?'second',
????entryModule:?{
??????id:?'./example/src/entry2.js',
??????dependencies:?Set?{},
??????name:?[Array],
??????_source:?'const?depModule?=?__webpack_require__("./example/src/module.js");\n'?+
????????'\n'?+
????????"console.log(depModule,?'dep');\n"?+
????????"console.log('This?is?entry?2?!');\n"?+
????????"const?loader2?=?'19Qingfeng';\n"?+
????????"const?loader1?=?'https://github.com/19Qingfeng';"
????},
????modules:?[]
??}
}?
復(fù)制代碼
這一步,我們得到了Webpack中最終輸出的兩個(gè)chunk。
它們分別擁有:
name:當(dāng)前入口文件的名稱entryModule: 入口文件編譯后的對(duì)象。modules: 該入口文件依賴的所有模塊對(duì)象組成的數(shù)組,其中每一個(gè)元素的格式和entryModule是一致的。
此時(shí)編譯完成我們拼裝chunk的環(huán)節(jié)就圓滿完成。
我們先放一下上一步所有編譯完成后拼裝出來的this.chunks。
分析原始打包輸出結(jié)果
這里,我把webpack/core/index.js中做了如下修改:
-?const?webpack?=?require('./webpack');
+?const?webpack?=?require('webpack')
...
復(fù)制代碼
運(yùn)用原本的webpack代替我們自己實(shí)現(xiàn)的webpack先進(jìn)行一次打包。
運(yùn)行webpack/core/index.js后,我們會(huì)在webpack/src/build中得到兩個(gè)文件:main.js和second.js,我們以其中一個(gè)main.js來看看它的內(nèi)容:
(()?=>?{
??var?__webpack_modules__?=?{
????'./example/src/module.js':?(module)?=>?{
??????const?name?=?'19Qingfeng';
??????module.exports?=?{
????????name,
??????};
??????const?loader2?=?'19Qingfeng';
??????const?loader1?=?'https://github.com/19Qingfeng';
????},
??};
??//?The?module?cache
??var?__webpack_module_cache__?=?{};
??//?The?require?function
??function?__webpack_require__(moduleId)?{
????//?Check?if?module?is?in?cache
????var?cachedModule?=?__webpack_module_cache__[moduleId];
????if?(cachedModule?!==?undefined)?{
??????return?cachedModule.exports;
????}
????//?Create?a?new?module?(and?put?it?into?the?cache)
????var?module?=?(__webpack_module_cache__[moduleId]?=?{
??????//?no?module.id?needed
??????//?no?module.loaded?needed
??????exports:?{},
????});
????//?Execute?the?module?function
????__webpack_modules__[moduleId](module,?module.exports,?__webpack_require__);
????//?Return?the?exports?of?the?module
????return?module.exports;
??}
??var?__webpack_exports__?=?{};
??//?This?entry?need?to?be?wrapped?in?an?IIFE?because?it?need?to?be?isolated?against?other?modules?in?the?chunk.
??(()?=>?{
????const?depModule?=?__webpack_require__(
??????/*!?./module?*/?'./example/src/module.js'
????);
????console.log(depModule,?'dep');
????console.log('This?is?entry?1?!');
????const?loader2?=?'19Qingfeng';
????const?loader1?=?'https://github.com/19Qingfeng';
??})();
})();
復(fù)制代碼
這里我手動(dòng)刪除了打包生成后的多余注釋,精簡了代碼。
我們來稍微分析一下原始打包生成的代碼:
webpack打包后的代碼內(nèi)部定義了一個(gè)__webpack_require__的函數(shù)代替了NodeJs內(nèi)部的require方法。
同時(shí)底部的
image.png這塊代碼相比大家都很熟悉吧,這就是我們編譯后的入口文件代碼。同時(shí)頂部的代碼是該入口文件依賴的所有模塊定義的一個(gè)對(duì)象:
image.png這里定義了一個(gè)__webpack__modules的對(duì)象,**對(duì)象的key為該依賴模塊相對(duì)于跟路徑的相對(duì)路徑,對(duì)象的value該依賴模塊編譯后的代碼。`
輸出文件階段
接下里在分析完webpack原始打包后的代碼之后,上我們來繼續(xù)上一步。通過我們的this.chunks來嘗試輸出最終的效果吧。
讓我們回到Compiler上的run方法中:
???class?Compiler?{
???
???}
??//?run方法啟動(dòng)編譯
??//?同時(shí)run方法接受外部傳遞的callback
??run(callback)?{
????//?當(dāng)調(diào)用run方式時(shí)?觸發(fā)開始編譯的plugin
????this.hooks.run.call();
????//?獲取入口配置對(duì)象
????const?entry?=?this.getEntry();
????//?編譯入口文件
????this.buildEntryModule(entry);
????//?導(dǎo)出列表;之后將每個(gè)chunk轉(zhuǎn)化稱為單獨(dú)的文件加入到輸出列表assets中
????this.exportFile(callback);
??}
復(fù)制代碼
我們?cè)?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(155,110,35);background-color:rgb(255,245,227);">buildEntryModule模塊編譯完成之后,通過this.exportFile方法實(shí)現(xiàn)導(dǎo)出文件的邏輯。
讓我們來一起看看this.exportFile方法:
?//?將chunk加入輸出列表中去
??exportFile(callback)?{
????const?output?=?this.options.output;
????//?根據(jù)chunks生成assets內(nèi)容
????this.chunks.forEach((chunk)?=>?{
??????const?parseFileName?=?output.filename.replace('[name]',?chunk.name);
??????//?assets中?{?'main.js':?'生成的字符串代碼...'?}
??????this.assets.set(parseFileName,?getSourceCode(chunk));
????});
????//?調(diào)用Plugin?emit鉤子
????this.hooks.emit.call();
????//?先判斷目錄是否存在?存在直接fs.write?不存在則首先創(chuàng)建
????if?(!fs.existsSync(output.path))?{
??????fs.mkdirSync(output.path);
????}
????//?files中保存所有的生成文件名
????this.files?=?Object.keys(this.assets);
????//?將assets中的內(nèi)容生成打包文件?寫入文件系統(tǒng)中
????Object.keys(this.assets).forEach((fileName)?=>?{
??????const?filePath?=?path.join(output.path,?fileName);
??????fs.writeFileSync(filePath,?this.assets[fileName]);
????});
????//?結(jié)束之后觸發(fā)鉤子
????this.hooks.done.call();
????callback(null,?{
??????toJson:?()?=>?{
????????return?{
??????????entries:?this.entries,
??????????modules:?this.modules,
??????????files:?this.files,
??????????chunks:?this.chunks,
??????????assets:?this.assets,
????????};
??????},
????});
??}
復(fù)制代碼
exportFile做了如下幾件事:
首先獲取配置參數(shù)的輸出配置,迭代我們的
this.chunks,將output.filename中的[name]替換稱為對(duì)應(yīng)的入口文件名稱。同時(shí)根據(jù)chunks的內(nèi)容為this.assets中添加需要打包生成的文件名和文件內(nèi)容。將文件寫入磁盤前調(diào)用
plugin的emit鉤子函數(shù)。判斷
output.path文件夾是否存在,如果不存在,則通過fs新建這個(gè)文件夾。將本次打包生成的所有文件名(
this.assets的key值組成的數(shù)組)存放進(jìn)入files中去。循環(huán)
this.assets,將文件依次寫入對(duì)應(yīng)的磁盤中去。所有打包流程結(jié)束,觸發(fā)
webpack插件的done鉤子。同時(shí)為
NodeJs Webpack APi呼應(yīng),調(diào)用run方法中外部傳入的callback傳入兩個(gè)參數(shù)。
總的來說,this.assets做的事情也比較簡單,就是通過分析chunks得到assets然后輸出對(duì)應(yīng)的代碼到磁盤中。
仔細(xì)看過上邊代碼,你會(huì)發(fā)現(xiàn)。this.assets這個(gè)Map中每一個(gè)元素的value是通過調(diào)用getSourceCode(chunk)方法來生成模塊對(duì)應(yīng)的代碼的。
那么getSourceCode這個(gè)方法是如何根據(jù)chunk來生成我們最終編譯后的代碼呢?讓我們一起來看看吧!
getSourceCode方法
首先我們來簡單明確一下這個(gè)方法的職責(zé),我們需要getSourceCode方法接受傳入的chunk對(duì)象。從而返回該chunk的源代碼。
廢話不多說,其實(shí)這里我用了一個(gè)比較偷懶的辦法,但是完全不妨礙你理解Webpack流程,上邊我們分析過原本webpack打包后的代碼僅僅只有入口文件和模塊依賴是每次打包不同的地方,關(guān)于require方法之類都是相通的。
把握每次的不同點(diǎn),我們直接先來看看它的實(shí)現(xiàn)方式:
//?webpack/utils/index.js
...
/**
?*
?*
?*?@param?{*}?chunk
?*?name屬性入口文件名稱
?*?entryModule入口文件module對(duì)象
?*?modules?依賴模塊路徑
?*/
function?getSourceCode(chunk)?{
??const?{?name,?entryModule,?modules?}?=?chunk;
??return?`
??(()?=>?{
????var?__webpack_modules__?=?{
??????${modules
????????.map((module)?=>?{
??????????return?`
??????????'${module.id}':?(module)?=>?{
????????????${module._source}
??????}
????????`;
????????})
????????.join(',')}
????};
????//?The?module?cache
????var?__webpack_module_cache__?=?{};
????//?The?require?function
????function?__webpack_require__(moduleId)?{
??????//?Check?if?module?is?in?cache
??????var?cachedModule?=?__webpack_module_cache__[moduleId];
??????if?(cachedModule?!==?undefined)?{
????????return?cachedModule.exports;
??????}
??????//?Create?a?new?module?(and?put?it?into?the?cache)
??????var?module?=?(__webpack_module_cache__[moduleId]?=?{
????????//?no?module.id?needed
????????//?no?module.loaded?needed
????????exports:?{},
??????});
??????//?Execute?the?module?function
??????__webpack_modules__[moduleId](module,?module.exports,?__webpack_require__);
??????//?Return?the?exports?of?the?module
??????return?module.exports;
????}
????var?__webpack_exports__?=?{};
????//?This?entry?need?to?be?wrapped?in?an?IIFE?because?it?need?to?be?isolated?against?other?modules?in?the?chunk.
????(()?=>?{
??????${entryModule._source}
????})();
??})();
??`;
}
...
復(fù)制代碼
這段代碼其實(shí)非常非常簡單,遠(yuǎn)遠(yuǎn)沒有你想象的多難!有點(diǎn)返璞歸真的感覺是嗎哈哈。
在getSourceCode方法中,我們通過組合而來的chunk獲得對(duì)應(yīng)的:
name: 該入口文件對(duì)應(yīng)輸出文件的名稱。entryModule: 存放該入口文件編譯后的對(duì)象。modules:存放該入口文件依賴的所有模塊的對(duì)象。
我們通過字符串拼接的方式去實(shí)現(xiàn)了__webpack__modules對(duì)象上的屬性,同時(shí)也在底部通過${entryModule._source}拼接出入口文件的代碼。
這里我們上文提到過為什么要將模塊的
require方法的路徑轉(zhuǎn)化為相對(duì)于跟路徑(context)的路徑,看到這里我相信為什么這么做大家都已經(jīng)了然于胸了。因?yàn)槲覀冏罱K實(shí)現(xiàn)的__webpack_require__方法全都是針對(duì)于模塊跟路徑的相對(duì)路徑自己實(shí)現(xiàn)的require方法。
大功告成同時(shí)如果不太清楚
require方法是如何轉(zhuǎn)變稱為__webpack_require__方法的同學(xué)可以重新回到我們的編譯章節(jié)仔細(xì)復(fù)習(xí)熬~我們通過babel在AST轉(zhuǎn)化階段將require方法調(diào)用變成了__webpack_require__。
至此,讓我們回到webpack/core/index.js中去。重新運(yùn)行這個(gè)文件,你會(huì)發(fā)現(xiàn)webpack/example目錄下會(huì)多出一個(gè)build目錄。
image.png這一步我們就完美的實(shí)現(xiàn)屬于我們自己的webpack。
實(shí)質(zhì)上,我們對(duì)于實(shí)現(xiàn)一個(gè)簡單版的webpack核心我還是希望大家可以在理解它的工作流的同時(shí)徹底理解compiler這個(gè)對(duì)象。
在之后的任何關(guān)于webpack相關(guān)底層開發(fā)中,真正做到對(duì)于compiler的用法了然于胸。了解compiler上的各種屬性是如何影響到編譯打包結(jié)果的。
讓我們用一張流程圖來進(jìn)行一個(gè)完美的收尾吧:
image.png寫在最后首先,感謝每一位可以看到這里的同學(xué)。
這篇文章相對(duì)有一定的知識(shí)門檻并且代碼部分居多,敬佩每一位可以讀到結(jié)尾的同學(xué)。
文章中對(duì)于實(shí)現(xiàn)一個(gè)簡易版的Webpack在這里就要和大家告一段落了,這其實(shí)只是一個(gè)最基礎(chǔ)版本的webpack工作流。
但是正是通過這樣一個(gè)小??可以帶我們真正入門webpack的核心工作流,希望這篇文章對(duì)于大家理解webpack時(shí)可以起到更好的輔助作用。
其實(shí)在理解清楚基礎(chǔ)的工作流之后,針對(duì)于
loader和plugin開發(fā)都是信手拈來的部分,文章中對(duì)于這兩部分內(nèi)容的開發(fā)介紹比較膚淺,后續(xù)我會(huì)分別更新有關(guān)loader和plugin的詳細(xì)開發(fā)流程。有興趣的同學(xué)可以及時(shí)關(guān)注??。
文章中的代碼你可以在這里下載[11],這份簡易版的
webpack我也會(huì)持續(xù)在代碼庫中完善更多工作流的邏輯處理。
作者:19組清風(fēng)同時(shí)這里這里的代碼我想強(qiáng)調(diào)的是源碼流程的講解,真實(shí)的webpack會(huì)比這里復(fù)雜很多很多。這里為了方便大家理解刻意進(jìn)行了簡化,但是核心工作流是和源碼中基本一致的。
https://juejin.cn/post/7031546400034947108
點(diǎn)贊和在看就是最大的支持??
