前言
在上一篇文章中,我介绍了 MF 的一些基本知识以及一些 MF 的应用场景,并且分析了主要的插件入口源码,因为入口插件源码比较简单,所以我详细介绍了插件的一些配置,从而得知 Webpack 官网上都没有介绍的高级配置,例如 library
、exposes
配置的 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
方法里面的代码,有些简单的逻辑,我也在上面的代码中用注释解释了,所以下面详细解析我觉得比较核心的部分实现。
首选,该插件监听的是一个叫make
的 hook
,这个hook
触发的时机是在 compilation
对象创建之后。然后接着在回调里面根据传入的options
选项新建了ContainerEntryDependency
实例 dep
,最后调用compilation.addEntry
并传入dep
。这几行代码是整个ContainerPlugin
最为核心的,特别是
addEntry
的调用,其核心的作用实际上就是把 exposes
配置的各个模块当成一个新的entry
加入到 Webpack 构建流程中。这里会涉及到 ContainerEntryDependency
的源码,我们这里先不着急看,留在下一小节介绍。
我们继续看下面的源码,接着插件监听了thisCompilation
的 hook
,回调里面的逻辑做了两件事,那就是将 ContainerEntryDependency
的 dependencyFactory
设置成
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
。
接着在构建阶段调用 addEntry
将 dependency
加入到构建流程,递归去分析dependency
的依赖,然后将 dependency
转换成 module
,最后根据前面的分析生成模块依赖图谱。我们经常说 Webpack 将所有的文件类型,无论是 .js
、.json
、.ts
、.png
、.vue
等都视作模块,准确来说,是在构建阶段所有从入口文件开始收集到的 dependency
最终都会转换成 module
,并且构建完整的依赖图谱。
最后在生成阶段,根据模块依赖图和spliting code
算法将一个模块或者多个模块组合在一起生成
chunk
,最后再生成文件,也就是最终的 bundle 产物。
单独讲这个过程,可能有点抽象,我们看一个构建阶段的流程图:
在构建阶段,最开始的流程其实就是从addEntry
出发,然后经历 factorizeModule
、addModule
、
build
、runLoaders
、parse
等核心的方法调用流程后,就会完成构建出一个基于 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
扩展的类:
ModuleDependency
、EntryDependency
、ContainerEntryDependency
。
Webpack 的构建阶段就是将 entry
对应生成的Dependency
转换成 Module
,构建模块依赖图谱。在 Webpack 源码也有相对应的数据结构,它的源码文件在 lib/Module.js
下,Module
是 Webpack 构建时处理的最小单位。而 Module
也是比较基础的数据结构,真正源码中创建的模块有:NormalModule
、
RuntimeModule
、ContainerEntryModule
等;
Chunk
是 Webpack 构建流程中生成文件之前最后一个数据结构的抽象,它是在最后生成阶段的时候”组装“出来的。很容易理解,实际上在编译阶段生成的所有 Module
,在生成阶段就会根据模块之间的关系将一个或者多个Module
组装起来生成一个个 Chunk
。在 Webpack 默认的策略中,以下场景会默认生成一个
Chunk
:
- entry 配置,一个 entry 以及文件所引用的所有 JS 模块组合成一个 Chunk;
- 动态 import 一个模块会单独生成一个 Chunk。
小结
虽然只是一个插入介绍,但是信息量也是非常庞大,不过有了以上的一些背景知识,我们再回到插件的源码中,我们能很容易理解它内部的运作原理。
下面,我们回到插件源码,继续深入 ContainerEntryDependency
和 ContainerEntryModule
源码。
ModuleFactory
在开始深入ContainerEntryDependency
和 ContainerEntryModule
源码之前,我们先解答插件源码中在后面 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 构建流程中。
当然这里还需要依赖 ContainerEntryDependency
和 ContainerEntryModule
的实现,下面我们继续深入到这两个类的源码中。
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
其实代表的是一些节点在源文件中的位置信息。在 entry
和Module
之间引入Dependency
,个人觉得有如下好处:
- 在真正做
module
转换之前,需要有一个数据结构来过渡,进行例如将模块创建过程工厂化; - 存储一些
entry
相关的上下文信息,例如ContainerEntryDependency
中的exposes
和
shareScope
等;
看了 ContainerEntryDependency
,下面我们继续 ContainerEntryModule
的源码。
ContainerEntryModule
ContainerEntryModule
的源码在lib/container/ContainerEntryModule.js
中,这里介绍下看
Module
源码的一个窍门:实际上 Module
最核心也是最复杂的两个函数实现是build
和
codeGeneration
,而通过 Module
派生出来的例如 NormalModule
和 ContainerEntryModule
两者之间最大的区别就是这两个方法的实现。其它的一些属性或者方法,可以在需要关注的时候再细看。
当然除此之外,每个 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
方法执行的逻辑是赋值了
buildMeta
和 buildInfo
,这两个属性是每个模块都具有的属性,用来方便存储一些构建模块和生成代码时需要用到的上下文信息。这里需要留意下 buildInfo
中的 topLevelDeclarations
属性,这个
Set
里面的几个字符串我们在后续的 codeGeneration
将会看到。
继续往下走,这里调用了 clearDependenciesAndBlocks
方法,我理解这里的作用是每一轮构建都会先清空上一轮的 depnedency
和block
的数据,其背后调用的是 DependenciesBlock
中的
clearDependenciesAndBlocks
方法:
clearDependenciesAndBlocks() {
this.dependencies.length = 0;
this.blocks.length = 0;
}
复制代码
因为模块构建是一次性的,个人理解这里更多的是考虑开发模式下,当代码发生变动的时候,重新走构建的过程。
这里引入了 DependenciesBlock
的数据结构,实际上它是一个更加基础的数据结构,Module
就是基于它派生出来的。它的作用就是提供管理模块之间依赖的方法和属性,例如上面的
clearDependenciesAndBlocks
方法,后面还有 addBlock
和 addDependecy
等方法,主要相关的属性就是 dependencies
和 blocks
。
接下来就是遍历传入的 exposes
配置,然后根据每一个配置项都会生成一个基于
AsyncDependenciesBlock
创建的实例block
,可以简单的理解这里的 block
就等同于模块,这里的 AsyncDependenciesBlock
也是继承自 DependenciesBlock
。看到这里,大家可能已经开始有点混乱了,从 Dependency
到 Module
,这里怎么又出现了一个 Block
。简单理解这三者的关系就是:
当然,前提是这个模块是一个 javascript/dynamic
的类型。
接下来就是遍历 option.import
,然后每一个import
项创建一个 ContainerExposedDependency
的实例,然后调用 block.addDependency
加入到block
的 dependencies
数组中。
看到这里,我们知道了 exposes
高级配置中的 import
配置有什么作用:就是你可以告诉
ContainerPlugin
你配置的 exposes
模块依赖了什么模块。当然 Webpack 本身就会去分析模块之间的依赖,所以我个人理解这里的使用场景是,比如依赖了三方的 external
模块,这时候就需要通过配置来实现。
遍历完所有的 exposes
选项后,最后调用了 addDependecy
为 ContainerEntryModule
增加了一个 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 作为一个模块打包器,支持大多数常用的模块类型,例如 esm
、commonjs
、amd
等等,但是这些模块构建出来的产物如果要运行在浏览器中,所以需要配套的 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
变量,它是一个key
为 request
,value
为一个函数,返回依赖的模块Promise.all
数组对象。
再下面分别定义了一个 get
和 init
方法,最后导出了一个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
通过对象的形式返回了前面构造的 sources
和 runtimeRequirements
。实际上所有模块的此方法都会返回一个对象,除了 sources
、runtimeRequirements
,还可以返回属性为
hash
、data
等数据,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
方法则是从当前的 shareScope
的 moduleMap
中取出你想要的模块。所以,官网文档提供了一个你获取远程模块的代码案例,它的实现是这样的:
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
本身依赖了这套机制,基于基础的数据结构扩展了
ContainerEntryDependency
和 ContainerEntryModule
等数据结构,通过在合适的时机调用
compilation.addEntry
将 exposes
加入到构建流程中,然后通过ContainerEntryModule
的
build
和 codeGeneration
的实现构建出不一样的模块产物;
- 而
ContainerEntryModule
模块本质上能做到运行时共享,是因为所有的exposes
模块是动态加载的,通过一定的运行时函数在使用的时候再去远程拉取模块代码。
Reference
后续文章
下一篇文章,我将解析 ContainerReferencePlugin
插件的源码,有了 remote
应用,那么消费方
host
又该怎么去构建,才能去消费 remote
应用导出的模块?且听下回分晓。
最后打一个广告,本人最近也创建了自己的公众号,不定时的会更新前端技术文章和读书感悟,有兴趣的小伙伴可以加个关注: