前言
在上一篇文章中,我介绍了 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应用导出的模块?且听下回分晓。
最后打一个广告,本人最近也创建了自己的公众号,不定时的会更新前端技术文章和读书感悟,有兴趣的小伙伴可以加个关注: