前言
作者:前端下饭菜
原文:https://juejin.cn/post/7427173759713296393
什么是模块联盟(Module Federation)?让 JavaScript 应用间共享代码更加简单,团队协作更加高效。
类似于服务端的微服务,Module Federation
是一种支持前端应用分治的架构模式,它允许你在多个应用或微前端应用之间共享功能级代码,这种方案的好处是:
减少代码重复; 提升代码可维护性; 降低应用程序的整体大小; 提高应用程序的性能;
什么是Module Federation 2.0?
除了老版本已支持的模块导出、模块加载、依赖共享之外,额外支持了:
📝 Manifest:定义 Module Federation
元数据信息;🚀 动态类型提示:使用远端模块时,和引入npm包一样的体验,支持类型提示,并且还支持热更新; 🎨 Module Federation 运行时:支持通过运行时API注册共享依赖、动态注册和加载远程模块; 🧩 运行时插件系统:提供了一套轻量的运行时插件系统,提供各种生命周期钩子,并可修改MF配置; 🛠️ Chrome Devtool:开发调试利器;
目前有哪些构建框架支持Module Federation?
rspack: 需安装插件 @module-federation/enhanced
;webpack: 需安装插件 @module-federation/enhanced
;rsbuild: 需安装插件 @module-federation/rsbuild-plugin
;Vite: 需安装插件 @module-federation/vite
, 不支持dts
、dev
配置选项;
案例
以实现一个共享的计数器为例,在远端remote
项目中实现一个计数器组件,其他host
项目可通过MF
方式引入并使用。
基础框架准备
在host、remote目录下分别按顺序执行以下步骤:
在项目新建目录:
packages host remote
host、remote都使用 npm create rsbuild@latest
创建以Rsbuild、Vue3为基础框架的项目,框架选择使用Vue
、TypeScript
。
通过以上步骤我们建立了host、remote两个可运行的APP,接下来进入正题,开始搭建支持Module Federation
的应用。
安装MF环境
在remote
、host
项目安装Module Federation
的rsbuild插件:
npm add @module-federation/enhanced
npm add @module-federation/rsbuild-plugin --save-dev
使用Vue的tsx
实现组件,因此需要对rsbuild安装支持jsx
的相应插件:
npm add @rsbuild/plugin-vue-jsx @rsbuild/plugin-babel -D
在rsbuild.config.ts
中添加配置:
plugins: [
pluginBabel({
include: /.(?:jsx|tsx)$/,
}),
pluginVue(),
pluginVueJsx(),
],
remote实现共享组件
在components/counter/index.tsx
添加计时器组件代码,和写常规的Vue组件无任何区别。
import { defineComponent } from "vue";
import './index.css';
export default defineComponent({
props: {
count: {
type: Number,
required: true,
},
},
emits: ['increase'],
setup(props: { count: number }, { emit }) {
console.log(props);
return () => ( <button
class="counter-button"
onClick={ () => emit('increase', props.count) }>
Remote counter: { props.count }
</button>)
},
});
组件提供了count属性以及事件increase
。接下来需要在rs.build.ts
中通过module federation
插件将counter
组件以remote方式提供给其他项目。在plugins
下添加pluginModuleFederation
插件配置:
pluginModuleFederation({
name: 'remote',
exposes: {
'./counter': './src/components/counter/index.tsx',
},
}),
以dev方式启动remote
项目,然后访问http://localhost:3001/mf-manifest.json
,查看返回结果。
module federation 2.0
其中的一个特性就是引入minifest
元数据信息,这里返回的正是使用pluginModuleFederation
定义的remote信息。先着重看exposes
列表,每一项即为我们在配置中定义的导出模块,例如 ./counter
对应的就是返回信息中id
为remote:counter
一项 ,其中assets下包含css
、js
属性。
css: 还记得在
components/counter/index.tsx
中的代码有通过import 'index.css'
引入css文件吗?而static/css/async/__federation_expose_counter.css
正是index.css
bundle后的css文件。js: manifest.json包含两个js文件,一个是组件代码,另一个是
Federation
运行时代码;static/js/async/__federation_expose_counter.js
: counter组件bundle后的js文件;static/js/vendors-node_modules_rspack_core_dist_cssExtractHmr_js-node_modules_vue_dist_vue_runtime_esm--6bd317.js
:还记得Module Federation V2.0
提出的Module Federation 运行时
特性吗?正是该文件所包含的运行时代码;
host使用共享组件
在host
项目的rsbuild.config.ts
中,为plugins部分添加pluginModuleFederation
插件配置:
pluginModuleFederation({
name: 'host',
remotes: {
remote: 'remote@http://localhost:3001/mf-manifest.json'
}
})
相比于v1.0,v2.0配置非常简单,仅需指定远端的manifest.json
地址即可,而manifest.json
文件就包含了远端模块或组件的元信息。
在App.vue
文件中引入remote
提供的counter组件并实现交互:
// script
const Counter = defineAsyncComponent(() => import('remote/counter'))
const onInrement = (val: number) => {
count.value = val + 1;
}
// template
<Counter :count="count" @increase="onInrement"></Counter>
代码中使用defineAsyncComponent
异步加载远端组件,这样的好处是安需动态加载,能减少应用包的大小。
虽然现在可以使用远端的remote/counter
组件,但使用过程没有智能提示也是让人头痛。这不Module Federation 2.0
就为我们带来了类型提示
特性。
远端默认开启类型提示
,所以仅需要在Host
侧的tsconfig.json
添加配置,将类型提示
引入到项目。
{
"compilerOptions": {
...
"paths": {
"*": ["./@mf-types/*"]
}
},
}
Host
的Module Federation
插件会将远端的@mf-types
下载下来并放到根目录下的@mf-types
文件夹中。目录结构如下所示:
目录中包含了3种类型定义:
运行时API: V2.0
支持动态加载远端模块,根目录下的index.d.ts
文件就包含了动态加载相关的API定义,具体有@module-federation/runtime
、@module-federation/enhanced/runtime
、@module-federation/runtime-tools
模块的类型定义;远端组件、模块类型:如上文中定义的Counter组件,其类型定义包含在 @mf-types/remote/compiled-types/src/components/counter/index.d.ts
目录下;依赖项类型:由于组件中引用了 Vue
,因此在@mf-types/remote/node_modules
下能看到Vue的类型定义文件;
添加了类型提示
配置后,我们写组件时就能方便的查看到远端组件定义的属性。
MF使用问题
css全局污染
和微前端框架类似,Module Federation
也有CSS全局污染的问题。
在remote
定义的counter组件有使用样式counter-button
,假如host
也同样定义了同名的class counter-button
,并设置background-color: #F00
。运行结果是远端的样式将本地的样式覆盖,也就是说本地的样式被远端样式给污染了,这不是我们想要的结果。
Module Federation
在后续计划有提到sandbox
,如果能为共享模块提供沙箱模式,那css
问题也会迎刃而解。
依赖复用
跨项目消费模块往往会碰到重复依赖加载、依赖单例限制等问题,这些问题可以通过设置 shared
来解决。
重复依赖加载
例如
remote
、host
都在使用loadash,并且lodash包体积比较大。那么可以将其配置到shared
选项,在两个项目的rsbuild.config.ts
同时添加:shared: {
lodash: {
singleton: true,
eager: true,
}
}重新请求
mf-manifest.json
文件,返回内容有增加shared
相关信息。如果host
已经加载过lodash
模块,则不会再从远端请求lodash文件static/js/async/vendors-node_modules_lodash_lodash_js.js
。依赖单例限制
由于react项目只允许单例运行,不能多次加载,这时也需要将其配置到shared中。这种场景,需要在host
、remote
的rsbuild.config.ts
同时配置:
shared: ['react, 'react-dom']
State状态
假如在remote
项目中的组件有使用pinia
创建store,那么在host
使用时会报如下错误:
原因是远端的pinia
实体是在main.ts通过app.use(createPinia())
创建,而直接使用组件时不会走main.ts
流程。
要解决这个问题,需要动态的获取app实体,并在useStore()
之前,保证createPinia
已经执行。可通过以下方式动态获取app实体。
import { getCurrentInstance } from 'vue';
const { appContext } = getCurrentInstance()!;
appContext.app.use(createPinia());
Vite不支持类型提示
@module-federation/vite
目前还不支持dts
配置,也就是不支持Module Federation v2.0
的类型提示
特性。如果想使用类型提示,只能绕道至Webpack
或者Rsbuild
。
manifest.json越来越大
上图为远端打包后的结果,生产环境也包含mf-manifest.json
。远端的模块信息通过mf-manitest.json
提供给应用端,但随着远端提供的模块越来越多并且依赖项也越来越多,manifest.json
会指数级增长,那会不会演变成Modufle Federation
的卡点问题?
总结
Module Federation
的主要应用场景是微前端,可以满足多个微前端之间的代码共享。相比于当前各种微前端
框架,Module Federation
的优势非常明显:
不需要引入一个庞大的微前端架构,也不需要对现有代码做过多改造,微前端相关的逻辑交给构建层(webpack、rsbuild、vite等)统一处理,因此不管是新、老项目,上手成本都比较低;
开发效率高,像多仓库或者发布npm包的方式,一方面开发时代码体量比较大,另一方面发布过程也比耗时间。而
Module Federation
架构,通过网络来共享代码模块,因此模块提供方提供一个url,应用方就可以开始使用,并且体验上和使用npm包完全一致。
🕰️ Module Federation 未来
Module Federation 希望能成为构建大型 Web 应用的一个架构方式,类似后端的微服务。后续计划包括的内容:
提供完善的 Devtool 工具 提供更多的上层框架能力 Router、Sandbox、SSR 提供大型 Web 应用基于 Module Federation 的最佳实践
demo
代码:github.com/cnmapos/mod…[1]
参考
rsbuild-plugin-vue-jsx[2] 何时使用shared[3] Module Federation官网[4]