webpack系列之五module生成2

滴滴WebApp架構組發表於2019-04-30

作者:崔靜

上一篇 module生成1中我們已經分析了 webpack 是如何根據 entry 配置找到對應的檔案的,接下來就是將檔案轉為 module 了。這個長長的過程,可以分成下面幾個階段

  1. create: 準備資料,生成 module 例項。
  2. add: 資訊儲存到 Compilation 例項上。
  3. build: 分析檔案內容。
  4. processDep: 處理3步驟中解析得到的依賴,新增到編譯鏈條中。

後面會以一個簡單的 js 檔案為例,看整個主流程

// a.js
export const A = 'a'

// demo.js,webpack 入口檔案
import { A } from './a.js'
function test() {
  const tmp = 'something'
  return tmp + A
}
const r = test()
複製程式碼

create

_addModuleChain 之後就是檔案的 create 階段,正式進入檔案處理環節。上面一節我們介紹 MultipleEntryPlugin 中曾簡單提到過:_addModuleChain 的回撥中執行的是 moduleFactory.create。對於上面例子來說這裡 create 方法,其實執行是 nromalModuleFactory.create 方法,程式碼主邏輯如下:

create(data, callback) {
	//...省略部分邏輯
	this.hooks.beforeResolve.callAsync(
		{
			contextInfo,
			resolveOptions,
			context,
			request,
			dependencies
		},
		(err, result) => {
			//...
			// 觸發 normalModuleFactory 中的 factory 事件。
			const factory = this.hooks.factory.call(null);
			// Ignored
			if (!factory) return callback();
			factory(result, (err, module) => {
				//...
				callback(null, module);
			});
		}
	);
}
複製程式碼

單獨看 create 內部邏輯:

  • 觸發 beforeResolve 事件:這裡 beforeResolve 事件中沒有做任務處理,直接進入回撥函式
  • 觸發 NormalModuleFactory 中的 factory 事件。在 NormalModuleFactory 的 constructor 中有一段註冊 factory 事件的邏輯。
  • 執行 factory 方法(具體程式碼位於 NormalModuleFactory 的 constructor 中),主要流程如下:

factory流程

  1. resolver 階段:得到 demo.js 的路徑資訊以及涉及到的 loader 和 loader 的路徑(詳細過程參考 resolver 和 loader)。這一步完成後,生成 module 的準備工作已經完成。
  2. createModule 階段:生成一個 module 例項,將上一步的資料存入例項中。

到此已經得到了一個 module 例項。為了方便,後文我們將這個 module 例項稱為 demo module。

addModule

得到 demo module 之後,需要將其儲存到全域性的 Compilation.modules 陣列中和 _modules 物件中。

這個過程中還會為 demo module 新增 reason ,即哪個 module 中依賴了 demo module。由於是 demo.js 是入口檔案,所以這個 reason 自然就是 SingleEntryDependency。 並且對於入口檔案來說,還會被新增到 Compilation.entries 中。

// moduleFactory.create 的 callback 函式
(err, module) => {
	//...
	
	let afterFactory;
	
	//...
	
	// addModule 會執行 this._modules.set(identifier, module); 其中 identifier 對於 normalModule 來說就是 module.request,即檔案的絕對路徑
	// 和 this.modules.push(module);
	const addModuleResult = this.addModule(module);
	module = addModuleResult.module;
	
	// 對於入口檔案來說,這裡會執行 this.entries.push(module);
	onModule(module);
	
	dependency.module = module;
	module.addReason(null, dependency);
	
	//... 開始 build 階段
}
複製程式碼

這個階段可以認為是 add 階段,將 module 的所有資訊儲存到 Compilation 中,以便於在最後打包成 chunk 的時候使用。隨後在這個回撥函式中,會呼叫 this.buildModule 進入 build 階段。

build

demo module 是 NormalModule 的例項,所以 Compilation.buildModule 中呼叫的 module.build 方法實際為 NormalModule.build 方法。build 方法主邏輯如下:

// NormalModule.build 方法
build(options, compilation, resolver, fs, callback) {
  //...
  return this.doBuild(options, compilation, resolver, fs, err => {
    //...
    try {
       // 這裡會將 source 轉為 AST,分析出所有的依賴
		const result = this.parser.parse(/*引數*/);
		if (result !== undefined) {
			// parse is sync
			handleParseResult(result);
		}
	} catch (e) {
		handleParseError(e);
	}
  })
}

// NormalModule.doBuild 方法
doBuild(options, compilation, resolver, fs, callback) {
	//...
	// 執行各種 loader
	runLoaders(
		{
			resource: this.resource,
			loaders: this.loaders,
			context: loaderContext,
			readResource: fs.readFile.bind(fs)
		},
		(err, result) => {
			//...
			// createSource 會將 runLoader 得到的結果轉為字串以便後續處理
			this._source = this.createSource(
				this.binary ? asBuffer(source) : asString(source),
				resourceBuffer,
				sourceMap
			);
			//...
		}
	);
}
複製程式碼

build 分成兩大塊: doBuild 和 doBuild 的回撥。

doBuild:獲取 source

在 doBuild 之前,我們實際上只得到了檔案的路徑,並沒有獲取到檔案的真正內容,而在這一環節在 doBuild 的 runLoader 方法中會根據這個路徑得到讀取檔案的內容,然後經過各種 loader 處理,得到最終結果,這部分已經在 loader 中分析過,參見 webpack系列之四loader詳解2

回撥:處理 source

上一步得到了檔案的 source 是 demo.js 的字串形式,如何從這個字串中得到 demo.js 的依賴呢?這就需要對這個字串進行處理了,this.parser.parse 方法被執行。

接下來我們詳細看一下 parse 的過程,具體的程式碼在 lib/Parser.js 中。程式碼如下:

parse(source, initialState) {
	let ast;
	let comments;
	if (typeof source === "object" && source !== null) {
		ast = source;
		comments = source.comments;
	} else {
		comments = [];
		ast = Parser.parse(source, {
			sourceType: this.sourceType,
			onComment: comments
		});
	}

	const oldScope = this.scope;
	const oldState = this.state;
	const oldComments = this.comments;
	
	// 設定 scope,可以理解為和程式碼中個作用域是一致的
	this.scope = {
		topLevelScope: true,
		inTry: false,
		inShorthand: false,
		isStrict: false,
		definitions: new StackedSetMap(),
		renames: new StackedSetMap()
	};
	const state = (this.state = initialState || {});
	this.comments = comments;
	
	// 遍歷 AST,找到所有依賴
	if (this.hooks.program.call(ast, comments) === undefined) {
		this.detectStrictMode(ast.body);
		this.prewalkStatements(ast.body);
		this.walkStatements(ast.body);
	}
	this.scope = oldScope;
	this.state = oldState;
	this.comments = oldComments;
	return state;
}
複製程式碼

在 parse 方法中,source 引數可能會有兩種形式:ast 物件或者 string。為什麼會有 ast 物件呢?要解釋這個問題,我們先看一個引數 source 從哪裡來的。回到 runLoaders 的回撥中看一下

runLoaders({...}, (err, result) => {
  //...省略其他內容
  const source = result.result[0];
  const sourceMap = result.result.length >= 1 ? result.result[1] : null;
  const extraInfo = result.result.length >= 2 ? result.result[2] : null;
  //...
  this._ast =
		typeof extraInfo === "object" &&
		extraInfo !== null &&
		extraInfo.webpackAST !== undefined
			? extraInfo.webpackAST
			: null;
})
複製程式碼

runLoader 結果是一個陣列: [source, sourceMap, extraInfo], extraInfo.webpackAST 如果存在,則會被儲存到 module._ast 中。也就是說,loader 除了返回處理完了 source 之後,還可以返回一個 AST 物件。在 doBuild 的回撥中會優先使用 module._ast

const result = this.parser.parse(
	this._ast || this._source.source(),
	//...
)
複製程式碼

這時傳入 parse 方法中的就是 loader 處理之後,返回的 extraInfo.webpackAST,型別是 AST 物件。這麼做的好處是什麼呢?如果 loader 處理過程中已經執行過將檔案轉化為 AST 了,那麼這個 AST 物件儲存到 extraInfo.webpackAST 中,在這一步就可以直接複用,以避免重複生成 AST,提升效能。

回到正題 parse 方法中,如果 source 是字串,那麼會經過 Parser.parse 之後被轉化為 AST(webpack 中使用的是 acorn)。到這裡 demo.js 中的原始碼會被解析成一個樹狀結構,大概結構如下圖

AST結構

接下來就是對這個樹進行遍歷了,流程為: program事件 -> detectStrictMode -> prewalkStatements -> walkStatements。這個過程中會給 module 增加很多 dependency 例項。每個 dependency 類都會有一個 template 方法,並且儲存了原來程式碼中的字元位置 range,在最後生成打包後的檔案時,會用 template 的結果替換 range 部分的內容。所以最終得到的 dependency 不僅包含了檔案中所有的依賴資訊,還被用於最終生成打包程式碼時對原始內容的修改和替換,例如將 return 'sssss' + A 替換為 return 'sssss' + _a_js__WEBPACK_IMPORTED_MODULE_0__["A"]

program 事件

program 事件中,會觸發兩個 plugin 的回撥:HarmonyDetectionParserPlugin 和 UseStrictPlugin

HarmonyDetectionParserPlugin 中,如果程式碼中有 import 或者 export 或者型別為 javascript/esm,那麼會增加了兩個依賴:HarmonyCompatibilityDependency, HarmonyInitDependency 依賴。

UseStrictPlugin 用來檢測檔案是否有 use strict,如果有,則增加一個 ConstDependency 依賴。這裡估計大家會有一個疑問:檔案中已經有了,為什麼還有增加一個這樣的依賴呢?在 UseStrictPlugin.js 的原始碼中有一句註釋

Remove "use strict" expression. It will be added later by the renderer again. This is necessary in order to not break the strict mode when webpack prepends code.

意識是說,webpack 在處理我們的程式碼的時候,可能會在開頭增加一些程式碼,這樣會導致我們原本寫在程式碼第一行的 "use strict" 不在第一行。所以 UseStrictPlugin 中通過增加 ConstDependency 依賴,來放置一個“佔位符”,在最後生成打包檔案的時候將其再轉為 "use strict"

總的來說,program 事件中,會根據情況給 demo module 增加依賴。

detectStrictMode

檢測當前執行塊是否有 use strict,並設定 this.scope.isStrict = true

prewalkStatements

prewalk 階段負責處理變數。結合上面的 demo AST ,我們看 prewalk 程式碼怎麼處理變數的。

首先進入 prewalkStatements 函式,該函式,對 demo AST 中第一層包含的三個結點分別呼叫 prewalkStatement

prewalkStatements(statements) {
	for (let index = 0, len = statements.length; index < len; index++) {
		const statement = statements[index];
		this.prewalkStatement(statement);
	}
}
複製程式碼

prewalkStatement 函式是一個巨大的 switch 方法,根據 statement.type 的不同,呼叫不同的處理函式。

prewalkStatement(statement) {
	switch (statement.type) {
		case "BlockStatement":
			this.prewalkBlockStatement(statement);
			break;
	    //...
	}
}
複製程式碼

第一個節點的 type 是 importDeclaration,所以會進入 prewalkImportDeclaration 方法。

prewalkImportDeclaration(statement) {
   // source 值為 './a.js'
	const source = statement.source.value;
	this.hooks.import.call(statement, source);
	// 如果原始程式碼為 import x, {y} from './a.js',則 statement.specifiers 包含 x 和 { y } ,也就是我們匯入的值
	for (const specifier of statement.specifiers) {
		const name = specifier.local.name; // 這裡是 import { A } from './a.js' 中的 A
		// 將 A 寫入 renames 和 definitions
		this.scope.renames.set(name, null);
		this.scope.definitions.add(name);
		switch (specifier.type) {
			case "ImportDefaultSpecifier":
				this.hooks.importSpecifier.call(statement, source, "default", name);
				break;
			case "ImportSpecifier":
				this.hooks.importSpecifier.call(
					statement,
					source,
					specifier.imported.name,
					name
				);
				break;
			case "ImportNamespaceSpecifier":
				this.hooks.importSpecifier.call(statement, source, null, name);
				break;
		}
	}
}
複製程式碼

涉及到的幾個外掛: import 事件會觸發 HarmonyImportDependencyParserPlugin,增加 ConstDependency 和 HarmonyImportSideEffectDependency。

importSpecifier 事件觸發 HarmonyImportDependencyParserPlugin,這個外掛中會在 rename 中設定 A 的值為 'imported var'

parser.hooks.importSpecifier.tap(
	"HarmonyImportDependencyParserPlugin",
	(statement, source, id, name) => {
	   // 刪除 A
		parser.scope.definitions.delete(name);
		// 然後將 A 設定為 import var
		parser.scope.renames.set(name, "imported var");
		if (!parser.state.harmonySpecifier)
			parser.state.harmonySpecifier = new Map();
		parser.state.harmonySpecifier.set(name, {
			source,
			id,
			sourceOrder: parser.state.lastHarmonyImportOrder
		});
		return true;
	}
);
複製程式碼

第一個節結束後,繼續第二個節點,進入 prewalkFunctionDeclaration。這裡只會處理函式名稱,並不會深入函式內容進行處理。

prewalkFunctionDeclaration(statement) {
	if (statement.id) {
	   // 將 function 的名字,test 新增到 renames 和 definitions 中
		this.scope.renames.set(statement.id.name, null);
		this.scope.definitions.add(statement.id.name);
	}
}
複製程式碼

其餘的這裡不一一介紹了,prewalkStatements 過程中會處理當前作用域下的變數,將其寫入 scope.renames 中,同時為 import 語句增加相關的依賴。

prewalk示意圖

walkStatements

上一步中 prewalkStatements 只負責處理當前作用域下的變數,如果遇到函式並不會深入內部。而在 walk 這一步則主要負責深入函式內部。對於 demo 的 AST 會深入第二個節點 FunctionDeclaration。

walkFunctionDeclaration(statement) {
	const wasTopLevel = this.scope.topLevelScope;
	this.scope.topLevelScope = false;
	for (const param of statement.params) this.walkPattern(param);
	// inScope 方法會生成一個新的 scope,用於對函式的遍歷。在這個新的 scope 中會將函式的引數名 和 this 記錄到 renames 中。
	this.inScope(statement.params, () => {
		if (statement.body.type === "BlockStatement") {
			this.detectStrictMode(statement.body.body);
			this.prewalkStatement(statement.body);
			this.walkStatement(statement.body);
		} else {
			this.walkExpression(statement.body);
		}
	});
	this.scope.topLevelScope = wasTopLevel;
}
複製程式碼

在遍歷之前會先呼叫 inScope 方法,生成一個新的 scope,然後對於 function(){} 的方法,繼續 detectStrictMode -> prewalkStatement -> walkStatement。這個過程和遍歷 body 類似,我們這裡跳過一下,直接看 return temp + A 中的 A,即 AST 中 BinaryExpression.right 葉子節點。因為其中的 A 是我們引入的變數, 所以會有所不同,程式碼如下

walkIdentifier(expression) {
    // expression.name = A
	if (!this.scope.definitions.has(expression.name)) {
		const hook = this.hooks.expression.get(
			this.scope.renames.get(expression.name) || expression.name
		);
		if (hook !== undefined) {
			const result = hook.call(expression);
			if (result === true) return;
		}
	}
}
複製程式碼

在 prewalk 中針對 A 變數有一個處理,重新設定會將其從 definitions 中刪除掉(HarmonyImportDependencyParserPlugin 外掛中邏輯)。

// 刪除 A
parser.scope.definitions.delete(name);
// 然後將 A 設定為 import var
parser.scope.renames.set(name, "imported var");
複製程式碼

所以這裡會進入到 if 邏輯中,同時this.scope.renames.get(expression.name) 這個值的結果就是 'import var'。同樣是在 HarmonyImportDependencyParserPlugin 外掛中,還註冊了一個 'import var' 的 expression 事件:

parser.hooks.expression
.for("imported var")
.tap("HarmonyImportDependencyParserPlugin", expr => {
	const name = expr.name;// A
	// parser.state.harmonySpecifier 會在 prewalk 階段寫入
	const settings = parser.state.harmonySpecifier.get(name);
	// 增加一個 HarmonyImportSpecifierDependency 依賴
	const dep = new HarmonyImportSpecifierDependency(
		settings.source,
		parser.state.module,
		settings.sourceOrder,
		parser.state.harmonyParserScope,
		settings.id,
		name,
		expr.range,
		this.strictExportPresence
	);
	dep.shorthand = parser.scope.inShorthand;
	dep.directImport = true;
	dep.loc = expr.loc;
	parser.state.module.addDependency(dep);
	return true;
});
複製程式碼

因此在 walkIdentifier 方法中通過 this.hooks.expression.get 獲取到這個事件的 hook,然後執行。執行結束後,會給 module 增加一個 HarmonyImportSpecifierDependency 依賴,同樣的,這個依賴同時也是一個佔位符,在最終生成打包檔案的時候會對 return tmp + A 中的 A 進行替換。

walk示意圖

parse總結

整個 parse 的過程關於依賴的部分,我們總結一下:

  1. 將 source 轉為 AST(如果 source 是字串型別)
  2. 遍歷 AST,遇到 import 語句就增加相關依賴,程式碼中出現 A(import 匯入的變數) 的地方也增加相關的依賴。 ('use strict'的依賴和我們 module 生成的主流程無關,這裡暫時忽略)

所有的依賴都被儲存在 module.dependencies 中,一共有下面4個

HarmonyCompatibilityDependency
HarmonyInitDependency
ConstDependency
HarmonyImportSideEffectDependency
HarmonyImportSpecifierDependency
複製程式碼

到此 build 階段就結束了,回到 module.build 的回撥函式。接下來就是對依賴的處理

依賴處理階段

首先回到的是 module.build 回撥中,原始碼位於 Compilation.js 的 buildModule 中。對 dependencies 按照程式碼在檔案中出現的先後順序排序,然後執行 callback,繼續返回,回到 buildModule 方法的回撥中,呼叫 afterBuild。

const afterBuild = () => {
	if (currentProfile) {
		const afterBuilding = Date.now();
		currentProfile.building = afterBuilding - afterFactory;
	}
	
	// 如果有依賴,則進入 processModuleDependencies
	if (addModuleResult.dependencies) {
		this.processModuleDependencies(module, err => {
			if (err) return callback(err);
			callback(null, module);
		});
	} else {
		return callback(null, module);
	}
};
複製程式碼

這時我們有4個依賴,所以會進入 processModuleDependencies。

processModuleDependencies(module, callback) {
	const dependencies = new Map();
	
	// 整理 dependency
	const addDependency = dep => {
		const resourceIdent = dep.getResourceIdentifier();
		// 過濾掉沒有 ident 的,例如 constDependency 這些只用在最後打包檔案生成的依賴
		if (resourceIdent) {
		   // dependencyFactories 中記錄了各個 dependency 對應的 ModuleFactory。
		   // 還記得前一篇文章中介紹的處理入口的 xxxEntryPlugin 嗎?
		   // 在 compilation 事的回撥中會執行 `compilation.dependencyFactories.set` 方法。
		   // 類似的,ImportPlugin,ConstPlugin 等等,也會在 compilation 事件回撥中執行 set 操作,
		   // 將 dependency 與用來處理這個 dependency 的 moduleFactory 對應起來。
			const factory = this.dependencyFactories.get(dep.constructor);
			if (factory === undefined)
				throw new Error(
					`No module factory available for dependency type: ${
						dep.constructor.name
					}`
				);
			let innerMap = dependencies.get(factory);
			if (innerMap === undefined)
				dependencies.set(factory, (innerMap = new Map()));
			let list = innerMap.get(resourceIdent);
			if (list === undefined) innerMap.set(resourceIdent, (list = []));
			list.push(dep);
		}
	};
	
	const addDependenciesBlock = block => {
		if (block.dependencies) {
			iterationOfArrayCallback(block.dependencies, addDependency);
		}
		if (block.blocks) {
			iterationOfArrayCallback(block.blocks, addDependenciesBlock);
		}
		if (block.variables) {
			iterationBlockVariable(block.variables, addDependency);
		}
	};

	try {
		addDependenciesBlock(module);
	} catch (e) {
		callback(e);
	}

	const sortedDependencies = [];
	// 將上面的結果轉為陣列形式
	for (const pair1 of dependencies) {
		for (const pair2 of pair1[1]) {
			sortedDependencies.push({
				factory: pair1[0],
				dependencies: pair2[1]
			});
		}
	}
	
	this.addModuleDependencies(/*引數*/);
}
複製程式碼

block, variable 哪裡來的?

build 階段得到的 dependency 在這一步都會進入 addDependency 邏輯。我們 demo 中得到的全部都是 dependency,但是除此之外還有 block 和 variable 兩種型別。

block 依賴

當我們使用 webpack 的懶載入時 import('xx.js').then() 的寫法,在 parse 階段,解析到這一句時會執行

//...省略其他邏輯
else if (expression.callee.type === "Import") {
	result = this.hooks.importCall.call(expression);
	//...
}
//...
複製程式碼

這時會進入到 ImportParserPlugin 中,這個外掛中預設是 lazy 模式,即懶載入。在該模式下,會生成一個 ImportDependenciesBlock 型別的依賴,並加入到 module.block 中。

// ImportParserPlugin
const depBlock = new ImportDependenciesBlock(
	param.string,
	expr.range,
	Object.assign(groupOptions, {
		name: chunkName
	}),
	parser.state.module,
	expr.loc,
	parser.state.module
);
// parser.state.current 為當前處理的 module 
parser.state.current.addBlock(depBlock);
複製程式碼

ImportDependenciesBlock 是一個單獨的 chunk ,它自己也會有 dependency, block, variable 型別的依賴。

variables 依賴

如果我們使用到了 webpack 內建的模組變數 __resourceQuery ,例如下面的程式碼

// main.js
require('./a.js?test')

// a.js
const a = __resourceQuery
console.log(a)
複製程式碼

a.js 的模組中 module.variables 中就會存在一個 __resourceQuery 。variables 依賴用來存放 webpack 內全域性變數(測試的時候暫時只發現 __resourceQuery 會存入 variables 中),一般情況下也很少用到(在最新的 webpack5 處理模組依賴中關於 variables 的部分已經被去掉了)。

回到我們的 demo 中,前面我們得到的 4 個 dependency 中,有一些是純粹用作“佔位符”(HarmonyCompatibilityDependency,HarmonyInitDependency,ConstDependency),addDependency 中第一步dep.getResourceIdentifier(); 邏輯則會將這些依賴都過濾掉,然後再將剩下的 dependency 按照所對應的 moduleFactory 和 dependency 的 ident 歸類,最終得到下面的結構:

dependencies = {
  NormalModuleFactory: {
    "module./a.js": [
       HarmonyImportSideEffectDependency,
       HarmonyImportSpecifierDependency
    ]
  }
}
複製程式碼

之後再轉化為陣列形式

sortedDependencies = [
  {
    factory: NormalModuleFactory,
    dependencies: [
      HarmonyImportSideEffectDependency,
      HarmonyImportSpecifierDependency
    ]
  }
]
複製程式碼

然後在 addModuleDependencies 方法中會對 sortedDependencies 陣列中的每一項執行相同的處理,將其加入到編譯鏈條中。細看一下 addModuleDependencies 中處理依賴的程式碼

// addModuleDependencies
addModuleDependencies(
  module,
  dependencies,
  bail,
  cacheGroup,
  recursive,
  callback
) {
  //...
  asyncLib.forEach(
    dependencies,
    (item, callback) => {
      const dependencies = item.dependencies;
      //...
      semaphore.acquire(() => {
        const factory = item.factory;
        // create 階段
        factory.create(
          {/*引數*/},
          (err, dependentModule) => {
            let afterFactory;
            const isOptional = () => {
              return dependencies.every(d => d.optional);
            };
            //...
            // addModule 階段
            const iterationDependencies = depend => {
              for (let index = 0; index < depend.length; index++) {
                const dep = depend[index];
                dep.module = dependentModule;
                dependentModule.addReason(module, dep);
              }
            };
            const addModuleResult = this.addModule(
              dependentModule,
              cacheGroup
            );
            dependentModule = addModuleResult.module;
            // 將 module 資訊寫入依賴中
            iterationDependencies(dependencies);

            // build 階段
            const afterBuild = () => {
              //...
              // build 階段結束後有依賴的話繼續處理依賴
              if (recursive && addModuleResult.dependencies) {
                this.processModuleDependencies(dependentModule, callback);
              } else {
                return callback();
              }
            };
            //...
            if (addModuleResult.build) {
              this.buildModule(/*引數*/);
            } else {
              //...
            }
          }
        );
      });
    },
    err => {
      //...
    }
  );
}
複製程式碼

上面程式碼可以看到,對於所有的依賴再次經過 create->build->add->processDep。如此遞迴下去,最終我們所有的檔案就都轉化為了 module,並且會得到一個 module 和 dependencies 的關係結構

_preparedEntrypoints:
  \
    module: demo.js module
			  |\
			  |  HarmonyImportSideEffectDependency
			  |    module: a.js module
			   \
			     HarmonyImportSpecifierDependency
			       module: a.ja module
複製程式碼

這個結構會交給後續的 chunck 和 生成打包檔案程式碼使用。module 生成的過程結束之後,最終會回到 Compiler.js 中的 compile 方法的 make 事件回撥中:

compile(callback) {
	const params = this.newCompilationParams();
	this.hooks.beforeCompile.callAsync(params, err => {
		//...
		this.hooks.make.callAsync(compilation, err => {
		   // 回到這個回撥中
			if (err) return callback(err);

			compilation.finish();

			compilation.seal(err => {
				if (err) return callback(err);

				this.hooks.afterCompile.callAsync(compilation, err => {
					if (err) return callback(err);

					return callback(null, compilation);
				});
			});
		});
	});
複製程式碼

回撥的 seal 方法中,將運用這些 module 以及 module 的 dependencies 資訊整合出最終的 chunck(具體過程,我們會在下一篇文章《webpack 系列之chunk生成》中介紹)。

總結

到此,module 生成的過程就結束了,我們以一張流程圖來整體總結一下 module 生成的過程:

module總結圖

相關文章