webpack5推出一个非常令人惊艳的功能叫
module federation,中文叫模块联邦,它提供了一套在不同项目构建之间的调度、运行机制。它很像微前端,但又不限于此。本文结合案例介绍一下该特性的基本应用和原理。
类似微前端
微前端的概念相信大家都不陌生,其本质是服务的拆分与隔离,最大程度地减少服务之间的冲突与碰撞。webpack的模块联邦做的事情与此差不多。
不过,webpack模块联邦有更多的优点:
- 基于webpack生态,学习成本、实施成本低。毕竟大多数项目都在webpack
- 天生的工程化,npm各种包任你发挥
- 相关概念脉络清晰易懂
- 配置简单易上手,官方也提供了基于各种框架的版本
那么,如果你之前有过在前端实践微服务的念头,又由于这样那样的原因没有实施,现在,你的机会来了!
三个概念
首先,要理解三个重要的概念:
- webpack构建。一个独立项目通过webpack打包编译而产生资源包。
- remote。一个暴露模块供其他
webpakc构建消费的webpack构建。 - host。一个消费其他
remote模块的webpack构建。
一言以蔽之,一个webpack构建可以是remote–即服务的提供方,也可以是host–即服务的消费方,也可以同时扮演服务提供者和服务消费者,完全看项目的架构。
host与remote两个角色的依赖关系可用下图表示:

需要指出的是,任何一个webpack构建既可以作为host消费方,也可以作为remote提供方,区别在于职责和webpack配置的不同。
案例实操
项目依赖关系介绍
一共有三个微应用:lib-app、component-app、main-app,角色分别是:
lib-appas remote,暴露了两个模块react和react-domcomponent-appas remote and host,依赖lib-app,暴露了一些组件供main-app消费main-appas host,依赖lib-app和component-app
lib-app暴露模块
//webpack.config.js
module.exports = {
//...省略
plugins: [
new ModuleFederationPlugin({
name: "lib_app",
filename: "remoteEntry.js",
exposes: {
"./react":"react",
"./react-dom":"react-dom"
}
})
],
//...省略
}
复制代码
编译后的结果如下:

除去生成的map文件,有四个文件:main.js、remoteEntry.js、...react_index.js、...react-dom_index.js;
- 第一个是本项目的入口文件(该项目只是暴露接口,所以该文件为空)
- 第二个是远程入口文件,其他webpack构建使用、访问本项目暴露的模块时,须通过它来加载
- 第三个和第四个是暴露的模块,供其他项目消费
component-app的配置
依赖lib-app,暴露三个模块组件Button、Dialog、Logo
//webpack.config.js
module.exports = {
//...省略
plugins:[
new ModuleFederationPlugin({
name: "component_app",
filename: "remoteEntry.js",
exposes: {
"./Button":"./src/Button.jsx",
"./Dialog":"./src/Dialog.jsx",
"./Logo":"./src/Logo.jsx"
},
remotes:{
"lib-app":"lib_app@http://localhost:3000/remoteEntry.js"
}
}),
]
}
复制代码
三个暴露的组件:
//Button.jsx
import React from 'lib-app/react';
export default function(){
return <button style={{color: "#fff",backgroundColor: "#409eff",borderColor: "#409eff"}}>按钮组件</button>
}
复制代码
//Dialog.jsx
import React from 'lib-app/react';
export default class Dialog extends React.Component {
constructor(props) {
super(props);
}
render() {
if(this.props.visible){
return (
<div style={{position:"fixed",left:0,right:0,top:0,bottom:0,backgroundColor:"rgba(0,0,0,.3)"}}>
<button onClick={()=>this.props.switchVisible(false)} style={{position:"absolute",top:"10px",right:"10px"}}>X</button>
<div style={{ marginTop:"20%",textAlign:"center"}}>
<h1>
What is your name ?
</h1>
<input style={{fontSize:"18px",lineHeight:2}} type="text" />
</div>
</div>
);
}else{
return null;
}
}
}
复制代码
// Logo.jsx
import React from 'lib-app/react';
import pictureData from './MF.jpeg'
export default function(){
return <img src={pictureData} style={{width:"500px",borderRadius:"10px"}}/>
}
复制代码
构建结果基本跟上一个类似。
需要说明的是,为了保证暴露的组件可以正常工作,需要在本地做测试,main.js 是测试的入口函数。该子项目下运行npm run start打开浏览器:localhost:3001可以看到组件正常工作:

并且打开控制台网络,react、react-dom模块已经从本项目中分离:

main-app的配置
main-app依赖两个项目lin-app、component-app。
///webpack.config.js
module.exports = {
//省略...
plugins: [
new ModuleFederationPlugin({
name: "main_app",
remotes:{
"lib-app":"lib_app@http://localhost:3000/remoteEntry.js",
"component-app":"component_app@http://localhost:3001/remoteEntry.js"
},
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
})
]
//省略...
};
复制代码
由于需要等待基础模块加载完毕,所以需要配置懒加载入口bootstrap.js.
- webpack打包入口文件
import("./bootstrap.js")
复制代码
- bootstrap.js
import App from './App.jsx'
import ReactDOM from 'lib-app/react-dom';
import React from 'lib-app/react'
ReactDOM.render(<App />, document.getElementById("app"));
复制代码
- 根组件App.jsx
import React from 'lib-app/react';
import Button from 'component-app/Button'
import Dialog from 'component-app/Dialog'
import Logo from 'component-app/Logo'
export default class App extends React.Component{
constructor(props) {
super(props)
//省略...
}
//省略...
render(){
return (<div>
//省略...
</div>)
}
}
复制代码
运行并打开浏览器http://localhost:3002:

查看控制台,资源进行了很好的分离:

基本原理
这一节,我们从host的代码着手,简单分析这一切是如何交互、工作的。
程序从main.js里的一段代码开始:
__webpack_require__.e("bootstrap_js").then(__webpack_require__.bind(__webpack_require__,"./bootstrap.js"))
复制代码
__webpack_require__.e("bootstrap_js")是加载id为bootstrap_js的chunk的所有依赖,返回一个promise.等一切依赖就绪,再获取./bootstrap.js模块并执行
这里是__webpack_require__.e的代码:
__webpack_require__.e = (chunkId) => {
return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};
复制代码
上面一段代码做了一件事,遍历__webpack_require__.f对象并依次执行对象里的成员函数,此时该对象有两个成员:
{
remotes:(chunkId, promises) => {
//查找chunkId bootstrap_js对应的所有远程模块并加载
var chunkMapping = {
"bootstrap_js": [
"webpack/container/remote/lib-app/react",
"webpack/container/remote/component-app/Button",
//省略...
]
};
var idToExternalAndNameMapping = {
"webpack/container/remote/lib-app/react": [
"default",
"./react",
"webpack/container/reference/lib-app"
],
"webpack/container/remote/component-app/Button": [
"default",
"./Button",
"webpack/container/reference/component-app"
],
//...省略
};
},
j:(chunkId,promises)=>{
//负责加载chunkId对应的本地模块
}
}
复制代码
综上,bootstrap_js对应了两个promises:
- 一个负责远程依赖加载
- 另一个负责本地加载
等到所有依赖模块加载完准备就绪,才会require模块并执行。
当然,细节远不止此。源码里还有一些比较有趣的模块,如__webpack_require__.l负责以script标签的方式加载脚本、webpackJsonpCallback负责更新本地模块的promsie状态、__webpack_require__.f.j里远程模块的层级调用等,
囿于篇幅有限,无法作做过多深入介绍,有兴趣的朋友,欢迎留言讨论!
最后
本文所涉及的案例已经托管到Github。
如有任何疑惑,欢迎留言讨论,如果本文对你有所帮助,可以点赞转发给更多的人哦。



















![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)