# Module Federation
Module Federation 实现多个应用(app)或者构建(build)在 运行时 共享模块。这些应用(app)或者构建(build)可以单独开发、单独部署。
可以把 Module Federation 看作是实现微前端的一种方式。
# 优势
- 技术栈无关
- 独立仓库、独立开发、独立部署
- 增量升级
- 模块按需加载
# 总览
Container(Remote) 暴露 modules 供 Container Reference(Host) 运行时加载使用。
Container Reference 和 Container 通过 share scope
共享他们依赖的模块,使用时需要判断共享模块的版本,只有符合版本要求时才能使用。
模块通过异步加载,只有在使用的时候才会加载对应的模块,在未加载时只存储对应的模块信息。
# 名称解释
Remote
一个 Webpack 构建,提供运行时共享模块。
Host
一个 Webpack 构建,使用 Remote 提供的共享模块。
Container
每个构建都是一个容器,每个容器都可以加载其它容器暴露出来的模块。
Bidirectional - Host
一个 Webpack 构建即可以是 Host 也可以是 Remote。
# 配置
我这里用的是 Vue-CLI 5.0+ 和 Vue 2.0+
# Remote
const { defineConfig } = require('@vue/cli-service')
const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin
module.exports = defineConfig({
publicPath: 'auto',
configureWebpack: {
optimization: {
splitChunks: false
},
plugins: [
new ModuleFederationPlugin({
name: 'remote',
filename: 'remoteEntry.js',
exposes: {
'./HelloWorld': './src/components/HelloWorld.vue',
'./AboutView': './src/components/AboutView.vue'
},
shared: {
vue: {
singleton: true
}
}
})
]
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
关于 splitChunks 的配置请看下文
# Host
const { defineConfig } = require('@vue/cli-service')
const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin
module.exports = defineConfig({
configureWebpack: {
plugins: [
new ModuleFederationPlugin({
name: 'host',
filename: 'remoteEntry.js',
remotes: {
remote: 'remote@http://localhost:9001/remoteEntry.js',
},
shared: {
vue: {
singleton: true
}
}
})
]
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 使用
首先在 Host 中正常使用 Modele Federation,我们需要修改入口文件,参考 官方文档 (opens new window)
创建 bootstrap.js 文件,并将入口文件的内容放到里面,然后将 bootstrap 引入到入口文件中
bootstrap.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
2
3
4
5
6
7
8
9
10
11
12
main.js
import('./bootstrap');
然后使用 Remote exposed 的模块
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<HelloWorld msg="Hello Module Federation" />
</div>
</template>
<script>
export default {
name: 'HomeView',
components: {
HelloWorld: () => import('remote/HelloWorld')
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如果想处理加载状态,可以使用下面这种 方式 (opens new window)
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})
2
3
4
5
6
7
8
9
10
11
12
13
运行效果:
# 加载过程
打开 chrome 的调试,可以查看 Host 加载 exposed 模块的过程
Module Federation 的加载过程如下:
这里比较奇怪的是加载 vue_runtime_esm_js.js
(即 shared library)是从 remote 中加载的。为此我还特意问了作者原因,issue#2065 (opens new window),作者是这么说的
Zack Jackson: Its loaded based on semver rules and whoever has the best version to serve the needs of the apps.
Webpack decides, its not always the host that loads the shared dep and usually has no perf hit since either way its an async chunk that needs to be loaded, either from host build or another build. But its a network request either way.
# Vue In React
相同框架使用 Remote exposed 的组件比较简单,但是如果跨不同的框架就比较麻烦了,比如在 React 中使用 Vue,因为在 React 不识别 Vue 的组件,所以就无法挂载 Vue 的组件,解决办法就是在 Remote (Vue) 的工程里,封装挂载 Vue 组件的方法。
# Remote (Vue)
// src/components/Mount.js
import Vue from "vue";
export { default as HelloWorld } from "./HelloWorld";
export const mount = (component, props, el) => {
const app = new Vue({
render: h =>
h(component, {
props: props
})
}).$mount(el);
return () => {
app.$destroy();
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
创建一个 Vue 实例,并挂载在 el
上,返回一个函数,执行这个函数卸载新建的 Vue 实例。最后修改 exposes
。
exposes: {
"./ComponentMount": "./src/components/Mount.js"
}
2
3
# Host (React)
加载 Remote 的 mount
方法,封装一个高阶组件
// src/components/ComponentMount.js
import { mount } from "remote/RemoteMount";
import React, { useRef, useEffect } from "react";
const VueComponent = component => props => {
const ref = useRef(null);
useEffect(() => {
const destroy = mount(component, props, ref.current);
return () => {
destroy();
};
}, []);
return (
<div className="vue-component-container">
<div ref={ref}></div>
</div>
);
};
export default VueComponent;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在 React 组件挂载时,调用 Vue 的 mount
方法,挂载 Vue 组件;在 React 组件卸载时,调用 Vue 的 destroy
方法,卸载 Vue 组件。
最后在 Host (React) 工程里使用这个 VueComponent 组件
import { HelloWorld } from "remote/RemoteMount";
const HelloWorldComponent = VueComponent(HelloWorld);
const App = () => {
return (
<div className="app">
<HelloWorldComponent />
</div>
)
}
2
3
4
5
6
7
8
9
10
还有一点需要注意的是事件,Vue 通过 emit
发送事件,但是 React 不支持,所以要想 React 接收到 Vue 事件,需要通过 props 传递事件函数。
# React in Vue
参考官方 react-in-vue example (opens new window)
# 共享路由
共享路由是很多架构实现微前端的方式,使用 Module Federation 有两种方式来共享路由。第一种方式共享路由组件,然后在 Host 定义路由,例如
const routes = [
{
path: '/about',
name: 'about',
component: () => import('remote/AboutView.vue'),
},
];
2
3
4
5
6
7
第二种方式共享路由数组,然后加入 Host 路由中,例如
# Remote
// src/router/shareRoutes.js
const routes = [
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue')
}
]
export default routes
2
3
4
5
6
7
8
9
10
// vue.config.js
new webpack.container.ModuleFederationPlugin({
name: 'remote',
filename: 'remoteEntry.js',
exposes: {
'./RemoteRoutes': './src/router/shareRoutes.js'
},
shared: {
vue: {
singleton: true
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
# Host
// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
...
]
const router = new VueRouter({
routes
})
import('remote/RemoteRoutes').then(res => {
router.addRoutes(res.default)
})
export default router
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Remote 配置问题
# publicPath
publicPath 必须设置为 auto
,或者 URL 绝对地址,例如 http://localhost:9001
。否则加载不到组件
[Vue warn]: Failed to resolve async component: () => webpack_require.e(/*! import() / "webpack_container_remote_remote_HelloWorld").then(webpack_require.t.bind(webpack_require, /! remote/HelloWorld */ "webpack/container/remote/remote/HelloWorld", 23)) Reason: ChunkLoadError: Loading chunk src_components_HelloWorld_vue failed. (error: http://localhost:9000/js/src_components_HelloWorld_vue.js) while loading "./HelloWorld" from webpack/container/reference/remote
从错误信息中,我们可以看出如果不设置 Remote 的 publicPath
,Module Federation 将从 Host 域里去加载 HelloWorld 组件,当然是找不到的。
# optimization.splitChunks
用 Vue-CLI 脚手架搭建的项目,需要修改 optimization.splitChunks
,否则报错
[Vue warn]: Failed to resolve async component: () => webpack_require.e(/*! import() / "webpack_container_remote_remote_HelloWorld").then(webpack_require.t.bind(webpack_require, /! remote/HelloWorld */ "webpack/container/remote/remote/HelloWorld", 23)) Reason: ScriptExternalLoadError: Loading script failed. (missing: http://localhost:9001/remoteEntry.js) while loading "./HelloWorld" from webpack/container/reference/remote
个人推测 Module Federation 与 Vue-CLI 的 optimization.splitChunks
配置有冲突。
下面是 Vue-CLI 的 optimization.splitChunks
配置
使用命令
vue inspect > output.js
optimization: {
splitChunks: {
cacheGroups: {
defaultVendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
解决办法
方法一:禁止 splitChunks
optimization: {
splitChunks: false
}
2
3
此方式导致 build 出两个包含 Vue 的 chunk。
方法二:删除 Vue-CLI splitChunks
的配置
chainWebpack: config => {
config.optimization.delete('splitChunks')
}
2
3
这将使用 webpack 的 splitChunks 默认配置 (opens new window)。
optimization: {
splitChunks: {
chunks: 'async',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
方法三:将 Vue-CLI splitChunks
的配置里的 chunks: 'initial'
改为 chunks: 'async'
optimization: {
splitChunks: {
cacheGroups: {
defaultVendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'async'
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: -20,
chunks: 'async',
reuseExistingChunk: true
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
难道 Module Federation 依赖 async chunk? 关于 all
、initial
、async
的区别请参考 stackoverflow (opens new window)。
# optimization.runtimeChunk
同样 runtimeChunk
也不能用,或者说如果使用了,需要额外的操作,比如手动加载 remote 的 runtime chunk,可以参考这篇文章 Module Federation with Webpacker/Rails host resulting in ScriptLoadError #1116 (opens new window),但是具体怎么做不得而知,因为 build 的 runtime chunk 文件名是不确定的 。
Zack Jackson: You'd have to load the runtime chunk & the remote entry since setting it to single removes all the shared runtime code from the remote file.
# 注意事项
共享的模块因为是运行在 Host 环境,所以 不能 访问 Remote 的全局变量或函数、全局组件、Vuex store、全局样式等涉及 app 全局的东西。相反共享的模块访问的是 Host 的全局变量或函数、全局组件、Vuex store、全局样式等。
注意跨域问题,共享的模块运行在 Host 环境,接口请求是从 Host 环境发出的。接口的服务器要添加 Host 所在的域名。
共享的模块可以访问 Host 的 localStorage 和 sessionStorage。
# 遗留问题
# optimization.runtimeChunk
Module Federation 怎么兼容 optimization.runtimeChunk: "single"
?
我想到同时设置 optimization.runtimeChunk: "single"
和 ModuleFederationPlugin 的 runtime: false
。经测试这样设置运行没有问题,build 生成了 runtime-xxxx.js 文件,同时 remoteEntry.js 也包含了运行时代码,确保能加载 exposed 模块。 但是这样设置 runtimeChunk 是否起作用?为此我还特意问了作者这个问题 issue#1116 (opens new window),作者回答说不起作用。
Zack Jackson: RuntimeChunk single doesn’t work. You’d want to make it false or undefined
另外官方的 例子 (opens new window) 运行失败。
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
runtime: 'my-runtime-name',
}),
],
};
2
3
4
5
6
7
8
concat-runtime (opens new window) 说是能解决这个问题,但是我试了一下好像不起作用。
# 为什么从 Remote 中加载共享库?
从加载过程来看,Module Federation 从 Remote 环境加载共享库(Vue),我理解应该从 Host 环境加载共享库,只有当 Host 没有 Remote 使用的共享库时,才从 Remote 环境加载,这样才能获得更好的性能。为此我还特意问了作者原因 issue#2065 (opens new window),作者是这么说的
Zack Jackson: Its loaded based on semver rules and whoever has the best version to serve the needs of the apps.
Webpack decides, its not always the host that loads the shared dep and usually has no perf hit since either way its an async chunk that needs to be loaded, either from host build or another build. But its a network request either way.
意思是从哪里加载无所谓,反正都是加载 chunk 的异步网络请求。
# Demo
module-federation-demo (opens new window)
# References
- Module Federation (opens new window)
- Webpack - Module Federation (opens new window)
- ModuleFederationPlugin (opens new window)
- Webpack 5 Module Federation: A game-changer in JavaScript architecture (opens new window)
- Webpack 5 and Module Federation - A Microfrontend Revolution (opens new window)
- https://github.com/webpack/webpack/issues/10352
- Webpack5 跨应用代码共享 - Module Federation (opens new window)
- How to add Vue (or another framework) to your React Micro Frontend (opens new window)
- React-Vue Micro-Frontend Application using Webpack 5 Module Federation (opens new window)
- 一文通透讲解webpack5 module federation (opens new window)
- module-federation-examples (opens new window)
- mfe-react-vue-module-federation-example (opens new window)
# Module Federation splitChunks issues
Module Federation with Webpacker/Rails host resulting in ScriptLoadError #1116 (opens new window)