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

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