一.为什么会出现Module Federation
我们用一个图片说明一下关键点
- 每个应用块由不同的组开发
TeamA TeamB
- 应用或应用块共享其他其他应用块或者库
React
- Module Federation的动机是为了不同开发小组间共同开发一个或者多个应用
- 应用将被划分为更小的应用块,一个应用块,可以是比如头部导航或者侧边栏的前端组件,也可以是数据获取逻辑的逻辑组件
什么是模块联邦?
我们再用一张图说明一下容器
和主机(host)
的关系
- 使用模块联邦,每个部分将是一个单独的构建, 这些构建被编译为
容器
- 容器可以被应用程序或其他容器引用
- 在这种关系中,容器是
远程
的,容器的使用者是主机
远程
可以将模块公开给主机
主机
可以使用此类模块,它们被称为远程模块
- 通过使用单独的构建,我们可以获得整个系统的良好构建性能
- 一个被引用的容器被称为
remote
, 引用者被称为host
,remote
暴露模块给host
,host
则可以使用这些暴露的模块,这些模块被称为remote
模块
我们来实际跑起来
我们还有一张图来表示我们的项目之间的关系
我们做一个双向依赖的项目,remote
本地list
组件,依赖host
的from
组件,host
本地from
组件,依赖remote
的list
组件。他们共同依赖react
和react-dom
配置参数
字段 | 类型 | 含义 |
---|---|---|
name | string | 必传值,即输出的模块名,被远程引用时路径为{expose} |
library | object | 声明全局变量的方式,name为umd的name |
filename | string | 构建输出的文件名 |
remotes | object | 远程引用的应用名及其别名的映射,使用时以key值作为name |
exposes | object | 被远程引用时可暴露的资源路径及其别名 |
shared | object | 与其他应用之间可以共享的第三方依赖,使你的代码中不用重复加载同一份依赖 |
安装
npm i webpack webpack-cli webpack-dev-server babel-loader @babel/core @babel/preset-env html-webpack-plugin react react-dom -D
host项目的配置
webpack.config.js
let path = require("path");
let webpack = require("webpack");
let HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
publicPath: "http://localhost:3001/",
clean: true,
path: path.join(__dirname, 'dist')
},
devServer: {
port: 3001,
static: {
directory: path.join(__dirname, 'dist'),
},
},
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
options: {
presets: ["@babel/preset-react"]
},
},
exclude: /node_modules/,
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new ModuleFederationPlugin({
filename: "hostEntry.js",
name: "host",
exposes: {
"./From": "./src/From",
},
remotes: {
remote: "remote@http://localhost:3000/remoteEntry.js"
}
})
]
}
我们主要讲一下new ModuleFederationPlugin()
的操作,主要是暴露一个模块./From
指向我们本地的From
组件,同时加载一个远程的模块remote@http://localhost:3000/remoteEntry.js
App.jsx
import React from "react";
import From from './From';
const RemotList = React.lazy(() => import("remote/List"));
const App = () => (
<div>
<h2>host项目</h2>
<From />
<React.Suspense fallback="LoadingList">
<RemotList />
</React.Suspense>
</div>
);
export default App;
import
语法webpack
是会自动转移成自己的webpack__require__
去加载远程模块的。
remote项目的配置
let path = require("path");
let webpack = require("webpack");
let HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
publicPath: "http://localhost:3000/",
clean: true,
path: path.join(__dirname, 'dist')
},
devServer: {
port: 3000,
static: {
directory: path.join(__dirname, 'dist'),
},
},
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
options: {
presets: ["@babel/preset-react"]
},
},
exclude: /node_modules/,
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new ModuleFederationPlugin({
filename: "remoteEntry.js",
name: "remote",
exposes: {
"./List": "./src/List",
},
remotes: {
remote: "host@http://localhost:3001/hostEntry.js"
}
})
]
}
Remot项目App主入口
import React from "react";
import List from './List';
const HostFrom = React.lazy(() => import("remote/From"));
const App = () => (
<div>
<h2>Remot项目</h2>
<List />
<React.Suspense fallback="LoadingList">
<HostFrom />
</React.Suspense>
</div>
)
export default App;
页面展示
Module Federation 在微前端的用法
之前我们在为什么需要微前端中讲过目前国内微前端方案大概分为:
- 基座模式:通过搭建基座、配置中心来管理子应用。如基于
SIngle Spa
的偏通用的qiankun方案,也有基于本身团队业务量身定制的方案。 - 自组织模式: 通过约定进行互调,但会遇到处理第三方依赖等问题。
- 去中心模式: 脱离基座模式,每个应用之间都可以彼此分享资源。如基于
Webpack 5 Module Federation
实现的EMP微前端方案,可以实现多个应用彼此共享资源分享。
那么基于Webpack 5 Module Federation
的微前端的解决方案,旧的项目的迁移成本就比较高了。对于一个大型的工程来说,需要进行按照项目目录或者功能点的拆分的时候,可能基座模式模式成本更小、更易上手。而去中心模式的优势在于,过多的项目同时有大量的公用性组件或者逻辑时,Module Federation
便能很好的彼此共享资源。
EMP 微前端方案
不啰嗦,我们直接上成果图
使用react-base
去加载了reace-project
的hello
组件,这里大家可以直接看EMP微前端方案,但是我看文档不是很全,vue的项目对接方案是缺失的。
下面是配置emp的关键点:
我们对比react-base和react-project项目的配置,看到react-base在emp.config.js配置文件中声明分享了一个Hello组件:
exposes: {
// 别名:组件的路径
'./components/Hello': 'src/components/Hello',
},
react-project在emp.config.js配置文件中声明需要使用react-base项目的Hello组件:
remotes: {
// 远程项目别名:远程引入的项目名
'@emp/react-base': 'empReactBase',
},
并且react-project在项目代码中,引入使用了react-base项目的Hello组件:
import HelloDEMO from '@emp/react-base/components/Demo'
const App = () => (
<>
// ...
<HelloDEMO />
// ...
</>
)
那么,我们跟着指令分别跑起react-base和react-project项目就可以看到上面的成果图了:
cd react-base && yarn && yarn dev
cd react-project && yarn && yarn dev
总结一下
- Module Federation对于无限嵌套支持较好
- Module Federation对于老项目不太友好,需要升级对应的webpack
- 与
single-spa
一样,不支持js沙箱,需要自己实现 - 第一次需要将引用的依赖前置,会导致加载时间变长的问题
- 对于大型拆分应用不太友好,对于多应用共享很友好。
写在最后:下一篇我们将讲single-spa的玩法《微前端从零到剖析qiankun源码 -- single-spa玩法!》