ModuleFederation是webpack5中一个激动人心的新特性,也是一个据说可以改变JavaScript架构游戏规则的功能。接下来,就让我们慢慢揭开模组联邦的神秘面纱。模块共享的方案对比场景:我们目前有项目A和项目B,发现它们有一定的共性,比如公共UI组件,utils等,那么我们如何共享这些公共信息呢?简单粗暴——CV大法直接将项目A的组件复制到项目B中。这种方式有时速度更快,但也存在维护成本极低的问题。接下来的两个项目各维护自己的一套。抽象成npm我们可以将一些公共模块抽象成npm,在每个项目中安装npm包,从而达到共享的目的,但是npm包的方式存在以下问题:编译构建:一些公共工具库,frameworks和UI库被反复构建,导致性能不佳。版本更新:每个项目都需要升级。“发布->通知->更新”的方式,效率相对较低。CDN+webpackexternals和npm类似,只是上传到CDN,结合webpackexternals加载。除了上面提到的问题,externals也不是按需加载的。gitsubmodule允许您将Git存储库作为另一个Git存储库的子目录。它允许您将另一个存储库克隆到您自己的项目中,同时仍将提交分开。还是存在重复建设的问题,上手会有一定的成本。相关命令:gitsubmoduleadd:添加子模块。gitsubmoduleupdate--recursive--remote:拉取所有子模块的更新。什么是模块联盟?官方文档是这样解释其动机的:多个独立构建可以组成一个应用,这些独立构建之间不应该存在依赖关系,因此可以独立开发和部署。这通常被称为微前端,但不限于此。模块联合使一个JavaScript应用程序能够从另一个JavaScript应用程序动态加载代码,这解决了我们上面提到的模块共享问题。不仅仅是微前端,场景粒度可以更细。一般微前端更多的是在应用层面,但是更倾向于模块层面的分享。ModuleFederation配置在实战之前,我们先了解一下ModuleFederation的配置项,首先是两个基本角色的约定:Host。使用模块的一方。偏僻的。提供模块的一方。每个应用程序都可以用作主机和远程。ModuleFederation配置项如下:name:必须且唯一。文件名:如果未提供文件名,构建将生成一个与容器名称同名的文件名。remotes:可选,作为referrer最关键的配置项,用于声明需要引用的远程资源包的名称和模块名称,作为Host使用时要消费哪些Remotes。exposes:可选,表示当作为Remote使用时会消耗export的哪些属性。库:可选,定义远程应用程序如何向主机应用程序公开输出。配置项的值为对象,如{type:'xxx',name:'xxx'}。shared,可选,表示哪些依赖可以在远程应用的输出内容和宿主应用之间共享。要让共享生效,宿主应用和远程应用的共享配置的依赖关系必须一致。Singleton:是否开启单例模式。默认值为假。启用后,远程应用组件和宿主应用共享的依赖只加载一次,且两者版本较高。requiredVersion:指定共享依赖的版本,默认值为当前应用的依赖版本。eager:共享依赖是否在打包时分成异步块。当设置为true时,共享依赖会被打包到main和remoteEntry中,不会分离,所以设置为true时共享依赖是没有意义的。实际演示这里我们使用Github中的ModuleFederationExamples[1]进行演示。这包括基本用法、高级用法以及与一些框架的结合实践。注意:这个存储库是使用lerna维护的。所以你需要安装lerna。npminstalllerna-g通过lernabootstrap安装依赖项。举个简单的例子,basic-host-remote目录下有两个独立的项目,分别是app1和app2。其中,app2中实现了一个Button组件,现在app1需要使用这个Button组件。importReactfrom'react';constButton=()=>;exportdefaultButton;app2暴露组件此时app2的作用是Remote,核心webpack配置:const{ModuleFederationPlugin}=require('webpack').container;//...plugins:[newModuleFederationPlugin({name:'app2',library:{type:'var',name:'app2'},filename:'remoteEntry.js',//生成的文件名exposes:{'./Button':'./src/Button',//导出Button组件},//共享react和react-domshared:{react:{singleton:true},'react-dom':{singleton:true}},}),],//...app1消费组件此时app1的角色是Host,webpack核心配置:const{ModuleFederationPlugin}=require('webpack').container;//...//http://localhost:3002/remoteEntry.js插件:[newModuleFederationPlugin({name:'app1',remotes:{//http://localhost:3002/remoteEntry.js//上面配置生成的模块文件app2:`app2@${getRemoteEntryUrl(3002)}`,},//共享模块shared:{react:{singleton:true},'react-dom':{singleton:true}},}),],//...模块用法:constRemoteButton=React.lazy(()=>import('app2/按钮'));constApp=()=>(BasicHost-Remote
App1
);导出默认App;effect可以看到react和react-dom也都加载了一次:进阶示例-远程模块的动态加载如果初始化没有加载remote某次交互后如何加载remote模块?这个例子可以在advanced-api/dynamic-remotes中找到。示例中有三个项目,app1/app2/app3。app1是Host,消费app2和app3提供的组件,只有点击对应的按钮时才会加载对应的远程模块。另外app2和app3都使用了moment.js。app2和app3暴露的两个项目配置类似,都是暴露Widget组件,都共享react、react-dom和moment.js。这里需要注意的是,如果没有声明requiredVersion,将使用它能找到的当前主版本的最高版本。constdeps=require('./package.json').dependencies;//...newModuleFederationPlugin({name:'app3',library:{type:'var',name:'app3'},filename:'remoteEntry.js',exposes:{'./Widget':'./src/Widget',},//添加react作为共享模块//版本是从package.json推断出来的//没有对所需的版本检查version//因此它将始终使用找到的更高版本shared:{react:{requiredVersion:deps.react,import:'react',//"react"包将使用提供的后备模块shareKey:'react',//在此名称下,共享模块将放置在共享范围内shareScope:'default',//将使用具有此名称的共享范围singleton:true,//仅允许共享模块的单一版本},'react-dom':{requiredVersion:deps['react-dom'],singleton:true,//只允许共享模块的单一版本},//添加moment作为sharedmodule//versionisinferredfrompackage.json//itwillusethehighestmomentversionthatis>=2.24andlessthan3moment:deps.moment,},})app1消费模块app1作为Host,这里是通用的配置,就不细说了,主要看负责动态加载的代码。当相应的按钮被点击时,就会触发useFederatedComponent方法。入参中的remoteUrl为远程地址,scope为对应的应用名,module为指定模块。其中useDynamicScript负责加载远程JavaScript脚本。加载完成后,通过loadComponent方法动态加载组件。exportconstuseFederatedComponent=(remoteUrl,scope,module)=>{constkey=`${remoteUrl}-${scope}-${module}`;const[Component,setComponent]=React.useState(null);const{就绪,错误加载}=useDynamicScript(remoteUrl);React.useEffect(()=>{if(Component)setComponent(null);//仅在键改变时重新计算},[key]);React.useEffect(()=>{if(ready&&!Component){constComp=React.lazy(loadComponent(scope,module));componentCache.set(key,Comp);setComponent(Comp);}//键包括所有依赖项(作用域/模块)},[Component,ready,key]);返回{errorLoading,组件};};让我们再次关注loadComponent,其中__webpack_init_sharing__初始化共享范围并用提供的已知构建和所有远程模块填充它。然后获取远程容器容器,支持get和init方法。init是一个异步兼容的方法,调用时只包含一个参数:共享作用域对象——__webpack_share_scopes__.default。最后调用容器的get方法获取对应的模块。functionloadComponent(scope,module){returnasync()=>{//初始化共享范围。Thisfillsitwithknownprovidedmodulesfromthisbuildandallremotes//Initializesthesharedscope(sharedscope)withtheprovidedknownThisbuildandallremotemodulespopulateitawait__webpack_init_sharing__('de??fault');复制代码const容器=窗口[作用域];//或者在其他地方获取容器//初始化可能提供共享模块的容器awaitcontainer.init(__webpack_share_scopes__.default);constfactory=awaitwindow[scope].get(module);常量模块=工厂();返回模块;};}效果演示点击不同的按钮加载不同的组件。moment.js不需要在第一次加载后重新加载。您可以通过动态加载共享模块的不同版本来实施A/B测试。ModuleFederation的问题上面讲了那么多ModuleFederation的优点。让我们来看看它的缺点。对环境要求稍高,需要使用webpack5,老项目改造成本高。需要权衡拆分的粒度。虽然可以实现依赖共享,但是共享的lib不能tree-shaking。也就是说,如果共享了一个lodash,那么整个lodash库都会被打包到shared-chunk中。Webpack为了支持远程模块的加载,对运行时做了很多改动,运行时要做的事情也急剧增加,这可能会对我们页面的运行时性能造成负面影响。运行时共享也是一把双刃剑。如何做版本控制,控制共享模块的影响,是需要考虑的问题。对于问题1,以后应该会慢慢好起来的。问题2感觉不错,场景应该不会太多,而且相对共享模块来说,不重复编译的优势还是比较可以接受的。问题3,感觉不大。第4题很头疼。例如,多个项目需要相同版本的react/react-dom/antd。如果版本更新了,我们该怎么办?我们可以利用ModuleFederation的能力,集成一些核心依赖react、react-dom、antd,使用一个远程服务来维护,然后每个项目引用这个服务导出的库。我们只需要维护这个远程服务的依赖版本就可以保证各个项目的核心依赖版本是一致的,而且在升级的时候各个项目不需要自己升级,大大提高了效率。总结使用ModuleFederation,我们可以在一个应用程序中动态加载和执行另一个应用程序的代码,而无需考虑技术栈,并且可以共享模块,从而减少编译时间和包大小。但是在使用ModuleFederation的时候,还需要权衡模块拆分的粒度和版本控制。参考深入探索Webpack5的ModuleFederation的《奇葩说》[2]官网ModuleFederation[3]WebpackModuleFederation在React.js中的实践解析[4]参考文献[1]ModuleFederation实例:https://github.com/module-federation/module-federation-examples[2]深入探究Webpack5的ModuleFederation的“奇葩技能”:https://juejin.cn/post/6938975818659921957[3]官网ModuleFederation:https://webpack.docschina.org/concepts/module-federation/[4]React.js中WebpackModuleFederation的实践解析:https://juejin.cn/post/7012990703714172964