webpack系列之七-附dependencyTemplates依賴模板

webpack 對於不一樣依賴模塊的模板處理都有單獨的依賴模塊類型文件來進行處理。例如,在你寫的源代碼當中,使用的是ES Module,那麼最終會由 HarmonyModulesPlugin 裏面使用的依賴進行處理,再例如你寫的源碼中模塊使用的是符合 CommonJS Module 規範,那麼最終會有 CommonJsPlugin 裏面使用的依賴進行處理。除此外,webpack 還對於其餘類型的模塊依賴語法也作了處理:javascript

  • AMD -> AMDPlugin
  • System -> SystemPlugin
  • Require.ensure -> RequireEnsurePlugin
  • Import (分包異步加載 module) -> ImportPlugin
  • ...
// WebpackOptionsApply.js

const LoaderPlugin = require("./dependencies/LoaderPlugin");
const CommonJsPlugin = require("./dependencies/CommonJsPlugin");
const HarmonyModulesPlugin = require("./dependencies/HarmonyModulesPlugin");
const SystemPlugin = require("./dependencies/SystemPlugin");
const ImportPlugin = require("./dependencies/ImportPlugin");
const AMDPlugin = require("./dependencies/AMDPlugin");
const RequireContextPlugin = require("./dependencies/RequireContextPlugin");
const RequireEnsurePlugin = require("./dependencies/RequireEnsurePlugin");
const RequireIncludePlugin = require("./dependencies/RequireIncludePlugin");

class WebpackOptionsApply extends OptionsApply {
  constructor() {
    super()
  }

  process(options, compiler) {
    ...
    new HarmonyModulesPlugin(options.module).apply(compiler);
		new AMDPlugin(options.module, options.amd || {}).apply(compiler);
		new CommonJsPlugin(options.module).apply(compiler);
		new LoaderPlugin().apply(compiler);

    new RequireIncludePlugin().apply(compiler);
		new RequireEnsurePlugin().apply(compiler);
		new RequireContextPlugin(
			options.resolve.modules,
			options.resolve.extensions,
			options.resolve.mainFiles
		).apply(compiler);
		new ImportPlugin(options.module).apply(compiler);
		new SystemPlugin(options.module).apply(compiler);
    ...
  }
}
複製代碼

模塊依賴語法的處理對於 webpack 生成最終的文件內容很是的重要。這些針對不一樣依賴加載語法的處理插件在 webpack 初始化建立 compiler 的時候就完成了加載及初始化過程。這裏咱們能夠來看下模塊遵循 ES Module 所使用的相關的依賴依賴模板的處理是如何進行的,即 HarmonyModulesPlugin 這個插件內部主要完成的工做。java

// part 1: 引入的主要是 ES Module 當中使用的不一樣語法的依賴類型
const HarmonyCompatibilityDependency = require("./HarmonyCompatibilityDependency");
const HarmonyInitDependency = require("./HarmonyInitDependency");
const HarmonyImportSpecifierDependency = require("./HarmonyImportSpecifierDependency");
const HarmonyImportSideEffectDependency = require("./HarmonyImportSideEffectDependency");
const HarmonyExportHeaderDependency = require("./HarmonyExportHeaderDependency");
const HarmonyExportExpressionDependency = require("./HarmonyExportExpressionDependency");
const HarmonyExportSpecifierDependency = require("./HarmonyExportSpecifierDependency");
const HarmonyExportImportedSpecifierDependency = require("./HarmonyExportImportedSpecifierDependency");
const HarmonyAcceptDependency = require("./HarmonyAcceptDependency");
const HarmonyAcceptImportDependency = require("./HarmonyAcceptImportDependency");

const NullFactory = require("../NullFactory");

// part 2: 引入的主要是 ES Module 使用的不一樣的語法,在編譯過程當中須要掛載的 hooks,方便作依賴收集
const HarmonyDetectionParserPlugin = require("./HarmonyDetectionParserPlugin");
const HarmonyImportDependencyParserPlugin = require("./HarmonyImportDependencyParserPlugin");
const HarmonyExportDependencyParserPlugin = require("./HarmonyExportDependencyParserPlugin");
const HarmonyTopLevelThisParserPlugin = require("./HarmonyTopLevelThisParserPlugin");

class HarmonyModulesPlugin {
	constructor(options) {
		this.options = options;
	}

	apply(compiler) {
		compiler.hooks.compilation.tap(
			"HarmonyModulesPlugin",
			(compilation, { normalModuleFactory }) => {
				compilation.dependencyFactories.set(
					HarmonyCompatibilityDependency,
					new NullFactory()
				);
				// 設置對應的依賴渲染所須要的模板
				compilation.dependencyTemplates.set(
					HarmonyCompatibilityDependency,
					new HarmonyCompatibilityDependency.Template()
				);

				compilation.dependencyFactories.set(
					HarmonyInitDependency,
					new NullFactory()
				);
				compilation.dependencyTemplates.set(
					HarmonyInitDependency,
					new HarmonyInitDependency.Template()
				);

				compilation.dependencyFactories.set(
					HarmonyImportSideEffectDependency,
					normalModuleFactory
				);
				compilation.dependencyTemplates.set(
					HarmonyImportSideEffectDependency,
					new HarmonyImportSideEffectDependency.Template()
				);

				compilation.dependencyFactories.set(
					HarmonyImportSpecifierDependency,
					normalModuleFactory
				);
				compilation.dependencyTemplates.set(
					HarmonyImportSpecifierDependency,
					new HarmonyImportSpecifierDependency.Template()
				);

				compilation.dependencyFactories.set(
					HarmonyExportHeaderDependency,
					new NullFactory()
				);
				compilation.dependencyTemplates.set(
					HarmonyExportHeaderDependency,
					new HarmonyExportHeaderDependency.Template()
				);

				compilation.dependencyFactories.set(
					HarmonyExportExpressionDependency,
					new NullFactory()
				);
				compilation.dependencyTemplates.set(
					HarmonyExportExpressionDependency,
					new HarmonyExportExpressionDependency.Template()
				);

				compilation.dependencyFactories.set(
					HarmonyExportSpecifierDependency,
					new NullFactory()
				);
				compilation.dependencyTemplates.set(
					HarmonyExportSpecifierDependency,
					new HarmonyExportSpecifierDependency.Template()
				);

				compilation.dependencyFactories.set(
					HarmonyExportImportedSpecifierDependency,
					normalModuleFactory
				);
				compilation.dependencyTemplates.set(
					HarmonyExportImportedSpecifierDependency,
					new HarmonyExportImportedSpecifierDependency.Template()
				);

				compilation.dependencyFactories.set(
					HarmonyAcceptDependency,
					new NullFactory()
				);
				compilation.dependencyTemplates.set(
					HarmonyAcceptDependency,
					new HarmonyAcceptDependency.Template()
				);

				compilation.dependencyFactories.set(
					HarmonyAcceptImportDependency,
					normalModuleFactory
				);
				compilation.dependencyTemplates.set(
					HarmonyAcceptImportDependency,
					new HarmonyAcceptImportDependency.Template()
				);

				const handler = (parser, parserOptions) => {
					if (parserOptions.harmony !== undefined && !parserOptions.harmony)
						return;

					new HarmonyDetectionParserPlugin().apply(parser);
					new HarmonyImportDependencyParserPlugin(this.options).apply(parser);
					new HarmonyExportDependencyParserPlugin(this.options).apply(parser);
					new HarmonyTopLevelThisParserPlugin().apply(parser);
				};

				normalModuleFactory.hooks.parser
					.for("javascript/auto")
					.tap("HarmonyModulesPlugin", handler);
				normalModuleFactory.hooks.parser
					.for("javascript/esm")
					.tap("HarmonyModulesPlugin", handler);
			}
		);
	}
}
module.exports = HarmonyModulesPlugin;
複製代碼

在 HarmonyModulesPlugin 引入的文件當中主要是分爲了2部分:webpack

  • ES Module 當中使用的不一樣語法的依賴類型
  • ES Module 使用的不一樣的依賴語法,在代碼經過 parser 編譯過程當中須要掛載的 hooks(這些 hooks 都是經過相關 plugin 進行註冊),方便作依賴收集

當 webpack 建立新的 compilation 對象後,便執行compiler.hooks.compilation註冊的鉤子內部的方法。其中主要完成了如下幾項工做:web

1.設置不一樣依賴類型的 moduleFactory,例如設置HarmonyImportSpecifierDependency依賴類型的 moduleFactory 爲normalModuleFactoryexpress

2.設置不一樣依賴類型的 dependencyTemplate,例如設置HarmonyImportSpecifierDependency依賴類型的模板爲new HarmonyImportSpecifierDependency.Template()實例;數組

3.註冊 normalModuleFactory.hooks.parser 鉤子函數。每當新建一個 normalModule 時這個鉤子函數都會被執行,即觸發 handler 函數的執行。handler 函數內部去初始化各類 plugin,註冊相關的 hooks。app

咱們首先來看下 handler 函數內部初始化的幾個 plugin 裏面註冊的和 parser 編譯相關的插件。異步

HarmonyDetectionParserPlugin

// HarmonyDetectionParserPlugin.js
const HarmonyCompatibilityDependency = require("./HarmonyCompatibilityDependency");
const HarmonyInitDependency = require("./HarmonyInitDependency");

module.exports = class HarmonyDetectionParserPlugin {
	apply(parser) {
		parser.hooks.program.tap("HarmonyDetectionParserPlugin", ast => {
			const isStrictHarmony = parser.state.module.type === "javascript/esm";
			const isHarmony =
				isStrictHarmony ||
				ast.body.some(statement => {
					return /^(Import|Export).*Declaration$/.test(statement.type);
				});
			if (isHarmony) {
				// 獲取當前的正在編譯的 module
				const module = parser.state.module;
				const compatDep = new HarmonyCompatibilityDependency(module);
				compatDep.loc = {
					start: {
						line: -1,
						column: 0
					},
					end: {
						line: -1,
						column: 0
					},
					index: -3
				};
				// 給這個 module 添加一個 compatDep 依賴
				module.addDependency(compatDep);
				const initDep = new HarmonyInitDependency(module);
				initDep.loc = {
					start: {
						line: -1,
						column: 0
					},
					end: {
						line: -1,
						column: 0
					},
					index: -2
				};
				// 給這個 module 添加一個 initDep 依賴
				module.addDependency(initDep);
				parser.state.harmonyParserScope = parser.state.harmonyParserScope || {};
				parser.scope.isStrict = true;
				// 初始化這個 module 最終被編譯生成的 meta 元信息,
				module.buildMeta.exportsType = "namespace";
				module.buildInfo.strict = true;
				module.buildInfo.exportsArgument = "__webpack_exports__";
				if (isStrictHarmony) {
					module.buildMeta.strictHarmonyModule = true;
					module.buildInfo.moduleArgument = "__webpack_module__";
				}
			}
		});

    ...
	}
};
複製代碼

在每一個 module 開始編譯的時候便會觸發這個 plugin 上註冊的 hooks。經過 AST 的節點類型來判斷這個 module 是不是 ES Module,若是是的話,首先會實例化一個HarmonyCompatibilityDependency依賴的實例,並記錄依賴須要替換的位置,而後將這個實例加入到 module 的依賴中,接下來實例化一個HarmonyInitDependency依賴的實例,並記錄依賴須要替換的位置,而後將實例加入到 module 的依賴當中。而後會設定當前被 parser 處理的 module 最終被渲染時的一些構建信息,例如exportsArgument可能會使用__webpack_exports__,即這個模塊輸出掛載變量使用__webpack_exports__ide

其中HarmonyCompatibilityDependency依賴的 Template 主要是:函數

HarmonyCompatibilityDependency.Template = class HarmonyExportDependencyTemplate {
	apply(dep, source, runtime) {
		const usedExports = dep.originModule.usedExports;
		if (usedExports !== false && !Array.isArray(usedExports)) {
			// 定義 module 的 export 類型
			const content = runtime.defineEsModuleFlagStatement({
				exportsArgument: dep.originModule.exportsArgument
			});
			source.insert(-10, content);
		}
	}
}
複製代碼

調用 RuntimeTemplate 實例上提供的 defineEsModuleFlagStatement 方法在當前模塊最終生成的代碼內插入代碼:

__webpack_require__.r(__webpack_exports__) // 用以在 __webpack_exports__ 上定義一個 __esModule 屬性,用以標識當前 module 是一個 ES Module
複製代碼

而在HarmonyInitDependency依賴的 Template 中主要完成的工做是:

HarmonyInitDependency.Template = class HarmonyInitDependencyTemplate {
	apply(dep, source, runtime, dependencyTemplates) {
		const module = dep.originModule;
		const list = [];
    // 遍歷這個依賴的所屬的 module 的全部依賴
		for (const dependency of module.dependencies) {
      // 獲取不一樣依賴所使用的 template
			const template = dependencyTemplates.get(dependency.constructor);
      // 部分 template 並非在 generator 調用 generate 方法當即執行相關模板依賴的替換工做的
      // 而是將相關的操做置於 harmonyInit 函數當中,在這個會被加入到一個數組當中
			if (
				template &&
				typeof template.harmonyInit === "function" &&
				typeof template.getHarmonyInitOrder === "function"
			) {
				const order = template.getHarmonyInitOrder(dependency);
				if (!isNaN(order)) {
					list.push({
						order,
						listOrder: list.length,
						dependency,
						template
					});
				}
			}
		}

    // 對模板依賴數組進行排序
		list.sort((a, b) => {
			const x = a.order - b.order;
			if (x) return x;
			return a.listOrder - b.listOrder;
		});

    // 依次執行模板依賴上的 harmonyInit 方法,這個時候開始相關模板的替換工做
		for (const item of list) {
			item.template.harmonyInit(
				item.dependency,
				source,
				runtime,
				dependencyTemplates
			);
		}
	}
}
複製代碼

HarmonyImportDependencyParserPlugin

接下來咱們再來看 HarmonyModulesPlugin 插件裏面初始化的第二個插件HarmonyImportDependencyParserPlugin,這個插件主要完成的工做是和 ES Module 當中使用 import 語法相關:

module.exports = class HarmonyImportDependencyParserPlugin {
  constructor() {
    ...
  }

  apply(parser) {
    ...
    parser.hooks.import.tap('HarmonyImportDependencyParserPlugin', (statement, source) => {
      ...
      const sideEffectDep = new HarmonyImportSideEffectDependency({ ... })

      parser.state.module.addDependency(sideEffectDep);
      ...
    })

    parser.hooks.importSpecifier.tap('HarmonyImportDependencyParserPlugin', (statement, source, id, name) => {
      ...
      // 設置引入模塊名的映射關係
      parser.state.harmonySpecifier.set(name, {
        source,
        id,
        sourceOrder: parser.state.lastHarmonyImportOrder
      });
      ...
    })

    parser.hooks.expression
      .for('imported var')
      .tap('HarmonyImportDependencyParserPlugin', expr => {
        ...
				const dep = new HarmonyImportSpecifierDependency({ ... });

        parser.state.module.addDependency(dep);
        ...
      })

    parser.hooks.call
      .for('imported var')
      .tap('HarmonyImportDependencyParserPlugin', expr => {
        ...
        const dep = new HarmonyImportSpecifierDependency({ ... })

        parser.state.module.addDependency(dep);
        ...
      })
  }
}
複製代碼

在這個插件裏面主要是註冊了在模塊經過 parser 編譯的過程當中,遇到不一樣 tokens 觸發的 hooks。例如hooks.importSpecifier主要是用於你經過import語法加載其餘模塊時所申明的變量名,會經過一個 map 結構記錄這個變量名。當你在源代碼中使用了這個變量名,例如做爲一個函數去調用(對應觸發hooks.call鉤子),或者是做爲一個表達式去訪問(對應觸發hooks.express鉤子),那麼它們都會新建一個HarmonyImportSpecifierDependency依賴的實例,並進入到當前被編譯的 module 當中。

這個HarmonyImportSpecifierDependency模板依賴主要完成的工做就是:

HarmonyImportSpecifierDependency.Template = class HarmonyImportSpecifierDependencyTemplate extends HarmonyImportDependency.Template {
	apply(dep, source, runtime) {
		super.apply(dep, source, runtime);
		const content = this.getContent(dep, runtime);
		source.replace(dep.range[0], dep.range[1] - 1, content);
	}

	getContent(dep, runtime) {
		const exportExpr = runtime.exportFromImport({
			module: dep._module,
			request: dep.request,
			exportName: dep._id,
			originModule: dep.originModule,
			asiSafe: dep.shorthand,
			isCall: dep.call,
			callContext: !dep.directImport,
			importVar: dep.getImportVar()
		});
		return dep.shorthand ? `${dep.name}: ${exportExpr}` : exportExpr;
	}
};
複製代碼

將源碼中引入的其餘模塊的依賴變量名進行字符串的替換,具體能夠查閱RuntimeTemplate.exportFromImport方法。

咱們來看個例子:

// 在 parse 編譯過程當中,觸發 hooks.importSpecifier 鉤子,經過 map 記錄對應變量名
import { add } from './add.js'

// 觸發 hooks.call 鉤子,給 module 加入 HarmonyImportSpecifierDependency 依賴
add(1, 2)

--- 

// 最終生成的代碼爲:
/* harmony import */ var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

Object(_b__WEBPACK_IMPORTED_MODULE_0__["add"])(1, 2);
複製代碼

HarmonyExportDependencyParserPlugin

這個插件主要完成的是和 ES Module 當中使用 export 語法相關的工做:

module.exports = class HarmonyExportDependencyParserPlugin {
	constructor(moduleOptions) {
		this.strictExportPresence = moduleOptions.strictExportPresence;
	}

	apply(parser) {
		parser.hooks.export.tap(
			"HarmonyExportDependencyParserPlugin",
			statement => {
        ...
				const dep = new HarmonyExportHeaderDependency(...);
				...
				parser.state.current.addDependency(dep);
				return true;
			}
		);
		parser.hooks.exportImport.tap(
			"HarmonyExportDependencyParserPlugin",
			(statement, source) => {
        ...
				const sideEffectDep = new HarmonyImportSideEffectDependency(...);
				...
				parser.state.current.addDependency(sideEffectDep);
				return true;
			}
		);
		parser.hooks.exportExpression.tap(
			"HarmonyExportDependencyParserPlugin",
			(statement, expr) => {
        ...
				const dep = new HarmonyExportExpressionDependency(...);
        ...
				parser.state.current.addDependency(dep);
				return true;
			}
		);
		parser.hooks.exportDeclaration.tap(
			"HarmonyExportDependencyParserPlugin",
			statement => {}
		);
		parser.hooks.exportSpecifier.tap(
			"HarmonyExportDependencyParserPlugin",
			(statement, id, name, idx) => {
        ...
				if (rename === "imported var") {
					const settings = parser.state.harmonySpecifier.get(id);
					dep = new HarmonyExportImportedSpecifierDependency(...);
				} else {
					dep = new HarmonyExportSpecifierDependency(...);
				}
				parser.state.current.addDependency(dep);
				return true;
			}
		);
		parser.hooks.exportImportSpecifier.tap(
			"HarmonyExportDependencyParserPlugin",
			(statement, source, id, name, idx) => {
				...
				const dep = new HarmonyExportImportedSpecifierDependency(...);
				...
				parser.state.current.addDependency(dep);
				return true;
			}
		);
	}
};
複製代碼

parse 在編譯源碼過程當中,根據你使用的不一樣的 ES Module export 語法去觸發不經過的 hooks,而後給當前編譯的 module 加入對應的依賴 module。仍是經過2個例子來看:

// export 一個 add 標識符,在 parse 環節會觸發 hooks.exportSpecifier 鉤子,會在當前 module 加入一個 HarmonyExportSpecifierDependency 依賴
export function add() {} 

---

// 最終在輸出文件當中輸出的內容爲
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "add", function() { return add; });

function add() {}
複製代碼
// export 從 add.js 模塊加載的 add 標識符,在 parse 環節會觸發 hooks.exportImportSpecifier 鉤子,會在當前 module 加入一個 HarmonyExportImportedSpecifierDependency 依賴
export { add } from './add'

---

// 最終在輸出文件當中輸出的內容爲
/* harmony import */ var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "add", function() { return _add__WEBPACK_IMPORTED_MODULE_0__["add"]; });
複製代碼

具體替換的工做能夠查閱HarmonyExportSpecifierDependency.TemplateHarmonyExportImportedSpecifierDependency.Template提供的依賴模板函數。

相關文章
相關標籤/搜索