接上文:淺析 webpack 打包流程(原理) 一 - 準(zhǔn)備工作
四侦副、遞歸編譯生成 module 實(shí)例
4.1 resolve 階段谤逼,解析返回包含當(dāng)前模塊所有信息的一個對象
此階段概述:利用 enhanced-resolve 庫谒兄,得到 resolve 解析方法 ?? 解析 inline loader 和它對應(yīng)資源的 resource,還有項目config的 loader汰蜘,然后對所有 loader 進(jìn)行合并晴弃、排序 ?? 得到 module 對應(yīng)的 parser 和 generator宛瞄,用于后面的 ast 解析及模板生成 ?? 輸出一個包含當(dāng)前模塊上下文、loaders爱咬、絕對路徑尺借、依賴等 module 所有信息的組合對象,提供給 afterResolve 鉤子觸發(fā)后的回調(diào)精拟。這個對象下一步會被用來初始化當(dāng)前文件 的 module 實(shí)例燎斩。
上一步我們已經(jīng)得知 moduleFactory 就是 normalModuleFactory,那么接著看 normalModuleFactory 的 create 方法:
觸發(fā)normalModuleFactory.hooks:beforeResolve
蜂绎,在回調(diào)里觸發(fā)NormalModuleFactory.hooks:factory
鉤子栅表,再執(zhí)行該 factory 函數(shù),即NormalModuleFactory.hooks:resolver
师枣。
// /lib/NormalModuleFactory.js
constructor(context, resolverFactory, options) {
this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
// hooks:factory 綁定的方法
let resolver = this.hooks.resolver.call(null); // 觸發(fā) resolver 鉤子返回一個 resolver 函數(shù)
resolver(result, (err, data) => {
//...
this.hooks.afterResolve.callAsync(data, (err, result) => {
let createdModule = this.hooks.createModule.call(result);
if (!createdModule) {
// 創(chuàng)建 normalModule 實(shí)例
createdModule = new NormalModule(result);
}
createdModule = this.hooks.module.call(createdModule, result);
return callback(null, createdModule);
});
});
});
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
const loaderResolver = this.getResolver("loader"); // 用于解析 loader 的絕對路徑
const normalResolver = this.getResolver("normal", data.resolveOptions); // 用于解析 文件 和 module 的絕對路徑
}
}
create(data, callback) {
// ...
this.hooks.beforeResolve.callAsync({...}, (err, result) => {
// 觸發(fā) NormalModuleFactory.hooks: factory
const factory = this.hooks.factory.call(null);
factory(result, (err, module) => {
//...
});
}
上面的 resolver 函數(shù)負(fù)責(zé)解析 構(gòu)建 module 所需 loaders 的絕對路徑 以及每個 module 的相關(guān)構(gòu)建信息(如獲取 module 的 packge.json 等)怪瓶。
this.getResolver 即/lib/ResolverFactory.js
的 get 方法,判斷如有緩存返回緩存践美,無則執(zhí)行 _create 方法:
// /lib/ResolverFactory.js
_create(type, resolveOptions) {
// Factory 指向的文件路徑:node_modules/enhanced-resolve/lib/ResolverFactory.js
const Factory = require("enhanced-resolve").ResolverFactory;
resolveOptions = this.hooks.resolveOptions.for(type).call(resolveOptions);
// enhanced-resolve/lib/ResolverFactory.js 導(dǎo)出的 createResolver 方法
const resolver = Factory.createResolver(resolveOptions);
// 利用 enhanced-resolve 庫注冊完鉤子插件后觸發(fā) ResolverFactory 的 resolver 鉤子
this.hooks.resolver.for(type).call(resolver, resolveOptions);
return resolver;
}
在編譯前準(zhǔn)備我們通過WebpackOptionsApply.js
在ResolverFactory.hooks: resolveOptions
鉤子上注冊了綁定事件洗贰,此刻觸發(fā)后用 cachedCleverMerge 判斷緩存及融合配置(如果 type 是 loader 則為 配置項: options.resolveLoader,如果是 normal 則為 配置項: options.resolve)拨脉,并添加屬性 fileSystem: compiler.inputFileSystem哆姻,最終返回一個 resolveOptions 對象,作為 Factory.createResolver 執(zhí)行的參數(shù)玫膀。
enhanced-resolve createResolver 方法內(nèi)矛缨,先融合處理了項目配置 resolve 與默認(rèn)配置 resolve/resolveLoader,如未傳入項目的 resolver,就自己 new 一個箕昭。接著定義了 Resolver 的生命周期鉤子并根據(jù)配置 push 了一大堆 plugins 實(shí)例灵妨。然后對每一個插件執(zhí)行 apply,在 Resolver 不同生命周期鉤子上注冊一些方法落竹,并在函數(shù)末尾執(zhí)行:
// node_modules/enhanced-resolve/lib/xxxPlugin.js
// 獲取hooks泌霍,target 為事件鉤子
const target = resolver.ensureHook(this.target);
// 觸發(fā)插件后的回調(diào)里,執(zhí)行:
resolver.doResolve(target, obj, ...);
在觸發(fā)完當(dāng)前插件后述召,會通過 doResolve 將 hook 帶入到下一個插件中朱转,實(shí)現(xiàn)遞歸串聯(lián)調(diào)用一系列的插件,包括 UnsafeCachePlugin积暖、ParsePlugin藤为、DescriptionFilePlugin、ModuleKindPlugin 等等夺刑,來完成各自的操作缅疟。
再回到 NormalModuleFactory.hooks: resolver,拿到 loaderResolver 和 normalResolver遍愿,用于解析路徑存淫。
接下來進(jìn)行 inline loader 和對應(yīng)資源文件 resource 的解析:
比如import Styles from style-loader!css-loader?modules!./styles.css
會被解析成:
{
"resource": "./styles.css",
"elements": [
{
"loader": "style-loader"
},
{
"loader": "css-loader",
"options": "modules"
}
]
}
然后執(zhí)行asyncLib.parallel(...)
,它會并行處理參數(shù)數(shù)組各個任務(wù)沼填,都完成之后返回一個 results 列表桅咆,列表順序?yàn)閰?shù)數(shù)組順序,與執(zhí)行順序無關(guān)倾哺。
得到的 results:
{
"results": [
[
{
"loader": "loader的絕對路徑1",
"options": "loader參數(shù)1"
},
{
"loader": "loader的絕對路徑2",
"options": "loader參數(shù)2"
}
],
{
"resource": "模塊絕對路徑",
"resourceResolveData": "模塊基本信息(即enhanced-resolve執(zhí)行結(jié)果)"
}
]
}
const result = this.ruleSet.exec({...})
解析 config module rules 里的 loader轧邪,遞歸過濾匹配出對應(yīng)的 loader:
{
"result": [
{ "type": "type", "value": "javascript/auto" },
{ "type": "resolve", "value": {} },
{ "type": "use", "value": { "loader": "babel-loader" } }
]
}
對 loader 進(jìn)行合并、排序:
接著處理inline loader
帶有前綴!
,!!
,-!
和result
項帶有enforce
參數(shù)的情況羞海,用來決定懟 loader
的禁用和排序忌愚。
又通過 asyncLib.parallel 與 this.resolveRequestArray 并行處理上一步得到的useLoadersPost、useLoadersPre却邓、useLoaders
硕糊,拿到對應(yīng)的 resolve 結(jié)果即路徑信息,再在回調(diào)里排序腊徙、合并简十,即 loaders 配置順序?yàn)?postLoader,inlineLoader撬腾,loader(normal)螟蝙,preLoader,執(zhí)行順序則相反民傻。
最后輸出以下組合對象:
// /lib/NormalModuleFactory.js
callback(null, {
context: context,
request: loaders
.map(loaderToIdent)
.concat([resource])
.join("!"),
dependencies: data.dependencies,
userRequest,
rawRequest: request,
loaders,
resource,
matchResource,
resourceResolveData,
settings,
type,
parser: this.getParser(type, settings.parser), // 創(chuàng)建 parser 并緩存
generator: this.getGenerator(type, settings.generator), // 創(chuàng)建 generator 并緩存
resolveOptions
});
其中 getParser 的主要作用是為 module 提供解析模塊為 ast 的 parser胰默。
createParser 時會根據(jù)不同 type 返回不同的 parser 實(shí)例场斑。
getGenerator 主要作用是為 module 提供模版生成時的 generator (的)方法。
createGenerator 時根據(jù) type 不同返回不同的 generator 實(shí)例(目前代碼里都是返回一致的 new JavascriptGenerator() )牵署。
跳出 NormalModuleFactory 的 resolver 鉤子函數(shù)漏隐,執(zhí)行 resolver 函數(shù)回調(diào),至此 resolve 流程結(jié)束奴迅。
4.2 執(zhí)行 loader 階段青责,初始化模塊 module,并用 loader 倒序轉(zhuǎn)譯
開啟構(gòu)建 module 流程取具。
new NormalModule(result)
得到初始化的 module ?? 在 build 過程中執(zhí)行 runLoaders 處理源碼脖隶,先正序讀取每個 loader 并執(zhí)行它的 pitch,再倒序執(zhí)行每個 loader 的 normal者填,最后得到一個編譯后的字符串或 Buffer浩村。
(繼續(xù)看/lib/NormalModuleFactory.js
) 觸發(fā) normalModuleFactory.hooks:afterResolve 和 normalModuleFactory.hooks:createModule,let createdModule = this.hooks.createModule.call(result);
的這個 result 參數(shù)就是normalModuleFactory.hooks.resolver.tap 輸出的組合 object占哟。如果不存在項目配置的自定義 module,就使用new NormalModule(result)
生成的 module酿矢。
跳出 factory 鉤子 tap 綁定的函數(shù)榨乎,執(zhí)行factory(result, (err, module) => {})
的回調(diào),傳入的 module 就是我們初始化的 NormalModule 實(shí)例瘫筐,進(jìn)行依賴緩存后蜜暑,結(jié)束 create 方法,回到/lib/Compilation.js
執(zhí)行 moduleFactory.create 的回調(diào)策肝。
// /lib/Compilation.js
addModule(module, cacheGroup) {
const identifier = module.identifier(); // 即 module.request
// 根據(jù) identifie 判斷`compilation._modules`是否有該 module
const alreadyAddedModule = this._modules.get(identifier);
if (alreadyAddedModule) { // 如果已經(jīng)存在則返回如下 object
return {
module: alreadyAddedModule,
issuer: false,
build: false,
dependencies: false
};
}
// ...
// 將這個 module 保存到全局的 `Compilation`的`modules` 數(shù)組和`_modules` Map 對象中
this._modules.set(identifier, module);
this.modules.push(module);
return { // 如是從未添加到`compilation`的模塊肛捍,返回如下對象
module: module,
issuer: true,
build: true,
dependencies: true
};
}
_addModuleChain(context, dependency, onModule, callback) {
// ...
moduleFactory.create({...}, (err, module) => {
// create 執(zhí)行完的回調(diào)
// 用初始化的 module 作為參數(shù)調(diào)用 addModule
const addModuleResult = this.addModule(module);
module = addModuleResult.module;
// 如果是入口文件還會將 module 保存到 `Compilation.entries`
onModule(module);
dependency.module = module;
module.addReason(null, dependency); // 添加該`module`被哪些模塊依賴的信息,會存到 module.reasons 數(shù)組里
if (addModuleResult.build) { // 沒有添加過的模塊 build 屬性默認(rèn)是 true
this.buildModule(module, false, null, null, err => {
afterBuild();
})
}
})
}
先執(zhí)行this.addModule
之众,返回一個對象 addModuleResult
:
如果這個 module 之前未被添加到compilation
拙毫,將它保存到全局compilation
對象的modules
數(shù)組和_modules
Map 對象中,返回結(jié)果的 module 屬性為當(dāng)前模塊棺禾,issuer缀蹄、build、dependencies 的值都為 true膘婶;如果已存在缺前,則 module 屬性為查到的值,其他三個屬性都為 false悬襟。
調(diào)用this.buildModule
進(jìn)入 build 階段衅码。做了回調(diào)緩存后,觸發(fā)compilation.hooks:buildModule
脊岳,然后執(zhí)行module.build()
逝段。
module 是 NormalModule 的實(shí)例垛玻,我們來到/lib/NormalModule.js
看 build 方法:在設(shè)置一些屬性后調(diào)用了 NormalModule 的 doBuild 方法。
// /lib/NormalModule.js
doBuild(options, compilation, resolver, fs, callback) {
// 為所有的 loader 提供上下文環(huán)境
const loaderContext = this.createLoaderContext(
resolver,
options,
compilation,
fs
);
runLoaders(
{
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
readResource: fs.readFile.bind(fs)
},
(err, result) => {
//...
}
);
}
runLoaders 方法來自 loader-runner惹恃,作用是按規(guī)定流程執(zhí)行各種 loader夭谤,將模塊源碼后處理成一個 String 或 Buffer 格式的 JavaScript (可能還有個 SourceMap)。
關(guān)于 loader 本身的機(jī)制可以看下這篇: webpack 之 Loader 詳解
主要流程:
runLoaders ?? iteratePitchingLoaders (正序 require 每個 loader) ?? loadLoader (將當(dāng)前 loader 的模塊導(dǎo)出函數(shù)賦值到loaderContext.loaders[index].normal
巫糙、loader 模塊的pitch 函數(shù)
賦值到loaderContext.loaders[index].pitch
朗儒,然后執(zhí)行pitch 函數(shù)
[如果有的話]) ?? 讀取完當(dāng)前模塊的全部 loader,執(zhí)行 processResource (設(shè)置 loaderIndex 為最后一個 loader 的 index / 轉(zhuǎn)換 buffer) ?? iterateNormalLoaders (倒序執(zhí)行所有 loader [normal]
)
其中執(zhí)行 pitch 和 normal 都調(diào)用了 runSyncOrAsync (同步或者異步執(zhí)行 loader) 方法参淹,如果在 iteratePitchingLoaders 階段某個 pitch 有返回值醉锄,則直接進(jìn)入 iterateNormalLoaders 階段 (將該pitch返回值作為參數(shù)),從前一個讀取的 loader 開始倒序執(zhí)行浙值。
// node_modules/loader-runner/lib/LoaderRunner.js
// 同步或者異步執(zhí)行 loader 函數(shù)
function runSyncOrAsync(fn, context, args, callback) {
try {
var result = (function LOADER_EXECUTION() {
return fn.apply(context, args); // 執(zhí)行 loader 函數(shù)恳不,參數(shù)傳遞前一個 loader 的執(zhí)行結(jié)果
})();
if (isSync) {
// ...
return callback(null, result);
}
} catch(e) {
callback(e)
}
}
// 核心方法,按正序 require 每個 loader
function iteratePitchingLoaders(options, loaderContext, callback) {
// 發(fā)現(xiàn)讀取完所有 loader 后开呐,執(zhí)行 processResource 方法
// 第一次執(zhí)行 loaderIndex 是 0烟勋,如果 loaders 數(shù)組的個數(shù)是 0 才走processResource,如果當(dāng)前模塊有 loader 則繼續(xù)往下走筐付。
if(loaderContext.loaderIndex >= loaderContext.loaders.length) return processResource(options, loaderContext, callback);
// 根據(jù) loaderIndex 獲取當(dāng)前要讀取的 loader 對象
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// 如果當(dāng)前 loader 的 pitch 階段已經(jīng)執(zhí)行過卵惦,則繼續(xù)迭代執(zhí)行
if(currentLoaderObject.pitchExecuted) {
// 增序后遞歸讀取下一個 loader
loaderContext.loaderIndex++;
return iteratePitchingLoaders(options, loaderContext, callback);
}
// node_modules/loader-runner/lib/loadLoader.js
// loadLoader 這個方法負(fù)責(zé)加載當(dāng)前 loader 模塊,將 loader 模塊導(dǎo)出的函數(shù)賦值到 loader.normal, 模塊的 pitch 方法賦值到 loader.pitch
loadLoader(currentLoaderObject, function(err) {
if(err) {
loaderContext.cacheable(false);
return callback(err);
}
// 獲取 loader 模塊的 pitch 方法
var fn = currentLoaderObject.pitch;
currentLoaderObject.pitchExecuted = true;
// 如沒有 pitch 函數(shù)直接 require 下一個 loader
if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
// 有 pitch 則執(zhí)行 pitch 函數(shù)瓦戚,根據(jù) runSyncOrAsync 的回調(diào)在沒報錯的情況下有無返回其他參數(shù)沮尿,決定是否繼續(xù)讀取剩下的loader
runSyncOrAsync(
fn,
loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
if(args.length > 0) { // 執(zhí)行 pitch 有返回結(jié)果則將 loaderIndex 減序,并將返回結(jié)果作為 iterateNormalLoaders 的參數(shù)较解,開始倒序執(zhí)行前面已經(jīng) require 的 loader
loaderContext.loaderIndex--; // 這個減序是為了從前一個讀取的 loader 開始執(zhí)行畜疾,不執(zhí)行當(dāng)前 loader 的 normal
iterateNormalLoaders(options, loaderContext, args, callback);
} else { // pitch 沒有返回值繼續(xù)讀取下一個 loader
iteratePitchingLoaders(options, loaderContext, callback);
}
}
);
});
}
// 設(shè)置 loaderIndex 為最后一個 loader 的 index
// 轉(zhuǎn)換 buffer 后再走 iterateNormalLoaders
function processResource(options, loaderContext, callback) {
loaderContext.loaderIndex = loaderContext.loaders.length - 1;
if(loaderContext.resourcePath) {
iterateNormalLoaders(options, loaderContext, [buffer], callback);
} else {
iterateNormalLoaders(options, loaderContext, [null], callback);
}
}
// 倒序執(zhí)行所有 loader
function iterateNormalLoaders(options, loaderContext, args, callback) {
// 執(zhí)行完所有 loader return,去執(zhí)行 callback 即 runLoaders 的回調(diào)
if(loaderContext.loaderIndex < 0) return callback(null, args);
// 獲取當(dāng)前 loader 模塊對象
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// 如果當(dāng)前 loader 的 normal 階段已經(jīng)執(zhí)行過印衔,則繼續(xù)迭代:
// 減序后遞歸執(zhí)行前一個 loader
if(currentLoaderObject.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, args, callback);
}
var fn = currentLoaderObject.normal;
currentLoaderObject.normalExecuted = true;
if(!fn) {
return iterateNormalLoaders(options, loaderContext, args, callback);
}
// 執(zhí)行 loader 函數(shù)
runSyncOrAsync(fn, loaderContext, args, function(err) {
var args = Array.prototype.slice.call(arguments, 1); // arg:[] 為 loader 轉(zhuǎn)換結(jié)果(String或者Buffer+可能的SourceMap)
iterateNormalLoaders(options, loaderContext, args, callback); // 遞歸執(zhí)行 loader啡捶,將 loader 轉(zhuǎn)換結(jié)果一并傳入
});
}
exports.runLoaders = function runLoaders(options, callback) {
// 讀取 options
var resource = options.resource || "";
var loaders = options.loaders || [];
var loaderContext = options.context || {};
var readResource = options.readResource || readFile;
// 準(zhǔn)備 loader 對象
loaders = loaders.map(createLoaderObject);
loaderContext.loaderIndex = 0;
loaderContext.loaders = loaders;
// ...
var processOptions = {
resourceBuffer: null,
readResource: readResource
};
iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {});
}
4.3 parse 階段,收集依賴
調(diào)用
parser
將上一步runLoaders
的編譯結(jié)果利用 acorn 庫轉(zhuǎn)換為 ast当编。生成的 AST 劃分為三部分:ImportDeclaration
届慈、FunctionDeclaration
和VariablesDeclaration
。?? 遍歷 ast忿偷,根據(jù)導(dǎo)入導(dǎo)出及異步的情況觸發(fā)相關(guān)鉤子插件來收集依賴金顿,這些依賴用于解析遞歸依賴和模板操作 ?? 根據(jù)每個 module 的相關(guān)信息生成各自唯一的 buildHash
runLoaders
運(yùn)行后的回調(diào)里執(zhí)行了createSource
,然后判斷經(jīng) loaders 編譯的 result 是否有第三個參數(shù)(為 object 格式)并且含有 webpackAST 屬性鲤桥,如果都符合則將 webpackAST 的值賦值到 _ast 上揍拆。
然后執(zhí)行doBuild
的回調(diào),根據(jù)項目配置項判斷是否需要 parse茶凳,若需要則執(zhí)行:
// /lib/NormalModule.js
const result = this.parser.parse(
// 如果 this._ast 不存在則傳 this._source._value嫂拴,即代碼字符串
this._ast || this._source.source(),
{
current: this,
module: this,
compilation: compilation,
options: options
},
(err, result) => {
handleParseResult(result);
}
);
this.parser 即是 resolve 階段最終得到對象里的 parser播揪,即 NormalModuleFactory 的 getParser 方法,它調(diào)用 NormalModuleFactory.hooks: createParser, parse
筒狠。
而編譯前準(zhǔn)備注冊的 webpack 默認(rèn)插件 JavascriptModulesPlugin 監(jiān)聽了 createParser 鉤子猪狈,提供了 /lib/Parser.js 的實(shí)例。
// /lib/JavascriptModulesPlugin.js`
const Parser = require("./Parser");
normalModuleFactory.hooks.createParser
.for("javascript/auto")
.tap("JavascriptModulesPlugin", options => {
return new Parser(options, "auto");
});
// /lib/Parser.js
const acorn = require("acorn"); // node_modules/acorn/dist/acorn.js
const acornParser = acorn.Parser;
class Parser extends Tapable {
parse(source, initialState) { // 提供給 Parser 實(shí)例的 parse 方法
ast = Parser.parse(source, {...}); // 執(zhí)行 Parser 類的靜態(tài)方法
// 觸發(fā) program 鉤子上的插件(HarmonyDetectionParserPlugin 和 UseStrictPlugin) 回調(diào)
// 根據(jù)是否有 import/export 和 use strict 增加依賴:HarmonyCompatibilityDependency, HarmonyInitDependency辩恼,ConstDependency
if (this.hooks.program.call(ast, comments) === undefined) {
this.detectMode(ast.body); // 檢測當(dāng)前執(zhí)行塊是否有 use strict雇庙,并設(shè)置 this.scope.isStrict = true
this.prewalkStatements(ast.body); // 處理 import 進(jìn)來的變量,是 import 就增加依賴 HarmonyImportSideEffectDependency灶伊,HarmonyImportSpecifierDependency;
// 處理 export 出去的變量疆前,是 export 就增加依賴 HarmonyExportHeaderDependency,HarmonyExportSpecifierDependency聘萨;還會處理其他相關(guān)導(dǎo)入導(dǎo)出的變量
this.blockPrewalkStatements(ast.body); // 處理塊遍歷
this.walkStatements(ast.body); // 深入函數(shù)內(nèi)部在 walkFunctionDeclaration 進(jìn)行遞歸竹椒,繼續(xù)查找 ast 上的依賴,異步此處深入會增加依賴 ImportDependenciesBlock
}
}
static parse(code, options) {
try { // acorn 的 parse
ast = acornParser.parse(code, parserOptions);
} catch (e) {}
}
}
由上分析可見:this.parser.parse
就是 Parser 實(shí)例的原型方法米辐,而實(shí)際的處理函數(shù)是 acorn 庫提供的胸完。由此通過 acorn.Parser.parse 方法等一系列處理,得到了源碼對應(yīng)的 ast翘贮。
觸發(fā) Parser 的 program 鉤子舶吗,根據(jù) import/export 即模塊間的相互依賴關(guān)系遍歷 ast 收集依賴,之后在對應(yīng)的module.dependencies
上增加相應(yīng)的依賴择膝。
在后面 generate / render 階段,會調(diào)用這些依賴 (dependencies) 對應(yīng)的 template.apply 來渲染生成代碼資源检激。(放在本文最后結(jié)合示例截圖說明)
parse 處理完畢后肴捉,執(zhí)行handleParseResult
,調(diào)用this._initBuildHash(compilation)
叔收。采用 nodeJS
提供的加密模塊 crypto 進(jìn)行 hash
加密齿穗,將結(jié)果賦值給this._buildHash
。
// /lib/NormalModule.js
_initBuildHash(compilation) {
// createHash 即 new BulkUpdateDecorator(require("crypto").createHash(algorithm))
const hash = createHash(compilation.outputOptions.hashFunction);
if (this._source) {
hash.update("source"); // 更新 hash source 內(nèi)容
this._source.updateHash(hash); // this._value
}
hash.update("meta"); // 更新 hash meta 內(nèi)容
hash.update(JSON.stringify(this.buildMeta)); // 更新 hash this.buildMeta
this._buildHash = /** @type {string} */ (hash.digest("hex")); // 得到 hash 值
}
又回到 Compilation.js 執(zhí)行module.build()
的回調(diào)饺律,按照在文件中出現(xiàn)的先后順序?qū)?code>module.dependencies進(jìn)行排序窃页,然后觸發(fā) Compilation.hooks: succeedModule
。接著執(zhí)行this.buildModule
的回調(diào)复濒,運(yùn)行afterBuild()
:
4.4 遞歸處理依賴階段 (重復(fù)以上步驟)
根據(jù) module 間的相互依賴關(guān)系脖卖,遞歸解析所有依賴 module。即 resolve ?? 執(zhí)行 loader ?? parse ?? 收集并處理該模塊依賴的模塊巧颈,直到所有入口依賴 (直接或間接) 的文件都經(jīng)過了這些步驟的處理畦木。最終返回一個入口 module。
// /lib/Compilation.js
_addModuleChain(context, dependency, onModule, callback) {
// ...
moduleFactory.create(
context,
entry,
module => this.entries.push(module), // 提供把 module 添加 compilation.entries 的方法
(err, module) => {
const afterBuild = () => {
// 在 this.addModule(module) 時如果發(fā)現(xiàn)`module.request`存在`identifier`標(biāo)識砸泛,則會設(shè)置 addModuleResult.dependencies 為 false十籍,即可避免該模塊被重復(fù)解析/創(chuàng)建
if (addModuleResult.dependencies) { // 如果該模塊是首次解析蛆封,即從未被添加過
this.processModuleDependencies(module, err => { // 去處理依賴
if (err) return callback(err);
callback(null, module); // 8. 執(zhí)行 addEntry 方法中 this._addModuleChain 的回調(diào),生成一個入口 module勾栗。歸根究底起來就是 hooks.make 鉤子的回調(diào)惨篱,調(diào)用 compilation.finish 方法
});
} else {
return callback(null, module);
}
}
);
}
processModuleDependencie
會分別處理 module 的 dependencies、blocks (import()
引入的異步依賴)和 variables(內(nèi)部變量 __resourceQuery)围俘,其中 blocks 會遞歸調(diào)用處理砸讳。整理過濾出無 Identifier 標(biāo)識的 module,得到處理結(jié)果 sortedDependencies楷拳。
跟著調(diào)用this.addModuleDependencies(module, sortedDependencies, this.bail, null, true, callback)
并傳入 module 和 sortedDependencies绣夺。
// /lib/Compilation.js
addModuleDependencies(module, dependencies, bail, cacheGroup, cacheGroup, callback) {
asyncLib.forEach(
dependencies,
(item, callback) => { // callback 是當(dāng)前 item 所有迭代功能完成或發(fā)生錯誤時調(diào)用的回調(diào),調(diào)用 callback() 可以手動觸發(fā)
const dependencies = item.dependencies;
const semaphore = this.semaphore;
semaphore.acquire(() => { // 并發(fā)編譯隊列控制
const factory = item.factory;
// 1. 并行調(diào)用每個依賴的 NormalModuleFactory.create
factory.create({...}, (err, dependentModule) => {
// 在經(jīng)過`factory.create`的 2. resolve 階段 ?? 3. 初始化`module` 后`create`完成欢揖,開始執(zhí)行`create`的回調(diào)
// 錯誤處理等...
if (!dependentModule) { // 如果 create 沒有返回 module
semaphore.release();
return process.nextTick(callback);
}
const iterationDependencies = depend => {
for (let index = 0; index < depend.length; index++) {
const dep = depend[index];
dep.module = dependentModule;
dependentModule.addReason(module, dep);
}
};
// 4. 執(zhí)行 addModule陶耍,得到處理后的包含當(dāng)前 module 信息的對象
const addModuleResult = this.addModule(dependentModule,cacheGroup);
dependentModule = addModuleResult.module;
iterationDependencies(dependencies);
const afterBuild = () => {
if (recursive && addModuleResult.dependencies) { // 7. 如果是遞歸遍歷(調(diào)用 addModuleDependencies 時傳的 recursive 是 true )且該模塊從未被添加過
// 執(zhí)行 processModuleDependencies 處理該模塊的依賴,再將流程遞歸走下去
// 第一次肯定是走這里她混,這時傳遞的 callback 是 asyncLib.forEach 的回調(diào)烈钞,這個回調(diào)不出錯的話,調(diào)用后是在當(dāng)前輪依賴遍歷完執(zhí)行的
this.processModuleDependencies(dependentModule, callback);
} else {
return callback(); // 7. 如該模塊已被添加過坤按,則等本輪迭代任務(wù)執(zhí)行完再執(zhí)行 asyncLib.forEach 的回調(diào)
}
};
if (addModuleResult.build) { // 5. 執(zhí)行 buildModule
this.buildModule(dependentModule, ... module, dependencies, err => {
semaphore.release();
afterBuild(); // 6. 執(zhí)行 afterBuild()
});
}
});
});
},
err => {
// 錯誤是在一個 Compilation 的引用的閉包中創(chuàng)建的毯欣,因此 errors 會暴露出 Compilation 對象。
if (err) return callback(err);
// 當(dāng)?shù)鱜(item, callback) => {}`的第二個參數(shù)(回調(diào))被調(diào)用臭脓,會觸發(fā)這里酗钞;如果調(diào)用的時候傳遞了參數(shù)`callback(sth)`,那么這個參數(shù)會被作為 err 值傳遞
// 這里的 callback 是`addModuleDependencies`的最后一個參數(shù)来累,也就是`_addModuleChain`內(nèi)的 build 完成后 this.processModuleDependencies(...) 傳的那個函數(shù)砚作,見上方代碼塊 8.標(biāo)注...
// process.nextTick 是把 callback 放到當(dāng)前宏任務(wù)出棧前執(zhí)行,即當(dāng)前模塊的依賴遍歷完 add 完執(zhí)行
// 面遞歸處理依賴就是 asyncLib.forEach 的 callback了嘹锁,就是把它上一輪模塊遍歷完成的回調(diào)一直放到到下一棧宏任務(wù)開始前執(zhí)行葫录,就一直套娃放 callback 直到所有模塊都被添加過,8.標(biāo)注的回調(diào)執(zhí)行
return process.nextTick(callback);
}
);
}
這里的 asyncLib.forEach 就是 neo-async 庫的 each 方法 领猾。它一般用來對集合進(jìn)行異步迭代米同,它的回調(diào)(最后一個參數(shù)即err=> {}
部分)傳給了 iterator 迭代器(第二個參數(shù)的最后一個參數(shù)),在迭代器函數(shù)內(nèi)手動調(diào)用這個回調(diào)的話摔竿,會在傳遞 err 或 iterator 全部執(zhí)行完成后執(zhí)行該回調(diào)面粮。詳細(xì)可以看這里 ?? 詳細(xì)說明
以及 process.nextTick 的用法 ?? 理解 process.nextTick()
它并行調(diào)用每個依賴的 NormalModuleFactory.create()
,與前文 【執(zhí)行 loader 階段拯坟,初始化模塊 module】部分提到的moduleFactory.create
功能一致但金,因此重復(fù)為每個依賴走以下流程:
1. 執(zhí)行 NormalModuleFactory.create ?? 2. resolve 階段 ?? 3. 初始化 module ?? 4. NormalModuleFactory.create 完成,執(zhí)行它的回調(diào): 主要內(nèi)容為 addModule ?? 5. buildModule ?? 6. afterBuild ?? 7. 如果該模塊從未 add 過則走 processModuleDependencies 處理依賴郁季,繼續(xù)遞歸 asyncLib.forEach 并行流程冷溃。
就這樣钱磅,從入口module
開始,根據(jù)module
之間的依賴關(guān)系似枕,遞歸將所有的module
都轉(zhuǎn)換編譯盖淡。
直到層層依賴都轉(zhuǎn)換完成,執(zhí)行return process.nextTick(callback);
凿歼,將在下一次事件循環(huán)tick之前調(diào)用 callback褪迟,即執(zhí)行_addModuleChain
中afterBuild
方法的this.processModuleDependencies
的回調(diào),即this._addModuleChain
傳入的回調(diào)函數(shù):
未出錯的話能拿到一個入口 module:
我們可以看到入口模塊的 dependencies 和 blocks 存放了名為"HarmonyCompatibilityDependency"答憔、"HarmonyExportHeaderDependency"味赃、"ImportDependenciesBlock"之類的依賴。上文我們提到在 render 階段會調(diào)用這些依賴對應(yīng)的模版來生成代碼資源虐拓,這里對這些依賴作一個簡單的解釋:
-
HarmonyCompatibilityDependency
:對應(yīng)模板HarmonyExportDependencyTemplate
心俗,會在源碼的最前面添加像:__webpack_require__.r(__webpack_exports__);
這樣的代碼,用于定義 exports:__esModule -
HarmonyInitDependency
:對應(yīng)模板HarmonyInitDependencyTemplate
-
ConstDependency
:對應(yīng)模板ConstDependencyTemplate
蓉驹,會在源碼里將同步 import 語句刪掉 -
HarmonyImportSideEffectDependency"
:對應(yīng)模板HarmonyImportSideEffectDependencyTemplate
城榛,調(diào)用父類 HarmonyImportDependencyTemplate 的 apply,即為空 -
HarmonyImportSpecifierDependency
:對應(yīng)模板HarmonyImportSpecifierDependencyTemplate
态兴,會在源碼里將引入的變量替換為 webpack 對應(yīng)的包裝變量 -
HarmonyExportHeaderDependency
:對應(yīng)模板HarmonyExportDependencyTemplate
狠持,會在源碼里將關(guān)鍵字 export 刪掉 -
HarmonyExportSpecifierDependency
:對應(yīng)模板HarmonyExportSpecifierDependencyTemplate
,執(zhí)行 apply 為空 -
ImportDependenciesBlock
(異步模塊):對應(yīng)模板ImportDependencyTemplate
瞻润, 會在源碼里將本 demo 中的import('./c.js')
替換為Promise.resolve(/*! import() */).then(__webpack_require__.bind(null, /*! ./c.js */ "./src/c.js"))
再觸發(fā)compilation.hooks: succeedEntry
喘垂,最后執(zhí)行調(diào)用compilation.addEntry
時傳入的回調(diào),到此 module 生成結(jié)束绍撞。
下文:淺析 webpack 打包流程(原理) 三 - 生成 chunk
webpack 打包流程系列(未完):
淺析 webpack 打包流程(原理) - 案例 demo
淺析 webpack 打包流程(原理) 一 - 準(zhǔn)備工作
淺析 webpack 打包流程(原理) 二 - 遞歸構(gòu)建 module
淺析 webpack 打包流程(原理) 三 - 生成 chunk
淺析 webpack 打包流程(原理) 四 - chunk 優(yōu)化
淺析 webpack 打包流程(原理) 五 - 構(gòu)建資源
淺析 webpack 打包流程(原理) 六 - 生成文件
參考鳴謝:
webpack打包原理 ? 看完這篇你就懂了 !
webpack 透視——提高工程化(原理篇)
webpack 透視——提高工程化(實(shí)踐篇)
webpack 4 源碼主流程分析
[萬字總結(jié)] 一文吃透 Webpack 核心原理
`