文章简介:通过例子,对 webpack5 联邦模块 Module Federation 的简单介绍以及使用,主要包括以下内容。基础例子、Module Federation 的参数配置要做到全面了解,其它例子都是在此基础上添加了配置,会更容易掌握。
- Module Federation 的简介
- 基础例子 - 1个 host、1个 remote
- 方向例子 - 既可以是 host 也可以是 remote
- 不同版本 React,Module Federation 的兼容处理
- 不同版本 Lodash,Module Federation 的兼容处理
- 共享 context
- redux 在 host、remote 的注册
- 路由共享
- Module Federation 的 webpack 构建如何工作
1 Module Federation 简单介绍
多个独立的应用可以组成一个应用程序,这些独立的应用可以单独开发以及部署。由此可以想到他的作用,例如:微前端、代码共享、代替 npm 发包等等,Module Federation 学习成本低,相比较更简洁一些。
2 Module Federation 模块介绍
- 模块分为本地模块和远程模块,可以通过 webpack 的配置不同来区分。
- 本地模块是当前构建的一部分,可以看成 host;远程模块不属于当前构建,需要 host 远程加载,称为 romote。
- 一个模块既可以是 host,也可以是 remote。
- 加载远程模块是异步操作,在 chunk 加载期间完成,执行模块是同步的,在本地和远程模块交错执行期间完成。
- 以下例子中:app1 主要为 Host,app2 主要为 Remote。
3 基础例子 - 1个 host、1个 remote
使用方式以及解决的问题
- webpack 的配置
- 远程加载 js、模块
- 例子链接: github.com/module-fede…
项目介绍
- 两个应用 app1(host 端口:3001)、app2(remote 端口:3002)
- webpack 引入
const { ModuleFederationPlugin } = require("webpack").container;
- app2 中暴露了 Button 组件
- app1 中引入远程 remote(app2) 中的 Button 组件
import React from "react";
const RemoteButton = React.lazy(() => import("app2/Button"));
const App = () => (
<div>
<h1>Basic Host-Remote</h1>
<h2>App 1</h2>
<React.Suspense fallback="Loading Button">
<RemoteButton />
</React.Suspense>
</div>
);
export default App;
- 页面呈现
4 webpack 参数配置简介
- name 必须,当前应用的名字,全局唯一ID,通过 name/{expose} 的方式使用
- library 可选,打包方式,与 name 保持一致即可
- filename 可选,打包后的文件名,对应上面的 remoteEntry.js
- remotes 可选,表示当前应用是一个 Host,可以引用 Remote 中 expose 的模块
- exposes 可选,表示当前应用是一个 Remote,exposes 内的模块可以被其他的 Host 引用,引用方式为 import(name/{expose})
- shared 可选,依赖的包(下面包含了 shared 中包含的配置项)
-
如果配置了这个属性。webpack在加载的时候会先判断本地应用是否存在对应的包,如果不存在,则加载远程应用的依赖包。
-
以 app2 来说,因为它是一个远程应用,配置了
["react", "react-dom"]
,而它被 app1 所消费,所以 webpack 会先查找 app1 是否存在这两个包,如果不存在就使用 app2 自带包。 app1里面同样申明了这两个参数,因为 app1 是本地应用,所以会直接用 app1 的依赖。 -
shared 配置项指示 remote 应用的输出内容和 host 应用可以共用哪些依赖。 shared 要想生效,则 host 应用和 remote 应用的 shared 配置的依赖要一致。
-
import 共享依赖的实际的 package name,如果未指定,默认为用户自定义的共享依赖名,即 react-shared。如果是这样的话,webpack 打包是会抛出异常的,因为实际上并没有 react-shared 这个包。
-
singleton 是否开启单例模式,true 则开启。如何启用单例模式,那么 remote 应用组件和 host 应用共享的依赖只加载一次,且与版本无关。 如果版本不一致,会给出警告。不开启单例模式下,如果 remote 应用和 host 应用共享依赖的版本不一致,remote 应用和 host 应用需要分别各自加载依赖。
-
requiredVersion 指定共享依赖的版本,默认值为当前应用的依赖版本。- 如果 requiredVersion 与实际应用的依赖的版本不一致,会给出警告。
-
strictVersion 是否需要严格的版本控制。单例模式下,如果 strictVersion 与实际应用的依赖的版本不一致,会抛出异常。默认值为 false。
-
shareKey 共享依赖的别名, 默认值值 shared 配置项的 key 值。
-
shareScope 当前共享依赖的作用域名称,默认为 default。
-
eager 共享依赖在打包过程中是否被分离为 async chunk。eager 为 false, 共享依赖被单独分离为 async chunk; eager 为 true, 共享依赖会打包到 main、remoteEntry,不会被分离。默认值为 false,如果设置为 true, 共享依赖其实是没有意义的。
-
shareScope 所用共享依赖的作用域名称,默认为 default。如果 shareScope 和 share["xxx"].shareScope 同时存在,share["xxx"].shareScope 的优先级更高。
5 注意事项
- 为什么 index.js 中需要以 import() 的方式引入 bootstrap.js ?
入口文件 index.js 不处理逻辑,逻辑放在 bootstrap.js 中,index.js 动态加载bootstrap.js。将逻辑直接放到 index.js里会报错,原因是 index.js 执行逻辑,会依赖 Remote 暴露的js,此时 remote.js 还未加载,就会有问题(参考官方文档)。
- 在使用 Module Federation 时,Host、Remote 必须同时配置 shared,且一致。
- module federation 是否可以做到与技术栈无关?
与技术栈无关。假设两个应用, host 应用使用 react 技术栈, remote 应用使用 vue 技术栈,host 应用在使用 remote 应用提供的组件时,不能直接使用,需要额外执行 vue.mount('#xxx') 方法,将 remote 组件挂载的指定位置。
- 共享依赖的版本控制
module federation 在初始化 shareScope 时,会比较 host 应用和 remote 应用之间共享依赖的版本,将 shareScope 中共享依赖的版本更新为较高版本。在加载共享依赖时,如果发现实际需要的版本和 shareScope 中共享依赖的版本不一致时,会根据 share 配置项的不同做相应处理:
- 如果配置 singleton 为 ture,实际使用 shareScope 中的共享依赖,控制台会打印版本不一致警告;
- 如果配置 singleton 为 ture,且 strictVersion 为 ture,即需要保证版本必须一致,会抛出异常;
- 如果配置 singleton 为 false,那么应用不会使用 shareScope 中的共享依赖,而是加载应用自己的依赖;
综上,如果 host 应用和 remote 应用共享依赖的版本可以兼容,可将 singleton 配置为 ture;如果共享依赖版本不兼容,需要将 singleton 配置为 false。
- 多个应用(超过 2 个) 是否可共用一个 shareScope ?
假设有这么一个场景, 三个应用 - app1、app2、app3, app2 是 app1 的 remote 应用, app3 是 app2 的 remote 应用, 那么他们是否可共用一个 shareScope ? 使用 module federation 功能以后,所有建立联系的应用,共用一个 shareScope。
6 一个项目既可以是 Host,也可以是 Remote
可以通过 webpack 配置来区分,可以参考例子:github.com/module-fede…
7 不同版本 React,Module Federation 的兼容处理
简单介绍:
- app1 使用的老版本 react
- app2 使用的是新版本 hooks
- 需要做到版本兼容
- app2 webpack 中配置 shared 配置项、exposes 暴露 react
plugins: [
new ModuleFederationPlugin({
name: "app2",
library: { type: "var", name: "app2" },
filename: "remoteEntry.js",
exposes: {
"./Button": "./src/Button",
"./ModernComponent": "./src/ModernReactComponent",
"./newReact": require.resolve("react"),
"./newReactDOM": require.resolve("react-dom"),
},
shared: [
"react-dom",
{
react: {
import: "react", // the "react" package will be used a provided and fallback module
shareKey: "newReact", // under this name the shared module will be placed in the share scope
shareScope: "default", // share scope with this name will be used
singleton: true, // only a single version of the shared module is allowed
},
},
],
}),
- app1 引入 app2 中 shared 中的 react 并初始化 DOM
constructor(props) {
super(props);
this.refHold;
}
init = (hydrate) => {
(async () => {
const ReactDOM = (await import("app2/newReactDOM")).default;
const React = (await import("app2/newReact")).default;
const RemoteComponent = await this.props.importer();
const { importer, children, ...rest } = this.props;
const renderMethod = hydrate ? ReactDOM.hydrate : ReactDOM.render;
renderMethod(
React.createElement(RemoteComponent.default, rest, children),
this.refHold
);
})();
};
componentDidUpdate(prevProps, prevState, snapshot) {
this.init(true);
}
componentDidMount() {
this.init();
}
8 不同版本 Lodash,Module Federation 的兼容处理
简单介绍:
- 不同版本 Lodash,Module Federation 的兼容处理
- 配置 shared 属性
shared: {
react: "react",
"react-dom": "react-dom",
[`lodash-${require("lodash").VERSION}`]: "lodash",
},
9 共享 context
简单介绍:
- 共享 context
- 配置 shared 属性
shared: [
"react",
"react-dom",
{
"@shared-context/shared-library": {
import: "@shared-context/shared-library",
requiredVersion: require("../shared-library/package.json").version,
},
},
],
- 文件引入
10 redux 在 host、remote 的注册
在 app2 中,注册 redux,做到 host、remote 共享,主要方法是在 app1 中定义方法去改写 store,例子链接:github.com/module-fede…
export default function configureStore(initialState) {
const composeEnhancers =
typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose;
const enhancer = composeEnhancers();
const store = createStore(createReducer(), enhancer);
store.asyncReducers = {};
store.injectReducer = (key, asyncReducer) => {
store.asyncReducers[key] = asyncReducer;
store.replaceReducer(createReducer(store.asyncReducers));
};
return store;
}
11 路由共享
在 app1、app2中,把路由对应的文件
exposes
出去即可,例子链接:github.com/module-fede…
12 Module Federation 的 webpack 构建如何工作
Module Federation 主要提供了两个功能:
- 应用间组件互用
- host 应用和 remote 应用组件的依赖共享
Module Federation 原理浅析
-
webpack 在构建过程中,会以 entry 配置项对应的入口文件为起点,收集整个应用中需要的所有模块,建立模块之间的依赖关系,生成一个模块依赖图。然后再将这个模块依赖图,切分为多个 chunk,输出到 output 配置项指定的位置。
-
Module Federation 最后构建内容,main-chunk 和 async chunk。其中, main-chunk 为入口文件(通常为 index.js) 所在的 chunk,内部包含 runtime 模块、index 入口模块、第三方依赖模块(如 react、react-dom、antd 等)和内部组件模块(如 com-1、com-2 等);async-chunk 为异步 chunk,内部包含需要异步加载(懒加载)的模块。
-
打包代码中,webpack_require.l 是一个方法,用于加载 async-chunk。webpack_require.l 会根据 async-chunk 对应的 url,通过动态添加 script 的方式,获取 async-chunk 对应的 js 文件,然后执行。
-
执行入口模块 - index 对应的代码时,1. 如果遇到懒加载模块,通过 webpack_modules.l 方法获取对应的 async-chunk 并执行,然后获取相应的输出。
组件互用的逻辑是怎么实现的呢?
remote 应用生成一个 remoteEntry-chunk 和多个 expose-chunk。 host 应用启动后,通过 remotes 配置项指定的 url,去动态加载 remote 应用的 remoteEntry-chunk 和 expose-chunk,然后执行并渲染 remote 应用的组件。
host 应用和 remote 应用组件的依赖共享
在 一个常见的 webpack 构建如何工作 的构建结果中,我们可以发现多个 webpack 构建之间其实是相互隔离、无法互相访问的。那么 module federation 是如何打破这种隔离的呢? 答案是 sharedScope - 共享作用域。应用启动以后, host 应用和 remote 应用的 remote-chunk 之间会建立一个可共享的 sharedScope,内部包含可共享的依赖。
13 总结
Module Federation 的介绍就到这里了,由于本人水平有限,有些地方可能没有讲解清楚。如果有问题或者哪里错误,欢迎大家留言指正,一起学习,共同进步。