Webpack5已经出来很久了,但是正式版还没有发布。在Webpack5的ChangeLog中,除了常规的性能优化和编译提速之外,还有一个更令人期待的功能,那就是ModuleFederation。ModuleFederation可以强行翻译成“模块联邦”,但是听起来很奇怪。我也在一个前端群里提过这个问题,没想到大家的回复五花八门。所以本文直接使用ModuleFederation,不翻译听起来更舒服。什么是模块联盟?ModuleFederation主要用来解决多个应用之间的代码共享问题,可以让我们更优雅的实现跨应用的代码共享。假设我们现在有两个项目A和B,项目A内部有一个轮播组件,项目B内部有一个新闻列表组件,现在有一个需求,将项目B的新闻列表迁移到项目A,需要确保双方新闻列表的风格在后续迭代中保持一致。这时候,你有两个选择:使用CV的方式,将项目B的代码完整拷贝一份到项目A中;将新闻组件独立出来,发布到内部npm,通过npm加载组件;CV方法肯定比独立成分快。毕竟没必要把组件代码从项目B中分离出来发布npm。但是CV方式的缺陷是代码不能及时同步。如果你复制代码后你的另一个同事修改了项目B的新闻组件,那么项目A和项目B的新闻组件就会不一致。这时候,如果你的两个项目恰好都使用了Webpack5,那应该是一件很开心的事情,因为你不需要任何代价,只需要简单的几行配置,就可以在项目中直接使用项目B的新闻组件A.不仅如此,项目A的轮播组件也可以在项目B中使用。也就是说,通过ModuleFederation的代码共享是双向的,听上去真想让人说:“我学不会!”。ModuleFederation实践我们来看看A/B项目的代码。项目A的目录结构如下:├──public│└──index.html├──src│├──index.js│├──bootstrap.js│├──App.js│└──Slides.js├──package.json└──项目B的webpack.config.js目录结构如下:├──public│└──index.html├──src│├──index.js│├──bootstrap.js│├──App.js│└──NewsList.js├──package.json└──项目A和项目B在webpack.config.js中的区别主要在于引入的组件App.js和索引。js,bootstrap.js是一样的。//index.jsimport("./bootstrap");//bootstrap.jsimportReactfrom"react";importReactDOMfrom"react-dom";importAppfrom"./App";ReactDOM.render(,document.getElementById("根"));项目A的App.js:importReactfrom"react";从“./Slides”导入幻灯片;constApp=()=>(
App1,LocalSlides
);导出默认应用程序;项目B的App.js:importReactfrom"react";从'./NewsList';constRemoteSlides=React.lazy(()=>import("app1/Slides"));constApp=()=>(
App2,LocalNewsList
);导出默认App;现在让我们看看访问ModuleFederation之前的webpack配置:constpath=require("path");constHtmlWebpackPlugin=require("html-webpack-plugin");module.exports={mode:"development",//入口文件entry:"./src/index",//开发服务配置devServer:{//项目A的端口是3001,项目B的端口是3002port:3001,contentBase:path.join(__dirname,"dist"),},output:{//项目A的端口为3001,项目B的端口为3002,loader:"babel-loader",exclude:/node_modules/,options:{presets:["@babel/preset-react"],},},],},plugins:[//processhtmlnewHtmlWebpackPlugin({模板:"./public/index.html",}),],};配置:exposes/remotes现在我们修改webpack配置,引入ModuleFederation,让项目A引入项目B的新闻组件//ProjectB的webpack配置const{ModuleFederationPlugin}=require("webpack").container;module.exports={plugins:[newModuleFederationPlugin({//提供给其他服务加载的文件filename:"remoteEntry.js",//唯一ID,用于标记当前服务名称:"app2",//模块需要暴露,使用时通过`${name}/${expose}`引入暴露:{"./NewsList":"./src/NewsList",}})]};//项目的webpack配置const{ModuleFederationPlugin}=require("webpack").container;module.exports={plugins:[newModuleFederationPlugin({name:"app1",//引用app2的服务remotes:{app2:"app2@http://localhost:3002/remoteEntry.js",}})]};我们重点关注exposes/remotes:提供exposes选项表示当前应用是一个Remote,exposes中的模块可以被其他Host引用,引用方法是import(${name}/${expose})。如果提供了remotes选项,则当前应用是一个Host,可以引用remote中的expose模块。然后修改项目A的App.js:importReactfrom"react";importSlidesfrom'./Slides';//导入项目B的新闻组件constRemoteNewsList=React.lazy(()=>import("app2/NewsList"));constApp=()=>(
App1,LocalSlides,RemoteNewsList
);导出默认应用程序;至此,项目A已经成功连接到项目B的新闻组件,我们来看项目A的网络请求,项目A配置app2的remote后:"app2@http://localhost:3002/remoteEntry.js",它会先请求项目B的remoteEntry.js文件作为入口。当我们导入B项目的news组件时,会得到B项目的src_NewsList_js.js文件。配置:除了上面提到的模块引入和模块暴露相关的配置外,shared还有一个shared配置,主要用到以避免项目中存在多个公共依赖项。比如我们现在的项目A引入了一个react/react-dom,项目B暴露的新闻列表组件也依赖于react/react-dom。如果你不解决这个问题,项目A将加载两个反应库。这让我想起刚入行时,公司的一个项目是基于拼接PHP模板的方式。不同部门在自己的模板中引入了一个jQuery,导致项目中引入了三个不同版本的jQuery,尤其是对页面的影响。表现。所以我们在使用ModuleFederation的时候,一定要记得将publicdependencies配置成shared。另外两个项目必须同时配置shared,否则会报错。接下来,我们在浏览器中打开项目A,在Chrome的网络面板中,我们可以看到项目A直接使用了项目B的react/react-dom。双向共享前面说到ModuleFederation的共享可以是双向。接下来,我们将项目A配置为Remote,将项目A的轮播组件暴露给项目B。//ProjectB的webpack配置const{ModuleFederationPlugin}=require("webpack").container;module.exports={plugins:[newModuleFederationPlugin({name:"app2",filename:"remoteEntry.js",//暴露了新闻列表组件公开:{"./NewsList":"./src/NewsList",},//引用app1的服务remotes:{app1:"app1@http://localhost:3001/remoteEntry.js",},shared:{react:{singleton:true},"react-dom":{singleton:true}}})]};//项目A的webpack配置const{ModuleFederationPlugin}=require("webpack").container;module.exports={plugins:[newModuleFederationPlugin({name:"app1",filename:"remoteEntry.js",//暴露轮播组件exposes:{"./Slides":"./src/Slides",},//引用app2的服务remotes:{app2:"app2@http://localhost:3002/remoteEntry.js",},shared:{react:{singleton:true},"react-dom":{singleton:true}},})]};在项目B中使用轮播组件://App.jsimportReactfrom"react";importNewsListfrom'./NewsList';+constRemoteSlides=React.lazy(()=>import("app1/Slides"));constApp=()=>(
-App2,LocalNewsList+App2、RemoteSlides,LocalNewsList+++
);导出默认App;在引入多个依赖的同时,ModuleFederation也支持一次性Remote多个项目,我们可以新建一个项目C,同时导入项目A的轮播组件和项目B的新闻列表组件。//项目C的webpack配置//其他配置与上一个项目基本一致,只是端口需要改为3003const{ModuleFederationPlugin}=require("webpack").container;module.exports={plugins:[newModuleFederationPlugin({name:"app3",//同时依赖项目A和Bremotes:{app1:"app1@http://localhost:3001/remoteEntry.js",app2:"app2@http://localhost:3002/remoteEntry.js",},shared:{react:{singleton:true},"react-dom":{singleton:true}}})]};访问组件:从“反应”导入反应;constRemoteSlides=React.lazy(()=>import("app1/Slides"));constRemoteNewsList=React.lazy(()=>import("app2/NewsList"));constApp=()=>(
App3,RemoteSlides,RemoteRemote
);导出默认App;加载逻辑这里有一点需要特别注意,就是入口文件index.js本身没有逻辑,而是把逻辑放在bootstrap.js、index.js中动态加载bootstrap.js//index.jsimport("./bootstrap");//bootstrap.jsimportReactfrom"react";importReactDOM来自“react-dom”;从“./App”导入App;ReactDOM.render(
,document.getElementById("root"));如果删掉bootstrap.js,直接把逻辑放到index.js里。jj可以吗?经过测试,确实不可行。主要是需要先加载remote暴露的js文件。如果bootstrap.js不是异步逻辑,在引入NewsList时,会依赖app2的remote.js。如果直接在main.js中执行,app2的remote.js根本就没有。加载,所以会有问题。从网络面板也可以看出,remote.js是在bootstrap.js之前加载的,所以我们的bootstrap.js肯定是一个异步逻辑。项目A的加载逻辑如下:加载main.jsmain.js主要包含webpack的一些运行时逻辑,以及远程请求和bootstrap请求。加载remote.jsmain.js会优先加载项目B的remote.js,将exposes中配置的内部组件暴露出来供外部使用。加载bootstrap.jsmain.js加载自己的主逻辑bootstrap.js,bootstrap.js会使用app2的新闻列表组件。内部使用__webpack_require__.e加载新闻组件,__webpack_require__.e定义在main.js中。/*webpack/runtime/ensurechunk*/(()=>{__webpack_require__.f={};__webpack_require__.e=(chunkId)=>{//__webpack_require__.e会传入__webpack_require__.f中传入的chunkIdfindreturnPromise.all(Object.keys(__webpack_require__.f).reduce((promises,key)=>{__webpack_require__.f[key](chunkId,promises);返回承诺;},[]));};})();__webpack_require__.f包含三个部分:__webpack_require__.f.remotes=(chunkId,promises)=>{}//webpack/runtime/remotes__webpack_require__.f.consumes=(chunkId,promises)=>{}//webpack/runtime/consumes__webpack_require__.f.j=(chunkId,promises)=>{}//webpack/runtime/jsonp我们现在只看remotes的逻辑,因为我们的新闻组件是作为remote加载的。/*webpack/runtime/remotesloading*/(()=>{varinstalledModules={};varchunkMapping={"webpack_container_remote_app2_NewsList":["webpack/container/remote/app2/NewsList"]};varidToExternalAndNameMapping={"webpack/container/remote/app2/NewsList":["default","./NewsList","webpack/container/reference/app2"]};__webpack_require__.f.remotes=(chunkId,promises)=>{//chunkId:webpack_container_remote_app2_NewsListchunkMapping[chunkId].forEach((id)=>{//id:webpack/container/remote/app2/NewsListvardata=idToExternalAndNameMapping[id];//require("webpack/container/reference/app2")["./NewsList"]varpromise=__webpack_require__(data[2])[data[1]];returnpromise;});}})();可以看到最终的调用方式会变成require("webpack/container/reference/app2")["./NewsList"],而这个模块之前加载的app2的remote.js已经定义好的src_NewsList_js的加载。js是由remote.js发起的。综上所述,Webpack5提供的ModuleFederation还是很强大的,尤其是对于多个项目中的代码共享,提供了极大的方便,但是它有一个致命的缺点,就是需要你所有的项目都基于Webpack并且已经升级到Webpack5。相比ModuleFederation,我个人更喜欢vite提供的方案,利用浏览器原生的模块化能力进行代码共享。你可以访问我的github以获得完整的代码。如果想看更多关于ModuleFederation的案例,可以访问官方仓库。