彻底搞懂 Module Federation(上):概述与实战
本文是《彻底搞懂 Module Federation》系列的第1篇,介绍 Module Federation 的基本概念和实战示例。
📚 系列文章
- ✅ 第1篇(本文):概述与实战示例
- 📖 第2篇:原理分析 - Webpack 异步加载流程
- 📖 第3篇:原理分析 - MF 模块加载(上)
- 📖 第4篇:原理分析 - MF 模块加载(下)
- 📖 第5篇:原理分析 - Runtime API + 项目实操
1. 概述
1.1 基本概念
MF是在webpack5出来后提出来的新概念,解决模块级别复用问题。简单分为两种应用,
- 生产者(Provider),通过Module federation构建插件设置exposes暴露内部模块给其他应用使用;一个remote仓库。
- 消费者(Consumer),同样通过插件设置remotes消费其他生产者的模块。消费者同样可以作为生产者。
1.2 接入方案
1.2.1 介绍
老版本的mf无法在webpack低版本等不支持module federation的构建插件中消费远程模块,而且导出模块和消费模块都是纯构建行为,加载过程被构建工具插件封装,只需要在代码中引入远程模块进行消费即可。
对原本项目的构建模式要求比较高。所以Module Federation2.0提出了Federation Runtime方法。提供高级Api在代码中动态引入消费远程模块,不受构建框架限制。具体的共享依赖复用、远程模块加载等行为全都封装到Runtime中。
目前Module Federation提供两种注册模块和加载模块的方式:
-
一种是在构建插件中声明(一般是在
module-federation.config.ts文件中声明) -
另一种方式是直接通过
runtime的 api 进行模块注册和加载。
| 运行时注册模块 | 插件中注册模块 |
|---|---|
| 可脱离构建插件使用,在 webpack4 等项目中可直接使用纯运行时进行模块注册和加载 | 构建插件需要是 webpack5 或以上 |
| 支持动态注册模块 | 不支持动态注册模块 |
| 不支持 import 语法加载模块 | 支持 import 同步语法加载模块 |
| 支持 loadRemote 加载模块 | 支持 loadRemote 加载模块 |
| 设置 shared 必须提供具体版本和实例信息 | 设置 shared 只需要配置规则即可,无须提供具体版本及实例信息 |
| shared 依赖只能供外部使用,无法使用外部 shared 依赖 | shared 依赖按照特定规则双向共享 |
| 可以通过 runtime 的 plugin 机制影响加载流程 | 目前不支持提供 plugin 影响加载流程 |
| 不支持远程类型提示 | 支持远程类型提示 |
1.2.2 构建时接入
在构建工具对应的配置项中,增加module-federation插件配置
1.2.2.1 消费者配置
1.2.2.1.1 webpack构建配置
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = (env = {}) => ({
mode: 'development',
cache: false,
...
plugins: [
new ModuleFederationPlugin({
name: 'layout',
filename: 'remoteEntry.js',
remotes: {
home: 'home@http://localhost:3002/remoteEntry.js',
},
exposes: {},
shared: {
vue: {
singleton: true,
},
},
}),
],
})
1.2.2.1.2 页面引入
const Content = defineAsyncComponent(() => import('home/Content'));
const Button = defineAsyncComponent(() => import('home/Button'));
1.2.2.2 生产者webpack配置
这里生产者通过配置exposes导出了Content和Button两个组件。
1.2.2.2.1 webpack构建配置
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = (env = {}) => ({
mode: 'development',
cache: false,
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
new ModuleFederationPlugin({
name: 'home',
filename: 'remoteEntry.js',
remotes: {
home: 'home@http://localhost:3002/remoteEntry.js',
},
exposes: {
'./Content': './src/components/Content',
'./Button': './src/components/Button',
},
shared: {
vue: {
singleton: true,
},
},
}),
],
});
1.2.3 Runtime接入
对于消费者提供了js api进行模块注册和模块加载,可以脱离构建插件使用,在 webpack4 等项目中可直接使用纯运行时进行模块注册和加载。生产者还是用对应的构建插件进行配置,需要单独打包出remoteEntry.js入口文件
1.2.3.1 核心API
1.2.3.1.1 Init
- 创建运行时实例,它可以重复调用,但只存在一个实例。若想动态注册远程模块或插件,请使用 registerPlugins 或 registerRemotes
// 可以只使用运行时加载模块,而不依赖于构建插件
// 当不使用构建插件时,共享的依赖项不能自动设置细节
import { init, loadRemote } from '@module-federation/enhanced/runtime';
init({
name: '@demo/app-main',
remotes: [
{
name: "@demo/app1",
// mf-manifest.json 是在 Module federation 新版构建工具中生成的文件类型,对比 remoteEntry 提供了更丰富的功能
// 预加载功能依赖于使用 mf-manifest.json 文件类型
entry: "http://localhost:3005/mf-manifest.json",
alias: "app1"
},
{
name: "@demo/app2",
entry: "http://localhost:3006/remoteEntry.js",
alias: "app2"
},
],
});
// 使用别名加载
loadRemote<{add: (...args: Array<number>)=> number }>("app2/util").then((md)=>{
md.add(1,2,3);
});
1.2.3.1.2 loadRemote
- 用于加载初始化的远程模块,当与构建插件一起使用时,它可以通过原生的
import("remote name/expose")语法直接加载,并且构建插件会自动将其转换为loadRemote("remote name/expose")用法。
import { init, loadRemote } from '@module-federation/enhanced/runtime';
init({
name: '@demo/main-app',
remotes: [
{
name: '@demo/app2',
alias: 'app2',
entry: 'http://localhost:3006/remoteEntry.js',
},
],
});
// remoteName + expose
loadRemote('@demo/app2/util').then((m) => m.add(1, 2, 3));
// alias + expose
loadRemote('app2/util').then((m) => m.add(1, 2, 3));
1.2.3.1.3 registerRemotes
- 用于在初始化后注册远程模块.
function registerRemotes(remotes: Remote[], options?: { force?: boolean }) {}
type Remote = (RemoteWithEntry | RemoteWithVersion) & RemoteInfoCommon;
interface RemoteInfoCommon {
alias?: string;
shareScope?: string;
type?: RemoteEntryType;
entryGlobalName?: string;
}
interface RemoteWithEntry {
name: string;
entry: string;
}
interface RemoteWithVersion {
name: string;
version: string;
}
1.2.3.1.4 registerPlugins
- 用于在初始化后注册远程插件.
import { registerPlugins } from '@module-federation/enhanced/runtime'
import runtimePlugin from 'custom-runtime-plugin.ts';
registerPlugins([runtimePlugin()]);
1.3 调试工具
1.3.1 安装Module Federation插件
chromewebstore.google.com/detail/modu…
1.3.2 插件提供了Devtools面板
1.3.3 查看远程依赖关系
2. 实战示例
介绍一下 简单的host-remote模式开发,及开发体验
官方发布的module-federation-examples,包含了很多不同构建框架之间结合使用的场景,
这里我们主要拿一个最简单的demo查看使用,依赖关系简单,方便对照分析后面的原理解析部分。
module-federation-examples/vue3-demo at master · module-federation/module-federation-examples
2.1 项目目录
项目可以拆分成两个文件夹
- home: 生产者,暴露出来Content和Button组件给外部使用,同时配置了共享库vue,设置singleton: true
- Layout: 消费者,也就是host,消费home项目暴露出来的Content和Button,也配置了vue共享库
vue3-demo/
├── home(remote)
│ ├── src
│ │ ├── App.vue -- 入口组件 components: { Content: defineAsyncComponent(() => import('./components/Content')),
│ │ ├── components
│ │ │ ├── Button.js。 -- 业务组件Button
│ │ │ └── Content.vue -- 业务组件Content
│ │ ├── index.js. -- import('./main.js');
│ │ └── main.js -- const app = createApp(App);app.mount('#app');
│ └── webpack.config.js -- expose:{Content, Button}
│
├── layout(host)
│ ├── src
│ │ ├── Layout.vue -- 使用业务组件Content,
│ │ ├── index.js -- import('./main.js');
│ │ └── main.js -- const Content = defineAsyncComponent(() => import('home/Content'));
│ └── webpack.config.js -- remotes:{home: 'home@http://home.com/remoteEntry.js'}
└── package.json
2.2 核心代码块
2.2.1 Remote
2.2.1.1 src/index.js
// https://webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption
import('./main.js');
2.2.1.2 src/main.js
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.mount('#app');
2.2.1.3 webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = (env = {}) => ({
target: 'web',
entry: path.resolve(__dirname, './src/index.js'),
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
new ModuleFederationPlugin({
name: 'home',
filename: 'remoteEntry.js',
remotes: {
home: 'home@http://localhost:3002/remoteEntry.js',
},
exposes: {
'./Content': './src/components/Content',
'./Button': './src/components/Button',
},
shared: {
vue: {
singleton: true,
},
},
}),
],
devServer: {
port: 3002,
},
});
2.2.2 Host
2.2.2.1 src/index.js
// https://webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption
import('./main.js');
2.2.2.2 src/main.js
import { createApp, defineAsyncComponent } from 'vue';
import Layout from './Layout.vue';
const Content = defineAsyncComponent(() => import('home/Content'));
const Button = defineAsyncComponent(() => import('home/Button'));
const app = createApp(Layout);
app.component('content-element', Content);
app.component('button-element', Button);
app.mount('#app');
2.2.2.3 wepback.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = (env = {}) => ({
entry: path.resolve(__dirname, './src/index.js'),
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
new ModuleFederationPlugin({
name: 'layout',
filename: 'remoteEntry.js',
remotes: {
home: 'home@http://localhost:3002/remoteEntry.js',
},
exposes: {},
shared: {
vue: {
singleton: true,
},
},
}),
],
devServer: {
port: 3001,
},
});