webpack5模块联邦源码探究

前端 | Webpack5模块联邦源码探究.png

前言

虽然webpack5已经发布了一段时间了,但一直都没有研究过,最近正好在做微前端相关的调研,恰好看到了webpack5的模块联邦与微前端的相关方案,便想着探究下模块联邦的相关源码。(ps:关于微前端,稍微说一句,个人觉得在选取微前端方案的时候可有结合现有资源以及形态进行相关取舍,从共享能力、隔离机制、数据方案、路由鉴权等不同维度综合考量,个人使用最小的迁移成本,渐进式的过渡,才是最优的选择。)

目录结构

image
  • 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的模块实现的依赖共享及传递

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">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) {} }</pre>

ContainerPlugin

image

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">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 ); } ); } }</pre>

ContainerPlugin的核心是实现容器的模块的加载与导出,从而在模块外侧进行一层的包装为了对模块进行传递与依赖分析

ContainerReferencePlugin

image

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">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()); }); } ); } }</pre>

ContainerReferencePlugin核心是为了实现模块的通信与传递,通过调用反馈的机制实现模块间的传递

sharing

image

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">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); } }</pre>

sharing的整个模块都在实现共享的功能,其利用Provider进行提供,Consumer进行消费的机制,将共享的数据隔离在特定的shareScope中,通过resolveMatchedConfigs实现了对provider依赖及consumer依赖的过滤,从而对共有依赖只进行一遍请求

总结

image
image

webpack5的模块联邦是在通过自定义Container容器来实现对每个不同module的处理,Container Reference作为host去调度容器,各个容器以异步方式exposed modules;对于共享部分,对于provider提供的请求内容,每个module都有一个对应的runtime机制,其在分析完模块之间的调用关系及依赖关系之后,才会调用consumer中的运行时进行加载,而且shared的代码无需自己手动打包。webapck5的模块联邦可以实现微前端应用的模块间的相互调用,并且其共享与隔离平衡也把控的较好,对于想研究模块联邦实现微前端的同学可以参考这篇文章【第2154期】EMP微前端解决方案,随着webpack5的推广及各大脚手架的跟进,相信webpack5的模块联邦方案会是未来微前端方案的主流。

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容