前言
尽管webpack5曾经公布了一段时间了,但始终都没有钻研过,最近正好在做微前端相干的调研,恰好看到了webpack5的模块联邦与微前端的相干计划,便想着探索下模块联邦的相干源码。(ps:对于微前端,略微说一句,集体感觉在选取微前端计划的时候可有联合现有资源以及状态进行相干取舍,从共享能力、隔离机制、数据计划、路由鉴权等不同维度综合考量,集体应用最小的迁徙老本,渐进式的过渡,才是最优的抉择。)
目录构造
container
- ModuleFederationPlugin.js (外围,重点剖析)
- options.js (用户输出的option)
- ContainerEntryDependency.js
- ContainerEntryModule.js
- ContainerEntryModuleFactory.js
- ContainerExposedDependency.js
- ContainerPlugin.js (外围,重点剖析)
- ContainerReferencePlugin.js (外围,重点剖析)
- FallbackDependency.js
- FallbackItemDependency.js
- FallbackModule.js
- FallbackModuleFactory.js
- RemoteModule.js
- RemoteRuntimeModule.js
- RemoteToExternalDependency.js
sharing
- SharePlugin.js (外围,重点剖析)
- ShareRuntimeModule.js
- utils.js
- resolveMatchedConfigs.js
- ConsumeSharedFallbackDependency.js
- ConsumeSharedModule.js
- ConsumeSharedPlugin.js
- ConsumeSharedRuntimeModule.js
- ProvideForSharedDependency.js
- ProvideSharedModule.js
- ProvideSharedModuleFactory.js
- ProvideSharedPlugin.js
- Module.js (webpack的module)
- ModuleGraph.js (module图的依赖)
源码解析
整体webpack5的模块联邦 Module Federation是基于ModuleFedreationPlugin.js的,其最初是以webapck插件的模式接入webpack中,其外部次要设计ContainerPlugin用于解析Container的配置信息,ContainerReferencePlugin用于两个或多个不同Container的调用关系的判断,SharePlugin是共享机制的实现,通过ProviderModule和ConsumerModule进行模块的生产和提供
Module
Webpack的module整合了不同的模块,抹平了不同的差别,模块联邦正是基于webpack的模块实现的依赖共享及传递
class Module extends DependenciesBlock { constructor(type, context = null, layer = null) { super(); // 模块的类型 this.type = type; // 模块的上下文 this.context = context; // 层数 this.layer = layer; this.needId = true; // 模块的id this.debugId = debugId++; } // webpack6中将被移除 get id() {} set id(value) {} // 模块的hash,Module图中依赖关系的惟一断定 get hash() {} get renderedHash() {} // 获取文件 get profile() {} set profile(value) {} // 模块的入口程序值 webpack模块加载的穿针引线机制 get index() {} set index(value) {} // 模块的进口信息值 webpack模块加载的穿针引线机制 get index2() {} set index2(value) {} // 图的深度 get depth() {} set depth(value) {} // chunk相干 addChunk(chunk) {} removeChunk(chunk) {} isInChunk(chunk) {} getChunks() {} getNumberOfChunks() {} get chunksIterable() {} // 序列化和反序列化上下文 serialize(context) {} deserialize(context) {}}
ContainerPlugin
class ContainerPlugin { constructor(options) {} apply(compiler) { compiler.hooks.make.tapAsync(PLUGIN_NAME, (compilation, callback) => { const dep = new ContainerEntryDependency(name, exposes, shareScope); dep.loc = { name }; compilation.addEntry( compilation.options.context, dep, { name, filename, library }, error => { if (error) return callback(error); callback(); } ); }); compiler.hooks.thisCompilation.tap( PLUGIN_NAME, (compilation, { normalModuleFactory }) => { compilation.dependencyFactories.set( ContainerEntryDependency, new ContainerEntryModuleFactory() ); compilation.dependencyFactories.set( ContainerExposedDependency, normalModuleFactory ); } ); }}
ContainerPlugin的外围是实现容器的模块的加载与导出,从而在模块外侧进行一层的包装为了对模块进行传递与依赖剖析
ContainerReferencePlugin
class ContainerReferencePlugin { constructor(options) {} apply(compiler) { const { _remotes: remotes, _remoteType: remoteType } = this; const remoteExternals = {}; new ExternalsPlugin(remoteType, remoteExternals).apply(compiler); compiler.hooks.compilation.tap( "ContainerReferencePlugin", (compilation, { normalModuleFactory }) => { compilation.dependencyFactories.set( RemoteToExternalDependency, normalModuleFactory ); compilation.dependencyFactories.set( FallbackItemDependency, normalModuleFactory ); compilation.dependencyFactories.set( FallbackDependency, new FallbackModuleFactory() ); normalModuleFactory.hooks.factorize.tap( "ContainerReferencePlugin", data => { if (!data.request.includes("!")) { for (const [key, config] of remotes) { if ( data.request.startsWith(`${key}`) && (data.request.length === key.length || data.request.charCodeAt(key.length) === slashCode) ) { return new RemoteModule( data.request, config.external.map((external, i) => external.startsWith("internal ") ? external.slice(9) : `webpack/container/reference/${key}${ i ? `/fallback-${i}` : "" }` ), `.${data.request.slice(key.length)}`, config.shareScope ); } } } } ); compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.ensureChunkHandlers) .tap("ContainerReferencePlugin", (chunk, set) => { set.add(RuntimeGlobals.module); set.add(RuntimeGlobals.moduleFactoriesAddOnly); set.add(RuntimeGlobals.hasOwnProperty); set.add(RuntimeGlobals.initializeSharing); set.add(RuntimeGlobals.shareScopeMap); compilation.addRuntimeModule(chunk, new RemoteRuntimeModule()); }); } ); }}
ContainerReferencePlugin外围是为了实现模块的通信与传递,通过调用反馈的机制实现模块间的传递
sharing
class SharePlugin { constructor(options) { const sharedOptions = parseOptions( options.shared, (item, key) => { if (typeof item !== "string") throw new Error("Unexpected array in shared"); /** @type {SharedConfig} */ const config = item === key || !isRequiredVersion(item) ? { import: item } : { import: key, requiredVersion: item }; return config; }, item => item ); const consumes = sharedOptions.map(([key, options]) => ({ [key]: { import: options.import, shareKey: options.shareKey || key, shareScope: options.shareScope, requiredVersion: options.requiredVersion, strictVersion: options.strictVersion, singleton: options.singleton, packageName: options.packageName, eager: options.eager } })); const provides = sharedOptions .filter(([, options]) => options.import !== false) .map(([key, options]) => ({ [options.import || key]: { shareKey: options.shareKey || key, shareScope: options.shareScope, version: options.version, eager: options.eager } })); this._shareScope = options.shareScope; this._consumes = consumes; this._provides = provides; } apply(compiler) { new ConsumeSharedPlugin({ shareScope: this._shareScope, consumes: this._consumes }).apply(compiler); new ProvideSharedPlugin({ shareScope: this._shareScope, provides: this._provides }).apply(compiler); }}
sharing的整个模块都在实现共享的性能,其利用Provider进行提供,Consumer进行生产的机制,将共享的数据隔离在特定的shareScope中,通过resolveMatchedConfigs实现了对provider依赖及consumer依赖的过滤,从而对共有依赖只进行一遍申请
总结
webpack5的模块联邦是在通过自定义Container容器来实现对每个不同module的解决,Container Reference作为host去调度容器,各个容器以异步形式exposed modules;对于共享局部,对于provider提供的申请内容,每个module都有一个对应的runtime机制,其在剖析完模块之间的调用关系及依赖关系之后,才会调用consumer中的运行时进行加载,而且shared的代码无需本人手动打包。webapck5的模块联邦能够实现微前端利用的模块间的互相调用,并且其共享与隔离均衡也把控的较好,对于想钻研模块联邦实现微前端的同学能够参考这篇文章【第2154期】EMP微前端解决方案,随着webpack5的推广及各大脚手架的跟进,置信webpack5的模块联邦计划会是将来微前端计划的支流。
参考
- webpack官网仓库
- Module federation 原理钻研
- 精读《Webpack5 新个性 - 模块联邦》
- Webpack 5 Module Federation: A game-changer in JavaScript architecture