webpack系列之五module生成2

做者:崔靜javascript

上一篇 module生成1中咱們已經分析了 webpack 是如何根據 entry 配置找到對應的文件的,接下來就是將文件轉爲 module 了。這個長長的過程,能夠分紅下面幾個階段java

  1. create: 準備數據,生成 module 實例。
  2. add: 信息保存到 Compilation 實例上。
  3. build: 分析文件內容。
  4. processDep: 處理3步驟中解析獲得的依賴,添加到編譯鏈條中。

後面會以一個簡單的 js 文件爲例,看整個主流程webpack

// 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 方法,代碼主邏輯以下:git

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 內部邏輯:github

  • 觸發 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。web

addModule

獲得 demo module 以後,須要將其保存到全局的 Compilation.modules 數組中和 _modules 對象中。express

這個過程當中還會爲 demo module 添加 reason ,即哪一個 module 中依賴了 demo module。因爲是 demo.js 是入口文件,因此這個 reason 天然就是 SingleEntryDependency。 而且對於入口文件來講,還會被添加到 Compilation.entries 中。json

// 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 方法主邏輯以下:async

// 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總結圖
相關文章
相關標籤/搜索