module federation v2原理
前言
我们在实际工作过程中会持续面临以下挑战:
- 由于公司业务需求的高速发展,各项目的模块积累成了一座座“大山”,致使项目的构建速度逐渐变慢
- 项目之间存在许多具有复用潜力的模块,然而,简单的复制粘贴代码可能会提升未来的维护难度和成本
- 在复杂的项目环境中,我们渴望能够将众多项目集成到一体,实现资源的有效共享和缜密复用
在此环境下,module federation应运而生,为我们提供了井然有序的解决方案。
什么是module federation?
模块联合是一种 JavaScript 应用去中心化的架构模式(类似于服务器端的微服务)。它允许您在多个 JavaScript 应用程序(或微前端)之间共享代码和资源。这可以帮助您:
- 减少代码重复
- 提高代码可维护性
- 降低应用程序的整体大小
- 增强应用程序的性能
module federationv1和v2的区别是什么?
Module Federation 2.0 与 Webpack5 内置的 Module Federation 不同,它不仅提供模块导出、加载和依赖共享等核心功能,还提供额外的动态类型提示、Manifest、Federation Runtime 和 Runtime Plugin System。这些特性使得 Module Federation 更适合用作大型 Web 应用程序中的微前端架构。
特点
- 代码共享、依赖复用
- 资源清单(Manifest)
- module federation runtime
- 运行时插件系统(tapable实现)
- 动态类型提示
- Chrome 开发工具
- Rspack 和 Webpack 支持
基本用法
插件用法
我们可以在webpack中使用插件进行配置,配置方式与v1一致,但是相比于v1,v2能够对远程模块的类型声明进行拉取,并放到本地项目中的src/@mf-types目录下,对ts支持友好,并且将expose的loadRemote逻辑抽离到单独的@module-federation/runtime包中,只要使用的构建工具使用这个包实现对应的逻辑,即可使用。
接下来我们来看下实际使用
导出远程组件
在需要导入远程组件的项目(remote1)中安装@module-federation/enhanced
pnpm i @module-federation/enhanced
配置webpack.config.js
// ...
module.exports = {
// ...常用webpack配置
devServer: {
port: 8081,
},
output: {
// ...
publicPath: 'http://localhost:8081/'
},
plugins: [
// ...
new ModuleFederationPlugin({
filename: 'remoteEntry.js',
name: pkg.name,
exposes: {
'./Button': './src/exposes/Button',
},
shared: {
react: {
singleton: true,
eager: true,
requiredVersion: '18.2.0',
},
'react-dom': {
singleton: true,
eager: true,
requiredVersion: '18.2.0',
},
},
}),
],
};
暴露远程Button组件,例如remote1/src/exposes/Button/index.tsx
import { useState } from 'react';
export default function Button() {
const [n, setN] = useState(0);
return (
<div>
<h1>remote1-{n}</h1>
<button onClick={() => setN(n + 1)}>+1</button>
</div>
);
}
这时当我们启动开发环境时,会发现remote1/dist下出现两个文件@mf-types.d.ts和@mf-types.zip
引入远程组件
在需要引入远程组件的项目(host)中安装@module-federation/enhanced
pnpm i @module-federation/enhanced
配置webpack.config.js
// ...
const ModuleFederationPlugin = require('@module-federation/enhanced').ModuleFederationPlugin;
/**
* @type {Configuration}
*/
module.exports = {
// ...常用webpack配置
devServer: {
port: 8080,
},
plugins: [
//...
new ModuleFederationPlugin({
name: pkg.name,
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/exposes/Button',
},
shared: {
react: {
eager: true,
singleton: true,
},
'react-dom': {
eager: true,
singleton: true,
},
},
remotes: {
remote1: 'remote1@http://localhost:8081/remoteEntry.js',
},
}),
],
};
将项目入口文件改为动态引入的方式,例如host/src/index.ts
import('./bootstrap');
编写启动文件host/src/bootstrap.tsx
import App from './App';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root')!);
root.render(<App />);
在host/src/App.tsx中使用远程组件
import React, { Suspense, lazy, useState } from 'react';
import Button from 'remote1/Button';
import ReactComponent from './component/ReactComponent';
import * as styles from './Button.module.css';
export default function App() {
const [n, setN] = useState(0);
return (
<div>
<Suspense fallback="loading">
<Button />
</Suspense>
</div>
);
}
当我们启动开发环境时,会发现多出一个目录host/@mf-types,这就是federation插件帮我们去拉取的类型声明,这时我们在host/tsconfig.json中添加如下配置就可以享受到远程模块的代码提示了
{
"compilerOptions": {
// ...
"paths": {
"*": ["./@mf-types/*"]
}
}
}
运行时用法(v2新增)
module federation v2允许我们在写运行时代码去加载远程模块,但是远程模块的导出还是交给了构建工具基于@module-federation/runtime去实现,下面我们来改造一下
导出远程组件
与插件导出方式保持一致
引入远程组件
修改host/webpack.config.js
const { Configuration } = require('webpack');
const ModuleFederationPlugin = require('@module-federation/enhanced').ModuleFederationPlugin;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const pkg = require('./package.json');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const { resolve } = require('path');
/**
* @type {Configuration}
*/
module.exports = {
// ...常用webpack配置
plugins: [
//...
// new ModuleFederationPlugin({
// name: pkg.name,
// filename: 'remoteEntry.js',
// exposes: {
// './Button': './src/exposes/Button',
// },
// shared: {
// react: {
// eager: true,
// singleton: true, // only a single version of the shared module is allowed
// },
// 'react-dom': {
// eager: true,
// singleton: true, // only a single version of the shared module is allowed
// },
// },
// remotes: {
// remote1: 'remote1@http://localhost:8080/remote1/remoteEntry.js',
// },
// }),
],
};
修改一下host/index.ts
// import('./bootstrap');
import "./bootstrap"
修改host/App.tsx
import pkg from '../package.json';
import React, { ComponentType, Suspense, lazy } from 'react';
import ReactDOM from 'react-dom';
import { init, loadRemote } from '@module-federation/enhanced/runtime';
const host = init({
name: pkg.name,
remotes: [
{
name: 'remote1',
entry: 'http://localhost:8081/remoteEntry.js',
// 加载远程模块时
shareScope: 'default',
},
],
shared: {
react: {
version: '18.2.0',
lib: () => React,
// 指定注册的作用域
scope: 'default',
shareConfig: {
singleton: true,
eager: true,
requiredVersion: '18.2.0',
},
},
'react-dom': {
version: '18.2.0',
lib: () => ReactDOM,
// 指定注册的作用域
scope: 'default',
shareConfig: {
singleton: true,
requiredVersion: '18.2.0',
eager: true,
},
},
},
});
const Button = lazy(() => loadRemote('remote1/Button') as Promise<{ default: ComponentType }>);
export default function App() {
return (
<div>
<Suspense fallback="loading">
<Button />
</Suspense>
</div>
);
}
访问http://localhost:8080我们可以看到效果跟v1是一致的
以上就是module federation的基本用法
module federation原理
其实模块联邦并没有多黑科技,本质上模块联邦就是通过script或dynamic import的方式对远程文件进行引用,并对这种能力进行增强(shareScope共享、shareScope资源复用)。
接下来我们看下这份remote端的webpack.config.json的module federation配置
const { Configuration } = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const pkg = require('./package.json');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const { resolve } = require('path');
const ModuleFederationPlugin = require('@module-federation/enhanced').ModuleFederationPlugin;
/**
* @type {Configuration}
*/
module.exports = {
//...webpack常用配置
plugins: [
new ModuleFederationPlugin({
filename: 'remoteEntry.js',
name: pkg.name,
exposes: {
'./Button': './src/exposes/Button',
},
// 指定接收host端共享的shareScope名称,如果不写则为default
shareScope: 'default',
shared: {
react: {
singleton: true,
eager: true,
// 往本地作用域中名为self的shareScope注册
shareScope: 'self',
},
'react-dom': {
singleton: true,
eager: true,
// 往本地作用域中名为self的shareScope注册
shareScope: 'self',
},
},
}),
],
};
这份配置配置中remote端会在全局共享空间中开辟一份属于自己的空间,并在里面注册一个名为default的scope去承接host端share过来的scope,然后会把自身的react和react-dom注册到指定好的名为self的scope里面,然后自身的react和react-dom就只能消费self scope中的包,所以这就会出现react多实例的报错!
接下来我们再看看host端引用remote的方式
const host = init({
name: pkg.name,
remotes: [
{
name: 'remote',
entry: 'http://localhost:8081/remoteEntry.js',
// 将本地作用域中的`default Scope`共享出去
shareScope: 'default',
},
],
shared: {
react: {
version: '18.3.0',
lib: () => React,
// 往本地作用域中名为default的shareScope注册
scope: 'default',
shareConfig: {
singleton: true,
eager: true,
requiredVersion: '18.3.0',
},
},
'react-dom': {
version: '18.3.0',
lib: () => ReactDOM,
// 往本地作用域中名为default的shareScope注册
scope: 'default',
shareConfig: {
singleton: true,
eager: true,
requiredVersion: '18.3.0',
},
},
},
});
const Button = lazy(() => {
debugger;
return loadRemote('remote1/Button') as Promise<{ default: ComponentType }>;
});
这里host端往自身的本地作用域中开辟了一个名为default的作用域,并将react和react-dom注册进default scope中,随后引入了remote模块,同时将自身的default scope共享给了remote。
我们来看下这张图
在模块联邦中,
host端会创建一个名为federation的实例,shareScope的默认值设为default。然后,host会在全局对象__SHARE__中创建自己的作用范围(scope),如__SHARE__={host: {}}。然后注册react和react-dom,并附加版本号,如__SHARE__={host: {react: {": {...}}, "react-dom": {"18.3.0": {...}}}}。
在加载remote模块时,remote本身也会创建自己federation的实例,由于指定了shareScope: default,host会将自己的default scope分享给remote模块,由于remote中的module federation plugin指定了shareScope: 'default',remote也会使用default来接受host的共享作用域。然后,remote按照相同的步骤将其自己所共享的react、react-dom注册到它自己指定好的的self scope中。
至此remote模块就可以开始消费这些已共享的模块。需要特别注意的是,由于remote模块是将react和react-dom共享到self scope中,因此只能在自己的self scope中消费这些模块。这就导致了host和remote使用的不是同一个react实例。
总结
本文主要讲述了模块联邦的主要概念、v1与v2的区别,以及模块联邦如何去拉取远程模块、以及shareScope是如何运作,大道至简,以最少的代码阐述通熟易懂的概念。