MF(Module Federation)是什么
Module Federation,中文翻译为"模块联邦",webpack的新功能,模块联邦可以在多个 Webpack 编译产物之间共享模块、依赖、页面甚至应用。模块联邦让Webpack达到了线上Runtime的效果,能让代码直接在项目间利用CDN直接共享,引用者不再需要本地安装Npm包、构建再发布。
官方解释: 多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系,因此可以单独开发和部署它们。
简单说,MF 实际上就是可以把多个无单独依赖的、单独部署的应用合为一个,这听着像是微前端。但是它的场景不止是应用,MF 支持的粒度更细,能做到更多事情。
模块共享的方式
NPM
UMD
UMD 是 (Universal Module Definition) 通用模块定义的缩写,UMD 将 commonjs、amd 规范集于一身,并支持导出全局变量使用。
Git SubmitModule
Monorepo
Monorepo 是一种项目代码管理方式,指单个仓库中管理多个项目,可以帮助改善代码共享、简化版本控制并实现更快的开发周期。
Module Federation
主角模块联邦,webpack5的新特性。
MF与微前端的对比
MF 解决的问题其实和微前端有些类似,都是将一个应用拆分成多个子应用,每个应用都可以独立开发、部署,但是他们也有一些区别,比如微前端需要一个中心应用(简称基座)去承载子应用,而 MF 不需要,因为任何一个应用都可以作为中心应用,其次就是 MF 可以实现应用之间的依赖共享。MF也可以成为微前端只不过是模块的粒度以及使用场景不同。
MF实践
三个概念
首先,要理解三个重要的概念:
- Webpack构建(容器):一个独立项目通过Webpack 打包编译而产生资源包。
- Remote(远程模块): 一个暴露模块供其他Webpack构建消费的webpack构建。
- Host(宿主模块):一个消费其他remote模块的Webpack构建。
一言蔽之, 一个Webpack构建可以是Remote(即服务的提供方),也可以是Host(即服务的消费方),也可以同时扮演服务提供者和服务消费者,完全看项目的架构。
项目实战
下面我们来看下A/B的代码
项目A的结构目录如下
├─ appA-*
│ ├─ src
│ │ ├─ components
│ │ │ └─ Example1.jsx
│ │ │ └─ Example2.jsx
│ │ ├─ utils
│ │ │ └─ index.js
│ │ └─ app.jsx
│ ├─ index.js
│ ├─ bootstrap.js
│ ├─ package.json
│ ├─ webpack.config.js
项目B的结构目录如下
├─ appB-*
│ ├─ src
│ │ ├─ components
│ │ │ └─ Exposes.jsx
│ │ └─ app.jsx
│ ├─ index.js
│ ├─ bootstrap.js
│ ├─ package.json
│ ├─ webpack.config.js
项目A、B的差异主要在App.js中import的组件不同,两者的index.js、bootstrap.js都是一样的。
现在我们看看在接入Module Federation之前的Webpack配置。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /.(js|jsx)$/,
exclude: /node_modules/,
use: ['babel-loader']
}
]
},
resolve: {
extensions: ['*', '.js', '.jsx']
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
})
],
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
compress: true,
port: 3367, // appA 3366 | appB 3367
open: false
},
};
配置参数
模块联邦本身是一个普通的 Webpack 插件 ModuleFederationPlugin,该插件有几个重要参数:
- name: 当前应用名,要全局唯一
- remotes: 用于为其他项目的名称指定映射规则
- exposes: 表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用
- shared: 可以让远程加载的模块对应依赖改为使用本地项目资源,需要注意的是主项目的shared集合需要包含远程项目的shared
配置exposes/remotes
现在修改webpack配置,引入ModuleFederationPlugin,让项目B引入项目A的Example1和Example2组件和utils方法,并且也让B组件成为Remote让A消费B的Exposes组件。
// appA的配置
const { ModuleFederationPlugin } = require("webpack").container;
new ModuleFederationPlugin({
// 唯一ID,用于标记当前服务
name: "appA",
// 提供给其他服务器加载的文件
filename: "remoteEntry.js",
// 需要暴露的模块,引用时候通过`${name}/${expose}`引入
exposes: {
"./Example1": "./src/components/Example1",
"./Example2": "./src/components/Example2",
"./utils": "./src/utils/index",
},
remotes: {
appB: "appB@http://localhost:3367/remoteEntry.js",
},
shared: ['react', 'react-dom']
}),
// appB的配置
const { ModuleFederationPlugin } = require("webpack").container;
new ModuleFederationPlugin({
name: "appB",
filename: "remoteEntry.js",
// 引入appA的服务
remotes: {
appA: "appA@http://localhost:3366/remoteEntry.js",
},
exposes: {
'./Exposes': './src/components/exposes'
},
shared: ['react', 'react-dom']
})
修改A/B项目消费引入的组件。
// appA
import React from 'react';
import Exposes from 'appB/Exposes';
export default function App() {
return <h1>
App(A)
<Exposes />
</h1>;
}
// appB
import React, { useEffect } from 'react';
import Example1 from 'appA/Example1'
import Example2 from 'appA/Example2'
import { get } from 'appA/utils' // get方法引用了lodash的get方法
export default function App() {
useEffect(() => { get() }, [])
return <h1>
App(B)
<Example1 count="100" />
<Example2 />
</h1>;
}
其中在B项目没有安装lodash的情况下,B项目直接复用了A项目的lodash,解决了需要重复安装依赖问题。
配置shared
除了前面提到的模块引入和模块暴露相关的配置外,还有个shared配置,主要是用来避免项目出现多个公共依赖。使用ModuleFederation时,要配置shared来避免多个项目出现重复依赖的性能问题。将公共依赖配置到shared中,同时确保两个项目都配置了shared。否则,会导致报错。如果不配置shared,项目A会直接使用自己的依赖。
为什么需要多了bootstrap文件
一般在我们的项目中 index.js 作为入口文件里面应该存放的是 bootstrap.js 中的代码,这里却将代码单独抽离出来放到 bootstrap.js 中,同时在 index.js 中使用import('./bootstrap')来异步加载 bootstrap.js,这是为什么呢?
appB/src/bootstrap.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
appB/src/index.js
import('./bootstrap')
如果 bootstrap.js 不是异步加载的话而是直接打包在 index.js 里面,那么import Example1 from 'appA/Example1'就会被立即执行了,但是此时组件的资源根本没有被下载下来,所以就会报错。
如果我们开启了shared功能的话,那么 import React from 'react'这句被同步执行也会报错,这时候还没有初始化好共享的依赖。
bootstrap.js文件也是官方的推荐做法,总结下来大致就是在通过在index.js异步导入bootstrap.js,确保了在初始化你的应用程序代码前,Webpack有足够的时间加载和设置好所有的共享依赖,如果同步执行Webpack没有机会先加载所有必要的共享模块。这个“等待过程”就像是你先告诉Webpack:“我需要这些共享模块,请先确认它们都加载并准备好了”。
微前端轮子
联邦模块优劣势
优点
- 简化开发流程且仅需要发布一次就能在所有使用后的项目中生效
- 支持运行时加载,无需将共享的代码打包进每一个消费方应用,可以减少打包时的代码体积。
- 能够减少重复第三方依赖。
- 支持在项目中直接导出某个模块,直接单独打包。
缺点
- 由于是 webpack5 的新特性,所以一些 webpack4 的项目要使用的话需要先迁移到 webpack5
- 为了支持加载remote模块对runtime做了大量改造,在运行时要做的事情也陡然增加,会对我们页面的运行时性能造成负面影响。
- 运行时共享也是一把双刃剑,如何去做版控以及控制共享模块的影响是需要去考虑的问题。
- 远程模块TS类型提示也是需要考虑的问题。
结语&&参考
- MF有很多想象空间,值得继续探索和留意。但它并不是万金油,还是需要结合场景去选择。