Module Federation落地实践(React与Angular)

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...

tailwind无法共享

相关推荐
y先森13 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy13 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891116 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿1 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡2 小时前
commitlint校验git提交信息
前端
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐3 小时前
前端图像处理(一)
前端