React
项目的技术栈是React+Vite,模块联邦虽然是webpack5特性,但是vite有对应的插件支持:@originjs/vite-plugin-federaion ,可以在免于webpack配置的情况下实现模块联邦。
host配置
php
export default defineConfig({
plugins: [
react(),
federation({
name: 'app',
remotes: {
remoteApp: 'http://localhost:5001/assets/remoteEntry.js',
},
shared: ['react','react-dom']
})
],
build: {
modulePreload: false,
target: 'esnext',
minify: false,
cssCodeSplit: false
}})
remote配置
php
export default defineConfig({
plugins: [
react(),
federation({
name: "remote_app",
filename: "remoteEntry.js",
exposes: {
'./Button': './src/components/Button',
'./Bootstrap': './src/components/Bootstrap'
},
shared: ['react','react-dom']
})
],
build: {
modulePreload: false,
target: 'esnext',
minify: false,
cssCodeSplit: false
}})
这里其实已经就是vite-plugin-federaion提供的示例用法了,防止示例后续被删除,这里做个记录。通常来说这么配置是已经足够实现模块联邦了,只要记住共享模块的版本号必须一致,一般都不会有问题,其实monorepo还是比较适合的,毕竟共用就不用操心一致的问题了。
示例1(静态导入)
javascript
import reactLogo from './assets/react.svg'
import './App.css'
import Button from 'remoteApp/Button';
function App() {
return (
<div className="App">
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" className="logo" alt="Vite logo" />
</a>
<a href="https://reactjs.org" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<Button />
<p>
Edit <code>src/App.jsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</div>
)
}
export default App
但是不通常的情况出现了,可以看到上面贴的代码都是静态导入,但是实际上,我希望实现的是动态导入:即宿主程序是运行时才知道要导入哪些、哪里、哪几个远程程序的模块。当我把代码改成这样时,第一个悲剧出现了
悲剧代码
javascript
function App() { const url = 'http://localhost:5001/assets/remoteEntry.js' const loadRemoteComponents =async ()=>{ try { const module = await import(/* @vite-ignore */ url); const bootstrap = await module.get('./Bootstrap'); const exportedModule = bootstrap(); exportedModule() } catch (error) { console.error('Error loading remote module:', error); } }
与悲剧 Cannot read properties of null (reading 'useState')
期间尝试了无数次痛骂gpt、对比依赖版本号、直接复制node_modules、debug源码,终于确认了根源是宿主程序与远程程序没有共用同一个React实例,但是问题并不出在配置与依赖版本上,而是因为动态导入。如果直接使用静态导入的import,则两个程序是共用一个实例,但走动态的import时,就会重新创建一个react实例。在实际开发过程中,我是先写angular再写react,angular版本的mf有提供一个loadRemoteModule,可以丝滑动态载入远程项目,但是我在vite-plugin-federation没有找到类似的导出方法。
查看了github上的issue,有几个开启了很久都没有close的相似issue,直到看到了另一种看着就很炸裂的写法,最后捣鼓出来了可用版本
示例2(运行时动态导入)
javascript
import {__federation_method_setRemote, __federation_method_getRemote, __federation_method_unwrapDefault} from 'virtual:__federation__'
function App() {
const loadRemoteComponents =async ()=>{
try { // 设置远程模块的配置
__federation_method_setRemote("remoteModule", {
url: () => Promise.resolve("http://localhost:5001/assets/remoteEntry.js"),
format: "esm",
from: "vite",
});
// 获取远程模块
const moduleWrapped = await __federation_method_getRemote("remoteModule", "./Bootstrap");
// 解包默认导出的模块
const bootstrap = await __federation_method_unwrapDefault(moduleWrapped);
bootstrap();
} catch (error) {
console.error('Error loading remote module:', error);
}
};
}
至此,完成Vite+React上项目的MF落地
Angular
我用的angular-cli搭建的angular项目,配置webpack时也相当顺滑
准备工作
安装插件npm i @angular-architects/module-federation@15 -D
导入webpack自定义配置插件@angular-builders/custom-webpack
修改angular.json,使webpack自定义配置能生效,比如"builder": "@angular-builders/custom-webpack:dev-server"
远程项目需要在angular.json的serve中加入
"options": { "port": 5555, "publicHost": "http://localhost:5555" }
宿主配置
php
//webpack.config.ts
const mf = require('@angular-architects/module-federation/webpack')
const path = require('path')
const sharedMappings = new mf.SharedMappings()
sharedMappings.register(path.join(__dirname, '../../tsconfig.json'),
[ /* mapped paths to share */])
module.exports = mf.withModuleFederationPlugin({
name: 'main',
filename: 'main.js',
shared: {
...mf.shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' })
}})
远程配置
php
import { environment } from "projects/core/src/environments/environment";
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const mf = require("@angular-architects/module-federation/webpack");
const path = require("path");
const share = mf.share;
const { dependencies, devDependencies } = require('./package.json')
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
path.join(__dirname, './tsconfig.json'),
[/* mapped paths to share */]);
module.exports = {
output: {
uniqueName: environment.pluginName,
publicPath: `/plugin/user/`,
},
optimization: {
runtimeChunk: false
},
resolve: {
alias: {
...sharedMappings.getAliases(),
}
},
experiments: {
outputModule: true
},
plugins: [
new ModuleFederationPlugin({
library: { type: "module" },
name: environment.pluginName,
filename: "EntryRemote.js",
exposes: {
'./AppModule': './projects/core/src/app/app.module.ts',
'./LoginModule': './projects/core/src/app/layout/login/login.module.ts',
'./Bootstrap':'./projects/core/src/app/layout/bootstrap/bootstrap.ts'
},
shared: share({
"@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/animations": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/platform-browser/animations": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'@angular/forms': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
...sharedMappings.getDescriptors()
})
}),
sharedMappings.getPlugin()
],};
宿主加载远程模块
注意这里使用了nginx代理,实际开发过程中也要注意跨域问题
javascript
import { loadRemoteModule } from '@angular-architects/module-federation'
...
loadModule =async () => {
const Module = await loadRemoteModule({
type: 'module',
remoteEntry: 'http://localhost:3000/plugin/user/entryRemote.js',
exposedModule: `./Bootstrap`,
remoteName: pluginName
})
return Module[exposedModule]
}
踩坑
关于路由切换后页面节点没有清空
微应用引用NoopAnimationsModule/BrowserAnimationModule导致微应用节点无法被清空(路由切换时) ,需要删除微应用内的BrowserAnimationModule(可能会影响微应用独立运行):stackoverflow.com/questions/6...