ModuleFederationPlugin 源码解析(二)

ModuleFederationPlugin 源码解析(二)

前言

上一篇文章中,我介绍了 MF 的一些基本知识以及一些 MF 的应用场景,并且分析了主要的插件入口源码,因为入口插件源码比较简单,所以我详细介绍了插件的一些配置,从而得知 Webpack 官网上都没有介绍的高级配置,例如 libraryexposes配置的 import配置等 。

本篇文章,我们将详细解析 ContainerPlugin的源码。为了帮助大家更好理解本篇文章的内容,我还会引入一些 Webpack 大致的构建流程和源码中的一些数据结构的知识。

下面我们开始进入正文。

ContainerPlugin 源码解析

基本介绍

开始之前,基于 MF 构建出来的前端应用,我们简单介绍几个名词:

  • Remote,如果一个应用只导出模块给其它应用消费,我们称这样的应用为 remote
  • Host,如果一个应用只消费其它应用导出的模块,我们称这样的应用为 host
  • Bidirectional-hosts,如果一个应用既消费其它应用导出的模块,也导出模块给其它应用消费,我们称这样的应用为 Bidirectional-hosts

从上一篇的文章内容中,我们得知其实 ModuleFedrationPlugin 插件是由三个插件组成,而且它会根据不同的配置,来决定是否初始化相关的插件,而 ContainerPlugin 则是必须在配置了 exposes 选项的时候才会初始化。这也就是说,如果一个应用构建的时候,在其 Weback 配置中如果只有exposes配置,那么我们构建出来的应用即为 remote 应用。

实际上,在一个大型的微前端架构中,如果我们设计的是一个中心化的微前端架构,那么需要重点考虑的问题有:

  • 构建速度,当基座的代码量不断膨胀,组件库、三方包等会慢慢拖垮应用的构建速度;
  • 页面性能,按需加载和 code spliting 是必不可少的手段;

那么基于 MF 构建出来的 remote 的应用很容易满足以上两点,或者说它天然就支持这两点,所以这就是基于 ContainerPlugin 构建出来的 remote 应用的比较核心的作用。

下面回到源码。

插件源码

ContainerPlugin 的源码在 Webpack 源目录的 lib/container/ContainerPlugin.js中:

class ContainerPlugin {
	/**
	 * @param {ContainerPluginOptions} options options
	 */
	constructor(options) {
		validate(options);

		this._options = {
			name: options.name,
			/* 共享作用域的名称 */
			shareScope: options.shareScope || "default",
			/* 模块构建产物的类型,类型为 LibraryType */
			library: options.library || {
				type: "var",
				name: options.name
			},
			// 设置了该选项,会单独为 mf 相关的模块创建一个指定名字的 runtime
			runtime: options.runtime,
			filename: options.filename || undefined,
			// container 导出的模块
			exposes: parseOptions(
				options.exposes,
				item => ({
					import: Array.isArray(item) ? item : [item],
					name: undefined
				}),
				item => ({
					import: Array.isArray(item.import) ? item.import : [item.import],
					name: item.name || undefined
				})
			)
		};
	}

	/**
	 * Apply the plugin
	 * @param {Compiler} compiler the compiler instance
	 * @returns {void}
	 */
	apply(compiler) {
		const { name, exposes, shareScope, filename, library, runtime } =
			this._options;

		// 	enabledLibraryTypes 专门存储 entry 需要输出的 library 类型,
    //  然后被 EnableLibraryPlugin 插件消费,
		//  在构建生成最终产物的时候决定 bundle 的 library 的类型
		compiler.options.output.enabledLibraryTypes.push(library.type);

		// 监听 make hook,这个钩子在完成本次构建过程完成 compilation 创建后触发,
    // 是一个 AsyncParallelHook 类型的 hook
		compiler.hooks.make.tapAsync(PLUGIN_NAME, (compilation, callback) => {
			// 根据 expose 配置创建 dep
			const dep = new ContainerEntryDependency(name, exposes, shareScope);
			dep.loc = { name };

			// 所有的 entry 都会调用 compilation.addEntry 添加到构建流程中
			compilation.addEntry(
				compilation.options.context,
				dep,
				{
					name,
					filename,
					runtime,
					library
				},
				error => {
					if (error) return callback(error);
					callback();
				}
			);
		});

		compiler.hooks.thisCompilation.tap(
			PLUGIN_NAME,
			(compilation, { normalModuleFactory }) => {
				// 对于特殊的 dependency 一般都有自己的 entry factory,
        // MF 下的 dep 对应的是 ContainerEntryModuleFactory
				compilation.dependencyFactories.set(
					ContainerEntryDependency,
					new ContainerEntryModuleFactory()
				);

				// 而 expose 模块的 dependency 则使用正常的 normalModuleFactory
				compilation.dependencyFactories.set(
					ContainerExposedDependency,
					normalModuleFactory
				);
			}
		);
	}
}
复制代码

我们发现,其核心的源码大概只有 80 行左右,然后有读者可能会感叹非常神奇,80行的代码能实现这么复杂的功能,这得益于 Webpack 高可扩展性架构以及底层完美的数据结构设计

初始化 options的部分非常简单,我们主要关注apply方法里面的代码,有些简单的逻辑,我也在上面的代码中用注释解释了,所以下面详细解析我觉得比较核心的部分实现。

首选,该插件监听的是一个叫makehook,这个hook触发的时机是在 compilation对象创建之后。然后接着在回调里面根据传入的options选项新建了ContainerEntryDependency实例 dep,最后调用compilation.addEntry并传入dep。这几行代码是整个ContainerPlugin最为核心的,特别是

addEntry的调用,其核心的作用实际上就是 exposes配置的各个模块当成一个新的entry加入到 Webpack 构建流程中。这里会涉及到 ContainerEntryDependency的源码,我们这里先不着急看,留在下一小节介绍。

我们继续看下面的源码,接着插件监听了thisCompilationhook,回调里面的逻辑做了两件事,那就是将 ContainerEntryDependencydependencyFactory设置成

ContainerEntryModuleFactory的实例,ContainerExposedDependency

dependencyFactory设置成 normalModuleFactory。要了解这里的 xxxModuleFactory的作用,我们首先看看 lib/container/ContainerEntryModuleFactory.js 的源码:

module.exports = class ContainerEntryModuleFactory extends ModuleFactory {
	/**
	 * @param {ModuleFactoryCreateData} data data object
	 * @param {function(Error=, ModuleFactoryResult=): void} callback callback
	 * @returns {void}
	 */
	create({ dependencies: [dependency] }, callback) {
		const dep = /** @type {ContainerEntryDependency} */ (dependency);
		callback(null, {
			module: new ContainerEntryModule(dep.name, dep.exposes, dep.shareScope)
		});
	}
};
复制代码

代码非常简单,该类继承自 ModuleFactory,并实现了 create方法,而这个 create就做一件事,执行传入的 callback,并且第二个参数传入的是一个带有 module属性的对象,然后创建了一个

ContainerEntryModule实例。突然变得有意思起来,从前面代码的 ContainerEntryDependecy到这里的 ContainerEntryModule,它们的源码里面是什么内容?它们之间看起来貌似有着千丝万缕的关系。

为了更好的帮助大家理解这十几行非常精髓的代码,这里插入 Webpack 构建流程和相关的一些数据结构的内容介绍。

Webpack 构建流程和基本数据结构

构建流程

我们知道 Webpack 的构建原理是在初始化阶段从一个entry出发,一般是一个 .js文件,首先将

entry配置转换成 dependency

接着在构建阶段调用 addEntrydependency加入到构建流程,递归去分析dependency的依赖,然后将 dependency转换成 module,最后根据前面的分析生成模块依赖图谱。我们经常说 Webpack 将所有的文件类型,无论是 .js.json.ts.png.vue等都视作模块,准确来说,是在构建阶段所有从入口文件开始收集到的 dependency最终都会转换成 module ,并且构建完整的依赖图谱

最后在生成阶段,根据模块依赖图和spliting code算法将一个模块或者多个模块组合在一起生成

chunk,最后再生成文件,也就是最终的 bundle 产物。

单独讲这个过程,可能有点抽象,我们看一个构建阶段的流程图:

在构建阶段,最开始的流程其实就是从addEntry出发,然后经历 factorizeModuleaddModule

buildrunLoadersparse等核心的方法调用流程后,就会完成构建出一个基于 entry的 模块依赖图谱。在这个过程中,会将js源码编译成 AST,然后通过 AST 去分析模块之间的依赖关系。

为什么需要loader?其实就是因为在构建流程中,Webpack 需要通过 AST 去分析依赖,所以需要将非标准的 js文件转换成能被acorn这样的编译器识别的标准 JS 模块。

了解基本的构建流程后,我们来看看,源码中几个核心的数据结构。

基本数据结构

在聊 Webpack 的时候,我们经常能听到模块和chunk的概念,实际上这些概念也不是火星来的,而是实实在在体现在 Webpack 的源码之中。但是我们平常口头说的 Webpack 将一切文件视为“模块”,其实也不全对,其实一个入口的 js文件,刚开始并不是模块,而是在构建阶段才被转换成源码中真正的module,然后被消费生成 chunk,最后写入到文件中,也就是我们经常说 bundle。用一个大概的数据结构流转图表示如下:

Webpack 初始化阶段就是从entry配置开始分析,将 entry对应的文件以及所有 entry引用的模块(文件)创建为 dep,然后通过 compilation.addEntry方法将这些 denpendency加入到构建流程照片那个。Dependency只是最基础的数据结构,在 Webpack 源码中,存在以下基于Dependency扩展的类:

ModuleDependencyEntryDependencyContainerEntryDependency

Webpack 的构建阶段就是将 entry对应生成的Dependency转换成 Module,构建模块依赖图谱。在 Webpack 源码也有相对应的数据结构,它的源码文件在 lib/Module.js下,Module是 Webpack 构建时处理的最小单位。而 Module也是比较基础的数据结构,真正源码中创建的模块有:NormalModule

RuntimeModuleContainerEntryModule等;

Chunk是 Webpack 构建流程中生成文件之前最后一个数据结构的抽象,它是在最后生成阶段的时候”组装“出来的。很容易理解,实际上在编译阶段生成的所有 Module,在生成阶段就会根据模块之间的关系将一个或者多个Module组装起来生成一个个 Chunk。在 Webpack 默认的策略中,以下场景会默认生成一个

Chunk

  • entry 配置,一个 entry 以及文件所引用的所有 JS 模块组合成一个 Chunk;
  • 动态 import 一个模块会单独生成一个 Chunk。

小结

虽然只是一个插入介绍,但是信息量也是非常庞大,不过有了以上的一些背景知识,我们再回到插件的源码中,我们能很容易理解它内部的运作原理。

下面,我们回到插件源码,继续深入 ContainerEntryDependencyContainerEntryModule源码。

ModuleFactory

在开始深入ContainerEntryDependencyContainerEntryModule源码之前,我们先解答插件源码中在后面 thisCompilation hook中设置 ModuleFactory的逻辑,其实 ModuleFactory从命名我们很容易大概猜到它的作用,加上上一节的背景知识我们知道,在 Webpack 构建流程中 从entry开始,是首先生成 Dependency,然后再到 Module,所以一句话介绍 ModuleFactory就是:它是一个将

Dependency转换成 Module的工厂类。


在 Webpack 构建阶段,执行 lib/Compilation.js中的_factorizeModule方法时,就会调用上一步 compolation.addEntry方法加入到构建流程中的Dependency对应的factory.create方法,大概的代码如下:

/**
	 * @param {FactorizeModuleOptions} options options object
	 * @param {ModuleOrFactoryResultCallback} callback callback
	 * @returns {void}
	 */
	_factorizeModule(
		{
			currentProfile,
			factory,
			dependencies,
			originModule,
			factoryResult,
			contextInfo,
			context
		},
		callback
	) {
		if (currentProfile !== undefined) {
			currentProfile.markFactoryStart();
		}
		factory.create(
			{
				contextInfo: {
					issuer: originModule ? originModule.nameForCondition() : "",
					issuerLayer: originModule ? originModule.layer : null,
					compiler: this.compiler.name,
					...contextInfo
				},
				resolveOptions: originModule ? originModule.resolveOptions : undefined,
				context: context
					? context
					: originModule
					? originModule.context
					: this.compiler.context,
				dependencies: dependencies
			},
			(err, result) => {
				if (result) {
				// 省略一些代码

        // 这里就会将生成的 module 通过 callback 暴露出去  
				callback(null, factoryResult ? result : result.module);
			}
		);
	}
复制代码

看到这里,我们就很容易理解 ContainerPlugin的运行原理了。总结来说,它本质还是在原来的 Webpack构建流程中,引入了新的entry ContainerEntryDependency ContainerEntryModule ,而这些新的数据结构同样是基于基础的 Dependpency Module派生出来的。它只需要在适当的时机,通过调用 addEntry方法,将 exposes模块加入到正常的 Webpack 构建流程中。

当然这里还需要依赖 ContainerEntryDependencyContainerEntryModule的实现,下面我们继续深入到这两个类的源码中。

ContainerEntryDependency 和 ContainerEntryModule 源码介绍

ContainerEntryDependency

先贴下具体的代码:

class ContainerEntryDependency extends Dependency {
	/**
	 * @param {string} name entry name
	 * @param {[string, ExposeOptions][]} exposes list of exposed modules
	 * @param {string} shareScope name of the share scope
	 */
	constructor(name, exposes, shareScope) {
		super();
		this.name = name;
		// container exposes 配置
		this.exposes = exposes;
		/* 共享作用域的名称 */
		this.shareScope = shareScope;
	}

	/**
	 * @returns {string | null} an identifier to merge equal requests
	 */
	getResourceIdentifier() {
		return `container-entry-${this.name}`;
	}

	// 用来标识 mf container dep 的类型名称
	get type() {
		return "container entry";
	}

	get category() {
		return "esm";
	}
}
复制代码

ContainerEntryDependency的实现还是比较简单,其实就是存储了一些外部传入的 options,在必要的时候拿出来消费,其它就是一些 getter 字符串属性,从命名上来看,是为了方便标识 Dependency

为了帮助大家更好理解 Dependency,这里我们也稍微看下基类的Dependency.js的实现,它放在

lib/Dependency.js中:

class Dependency {
    constructor() {
        /** @type {Module} */
	this._parentModule = undefined;
	/** @type {DependenciesBlock} */
	this._parentDependenciesBlock = undefined;
        // 省略一些代码
	this._locSL = 0;
	this._locSC = 0;
	this._locEL = 0;
	this._locEC = 0;
	this._locI = undefined;
	this._locN = undefined;
	this._loc = undefined;
    }
    /**
        * @returns {DependencyLocation} location
    */
    get loc() {
	// 省略一些代码
    }

    set loc(loc) {
	// 省略一些代码
    }

    setLoc(startLine, startColumn, endLine, endColumn) {
        // 省略一些代码
    }
  // 省略一些代码
}

module.exports = Dependency;
复制代码

这里我省略了一些代码,它们不影响我们对主流程的理解,因为 Webpack 源码非常庞大,有时候我们需要忽略一些不影响我们理解流程的代码,我们应该把焦点放在更加关键的流程和核心的方法上。

我们可以看到,Dependency主要存储的时候一些 loc信息,如果对 AST 有过了解的小伙伴应该知道,

loc其实代表的是一些节点在源文件中的位置信息。在 entryModule之间引入Dependency,个人觉得有如下好处:

  • 在真正做 module转换之前,需要有一个数据结构来过渡,进行例如将模块创建过程工厂化
  • 存储一些 entry相关的上下文信息,例如 ContainerEntryDependency 中的 exposes

shareScope等;

看了 ContainerEntryDependency,下面我们继续 ContainerEntryModule的源码。

ContainerEntryModule

ContainerEntryModule的源码在lib/container/ContainerEntryModule.js中,这里介绍下看

Module源码的一个窍门:实际上 Module最核心也是最复杂的两个函数实现是build

codeGeneration,而通过 Module派生出来的例如 NormalModuleContainerEntryModule两者之间最大的区别就是这两个方法的实现。其它的一些属性或者方法,可以在需要关注的时候再细看。

当然除此之外,每个 Module的实例化的时候参数也会有差异化,我们首先看 ContainerEntryModule 实例化时需要传递的参数:

class ContainerEntryModule extends Module {
	constructor(name, exposes, shareScope) {
		// 这里的 javascript/dynamic 代表模块类型
		super("javascript/dynamic", null);
		this._name = name;
		this._exposes = exposes;
		this._shareScope = shareScope;
	}

  // 省略一些代码
}  
复制代码

比较简单的几个参数,实际上基本是在 ContainerPlugin初始化时传入的配置,我们留意下这里的

super方法调用,传入的是一个 javascript/dynamic字符串,每个模块都有一个 type属性,用来存储这个模块的类型,它的值还有 javascript/esm javascript/auto。这里代表的是所有的

exposes模块构建出来的产物都是一个 dynamic的模块,需要通过动态加载的方式引入

接下来,我们看下 build方法的实现:

/**
	 * @param {WebpackOptions} options webpack options
	 * @param {Compilation} compilation the compilation
	 * @param {ResolverWithOptions} resolver the resolver
	 * @param {InputFileSystem} fs the file system
	 * @param {function(WebpackError=): void} callback callback function
	 * @returns {void}
	 */
	build(_options, _compilation, _resolver, _fs, callback) {
		this.buildMeta = {};
		this.buildInfo = {
			strict: true,
			topLevelDeclarations: new Set(["moduleMap", "get", "init"])
		};
		this.buildMeta.exportsType = "namespace";

		this.clearDependenciesAndBlocks();

		// 这里就是根据 exposes 的配置创建 module 的依赖关系的过程,
    // 这里的 block 其实就是 module,对于 MF exposes 配置来说都是异步的 module
		// 这里的 name 就是 exposes 配置中的 key,options 就是处理之后的 
    // value,例如 exposes: { './share', './src/shared.ts' },name 就是 ./share
		for (const [name, options] of this._exposes) {
			// Entry、Dependency 、DependenciesBlock、Module 之间的关系是什么
			/* 
				Entry 就是 webpack 的入口配置,我们知道 webpack 构建过程就是
    		从 entry 出发构建依赖图谱,构建结束后将所有的依赖组合起来,
				输出多个 bundle 对于 MF,expose 配置就是 entry
				
				Dependency 就是一个模块依赖的另一个文件模块的抽象,每个 module 
    		都有一个 dependencies 数组,而一般的 module 通常都是继承自 DependenciesBlock
				所以,可以简单理解 DependenciesBlock = Module
				在 MF 中,所有的 module 都是 AsyncDependencyBlock,因为 
    		expose 出去的模块都是异步模块, 在运行时动态加载
			 */
			const block = new AsyncDependenciesBlock(
				{
					name: options.name
				},
				{ name },
				options.import[options.import.length - 1]
			);
			let idx = 0;
			// 构建 block 依赖的 deps
			for (const request of options.import) {
				const dep = new ContainerExposedDependency(name, request);
				dep.loc = {
					name,
					index: idx++
				};

				block.addDependency(dep);
			}
			// 建立 block 和 block 之间的父子关系,当一个 module 有异步的 
      // AsyncDependencyBlock 时,就会需要维护 blocks
			// 后面用于 code-splitting
			this.addBlock(block);
		}
		// reference https://webpack.js.org/configuration/optimization/#optimizationprovidedexports
		// 添加这个 dep 是为了告诉 webpack 一个模块导出了哪些方法,
    // 能让 webpack 构建的时候为 export * from xxx 生成执行效率更高的代码
		this.addDependency(new StaticExportsDependency(["get", "init"], false));

		callback();
	}
复制代码

每个模块在构建阶段都会执行 build方法,用来收集该模块的依赖,简单的理解就是利用 AST 分析模块源码的 import或者 require语法。

build方法接收5个参数,前面四个参数,为了帮助大家更容易理解,我加了_前缀,在

ContainerEntryModule场景中,它们并没有被使用到。首先 build方法执行的逻辑是赋值了

buildMetabuildInfo,这两个属性是每个模块都具有的属性,用来方便存储一些构建模块和生成代码时需要用到的上下文信息。这里需要留意下 buildInfo中的 topLevelDeclarations属性,这个

Set里面的几个字符串我们在后续的 codeGeneration将会看到。

继续往下走,这里调用了 clearDependenciesAndBlocks方法,我理解这里的作用是每一轮构建都会先清空上一轮的 depnedencyblock的数据,其背后调用的是 DependenciesBlock中的

clearDependenciesAndBlocks方法:

clearDependenciesAndBlocks() {
    this.dependencies.length = 0;
    this.blocks.length = 0;
  }
复制代码

因为模块构建是一次性的,个人理解这里更多的是考虑开发模式下,当代码发生变动的时候,重新走构建的过程。

这里引入了 DependenciesBlock的数据结构,实际上它是一个更加基础的数据结构,Module就是基于它派生出来的。它的作用就是提供管理模块之间依赖的方法和属性,例如上面的

clearDependenciesAndBlocks方法,后面还有 addBlockaddDependecy等方法,主要相关的属性就是 dependenciesblocks

接下来就是遍历传入的 exposes配置,然后根据每一个配置项都会生成一个基于

AsyncDependenciesBlock 创建的实例block,可以简单的理解这里的 block就等同于模块,这里的 AsyncDependenciesBlock也是继承自 DependenciesBlock。看到这里,大家可能已经开始有点混乱了,从 DependencyModule,这里怎么又出现了一个 Block。简单理解这三者的关系就是:

当然,前提是这个模块是一个 javascript/dynamic的类型。

接下来就是遍历 option.import,然后每一个import项创建一个 ContainerExposedDependency的实例,然后调用 block.addDependency加入到blockdependencies数组中。

看到这里,我们知道了 exposes高级配置中的 import配置有什么作用:就是你可以告诉

ContainerPlugin你配置的 exposes模块依赖了什么模块。当然 Webpack 本身就会去分析模块之间的依赖,所以我个人理解这里的使用场景是,比如依赖了三方的 external模块,这时候就需要通过配置来实现。

遍历完所有的 exposes选项后,最后调用了 addDependecyContainerEntryModule增加了一个 StaticExportsDependency的 dep。关于这个逻辑的作用,我查阅了官网,实际上这是一个可配置的优化,目的是为了告诉 webpack 一个模块导出了哪些方法,能让 webpack 构建的时候为 export * from 'xxx' 语法生成执行效率更高的代码

最后执行了 callback,表示该模块 build结束,进入下一个阶段,

compilation.handleModuleCreation方法调用,比较相关的就是 codeGeneration的方法,它的执行时机是在开始 runLoaders之前。

我们来看 codeGeneration的实现:

/**
	 * @param {CodeGenerationContext} context context for code generation
	 * @returns {CodeGenerationResult} result
	 */
	codeGeneration({ moduleGraph, chunkGraph, runtimeTemplate }) {
		const sources = new Map();
		const runtimeRequirements = new Set([
			RuntimeGlobals.definePropertyGetters,
			RuntimeGlobals.hasOwnProperty,
			RuntimeGlobals.exports
		]);
		const getters = [];

		// 这里是取出 module 依赖的 blocks,然后取出 dependencies,然后生成模块的 runtime 时的依赖数组
		// this.blocks 是一个 AsyncDependenciesBlock 数组
		for (const block of this.blocks) {
			const { dependencies } = block;

			const modules = dependencies.map(dependency => {
				const dep = /** @type {ContainerExposedDependency} */ (dependency);
				return {
					name: dep.exposedName,
					// 从 moduleGraph 中根据 dep 取出对应的 module,在 build 阶段 webpack 会生成 moduleGraph 来存放 module 之间的关系
					module: moduleGraph.getModule(dep),
					request: dep.userRequest
				};
			});

			let str;

			if (modules.some(m => !m.module)) {
				str = runtimeTemplate.throwMissingModuleErrorBlock({
					request: modules.map(m => m.request).join(", ")
				});
			} else {
				str = `return ${runtimeTemplate.blockPromise({
					block,
					message: "",
					chunkGraph,
					runtimeRequirements
				})}.then(${runtimeTemplate.returningFunction(
					runtimeTemplate.returningFunction(
						`(${modules
							.map(({ module, request }) =>
								runtimeTemplate.moduleRaw({
									module,
									chunkGraph,
									request,
									weak: false,
									runtimeRequirements
								})
							)
							.join(", ")})`
					)
				)});`;
			}

			getters.push(
				`${JSON.stringify(modules[0].name)}: ${runtimeTemplate.basicFunction(
					"",
					str
				)}`
			);
		}

		// 这里是拼接 mf runtime 代码,每一个 mf expose 模块都会导出一个含有 get 和 init 方法的对象
		const source = Template.asString([
			`var moduleMap = {`,
			Template.indent(getters.join(",\n")),
			"};",
			`var get = ${runtimeTemplate.basicFunction("module, getScope", [
				`${RuntimeGlobals.currentRemoteGetScope} = getScope;`,
				// reusing the getScope variable to avoid creating a new var (and module is also used later)
				"getScope = (",
				Template.indent([
					`${RuntimeGlobals.hasOwnProperty}(moduleMap, module)`,
					Template.indent([
						"? moduleMap[module]()",
						`: Promise.resolve().then(${runtimeTemplate.basicFunction(
							"",
							"throw new Error('Module "' + module + '" does not exist in container.');"
						)})`
					])
				]),
				");",
				`${RuntimeGlobals.currentRemoteGetScope} = undefined;`,
				"return getScope;"
			])};`,
			`var init = ${runtimeTemplate.basicFunction("shareScope, initScope", [
				`if (!${RuntimeGlobals.shareScopeMap}) return;`,
				`var name = ${JSON.stringify(this._shareScope)}`,
				`var oldScope = ${RuntimeGlobals.shareScopeMap}[name];`,
				`if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");`,
				`${RuntimeGlobals.shareScopeMap}[name] = shareScope;`,
				`return ${RuntimeGlobals.initializeSharing}(name, initScope);`
			])};`,
			"",
			"// This exports getters to disallow modifications",
			`${RuntimeGlobals.definePropertyGetters}(exports, {`,
			Template.indent([
				`get: ${runtimeTemplate.returningFunction("get")},`,
				`init: ${runtimeTemplate.returningFunction("init")}`
			]),
			"});"
		]);

		sources.set(
			"javascript",
			this.useSourceMap || this.useSimpleSourceMap
				? new OriginalSource(source, "webpack/container-entry")
				: new RawSource(source)
		);

		return {
			sources,
			runtimeRequirements
		};
	}
复制代码

这里的代码实现大概有 110 行左右,但其实逻辑还是非常容易看懂,当然前提是你对 Webpack 生成产物代码的机制有大概的了解。

首先函数定了一个 sources变量,其类型是 Map;还有一个变量 runtimeRequirements,其类型是 Set,最后是一个 getters变量,这是一个临时过渡的变量。

我们先简单说一下 Webpack 源码生成的一点知识,简单地说 Webpack 产物的源码主要有两部分:一部分是 Webpack runtime,一部分就是源码文件编译后的代码。Webpack 作为一个模块打包器,支持大多数常用的模块类型,例如 esmcommonjsamd等等,但是这些模块构建出来的产物如果要运行在浏览器中,所以需要配套的 runtime代码去支持这些模块化,所以 Webpack 实现了自己的一套模块加载机制,如果你看过 Webpack 构建产物的源码,我们能发现很多的 __webpack_require__等相关的关键字。除此之外,Webpack 实现了一套方便生成 runtime代码地 runtimeTemplate模板,所以在 codeGeneration我们能看到大量的关于runtimeTemplate方法调用。

所以,上面提到的 runtimeRequirements 实际上存储的是相关的模块构建产物所需要依赖的 Webpack runtime 一些方法的集合,而所有的 runtime方法存储在 lib/RuntimeGlobal.js中。我们简单看下这里用到的3个 runtime方法的作用:

  • RuntimeGlobals.definePropertyGetters,定义一个模块导出的属性的 getter 方法,对应的名称是 __webpack_require__.d
  • RuntimeGlobals.hasOwnProperty,对于原生的 Object.prototype.hasOwnProperty方法的封装,对应的名称是 __webpack_require__.o
  • RuntimeGlobals.exports,Webpack 模块机制中的 exports方法,对应的名称是

__webpack_exports__

所以,ContainerEntryModule编译后的代码中就会注入以上的几个 Webpack runtime 方法。

接着就是遍历模块的 blocks,然后取出每一个 block中的 dependencies去构建出一个 modules 数组。继续往下就是一个防御性的处理,如果发现 modules里面的一些项没有 module属性,也就是从

moduleGraph找不到,就会在源码中生成一个提示模块找不到的日志。

这里我们主要看 else中的处理,发现这里会拼接出一个基于 modules数组的 blockPromise字符串,然后使用 getters存储起来。光看这里的拼接可能比较抽象,我们直接看一个具体的例子。以第一篇文章中的 APP1 为例,它导出了一个 Input组件,实际上编译后的这部分运行时代码如下:

var o = {
  "./input": ()=>Promise.all([t.e("antd-icons-vendor"), t.e("defaultVendors-node_modules_ant-design_colors_dist_index_esm_js-node_modules_antd_es_input_index_js"), t.e("default-webpack_sharing_consume_default_react_react"), t.e("default-webpack_sharing_consume_default_react-dom_react-dom"), t.e("src_components_Input_index_tsx")]).then((()=>()=>t("./src/components/Input/index.tsx")))
}
复制代码

这行代码的含义是,当我们需要去加载该组件,那么需要拉取所有其依赖的 dependencies模块,而这些模块通过 Promise.all进行异步加载。

我们继续下面的代码,紧接着通过模板字符串生成了一段使用变量 source存储的字符串代码,这段代码就是 MF exposes模块的核心 runtime 代码。 从字符串我们能隐隐看到几个关键的变量,一个是 moduleMap,这个 modulMap实际上就是上面我们遍历 blocks所生成的 getters数组的每一项拼接成上面代码例子中的 o变量,它是一个keyrequestvalue为一个函数,返回依赖的模块Promise.all数组对象。

再下面分别定义了一个 getinit方法,最后导出了一个getter方法,方法中返回的是含有 get

init方法的对象。我们也通过一个例子,看下这段字符串拼接的代码:

var moduleMap = {
	"./Button": () => {
		return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_react_react"), __webpack_require__.e("src_Button_tsx")]).then(() => (() => ((__webpack_require__(/*! ./src/Button */ "./src/Button.tsx")))));
	}
};
var get = (module, getScope) => {
	__webpack_require__.R = getScope;
	getScope = (
		__webpack_require__.o(moduleMap, module)
			? moduleMap[module]()
			: Promise.resolve().then(() => {
				throw new Error('Module "' + module + '" does not exist in container.');
			})
	);
	__webpack_require__.R = undefined;
	return getScope;
};

var init = (shareScope, initScope) => {
	if (!__webpack_require__.S) return;
	var name = "default"
	var oldScope = __webpack_require__.S[name];
	if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
	__webpack_require__.S[name] = shareScope;
	return __webpack_require__.I(name, initScope);
};

// This exports getters to disallow modifications
__webpack_require__.d(exports, {
	get: () => (get),
	init: () => (init)
});
复制代码

我们待会再解释这段代码,我们继续后面的一点源码介绍。

最后 codeGeneration通过对象的形式返回了前面构造的 sourcesruntimeRequirements。实际上所有模块的此方法都会返回一个对象,除了 sourcesruntimeRequirements,还可以返回属性为

hashdata等数据,hash代表的是代码生成的唯一标识,如果没有返回,Webpack 将自动帮你生成。data代表的是所有模块的非runtime部分代码,其实就是源码。

到这里,我们就看完了 ContainerEntryModule的核心实现,其实本身的逻辑不复杂,但是为了方便读者更加容易理解这部分源码,这里引入了很多的 Webpack 背景知识介绍,信息量还是比较大的。对于有些背景知识,大家可以通过自行通过官网和源码再了解更多的细节,鉴于篇幅问题,就不会再继续展开。

下面我们继续介绍下上面那段生成后的代码的含义,其实本质就是 ContainerEntryModule模块加载机制的实现。

Container 模块的加载机制

在前面源码分析的时候,我们知道所有的 ContainerEntryModule 都是默认模块类型是动态的,也就是说可以结合 import语法进行使用。结合 MF 的运行时共享机制,我们可以理解肯定是需要将模块设计成这种方式,否则怎么做到动态更新。其实,最大的问题在于,怎么能实现动态加载的同时,还能有办法获取到 remote 应用提供的一些上下文,例如它导出的模块,通过什么样的方式去匹配。

答案很简单:通过 js runtime 的全局对象做一个上下文存储,在浏览器端,就是 window 对象。

所以前面那段构建出来的 ContainerEntryModule runtime代码的作用就是:首先它将所有导出的模块生成一个 moduleMap,然后在上下文里面提供了一个 init方法和get方法,init的方法作用就是从

__webpack_require__.S(存储所有的 remote 模块,在 Webpack 中称之为 shareScopeMap)找到当前你需要 shareScope并返回,而 get方法则是从当前的 shareScopemoduleMap中取出你想要的模块。所以,官网文档提供了一个你获取远程模块的代码案例,它的实现是这样的:

function loadComponent(scope, module) {
  return async () => {
    // Initializes the shared scope. Fills it with known provided modules from this build and all remotes
    await __webpack_init_sharing__('default');
    const container = window[scope]; // or get the container somewhere else
    // Initialize the container, it may provide shared modules
    await container.init(__webpack_share_scopes__.default);
    const factory = await window[scope].get(module);
    const Module = factory();
    return Module;
  };
}
复制代码

通过这个方法,我们就可以异步加载 remote 的所有导出的模块,当然,使用这个方法的前提是,host 应用必须配置 MF 插件的remotes选项。这部分,我在下一篇文章中再详细讲解。

总结

本篇文章,我们详细解析了 ContainerPlugin的源码,但是为了帮助大家理解这部分的源码解读,从而引入了大量的 Webpack 背景知识。其实 ContainerPlugin本身的源码实现并不复杂,它更多的是基于Webpack 之前的构建机制,新增了一种新的 addEntry方式。从本文中,我们能得到如下信息:

  • Webpack 本身的构建机制,在不同的阶段依赖了不同的数据结构去进行处理,从初始化阶段的

Dependency到构建阶段的 Module,最后是生成阶段的 Chunk

  • ContainerPlugin本身依赖了这套机制,基于基础的数据结构扩展了

ContainerEntryDependencyContainerEntryModule等数据结构,通过在合适的时机调用

compilation.addEntryexposes加入到构建流程中,然后通过ContainerEntryModule

buildcodeGeneration的实现构建出不一样的模块产物;

  • ContainerEntryModule 模块本质上能做到运行时共享是因为所有的 exposes模块是动态加载的,通过一定的运行时函数在使用的时候再去远程拉取模块代码。

Reference

后续文章

下一篇文章,我将解析 ContainerReferencePlugin插件的源码,有了 remote应用,那么消费方

host又该怎么去构建,才能去消费 remote应用导出的模块?且听下回分晓。

最后打一个广告,本人最近也创建了自己的公众号,不定时的会更新前端技术文章和读书感悟,有兴趣的小伙伴可以加个关注:

image.png

分类:
前端
标签: