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无法共享

相关推荐
前端李易安2 小时前
Web常见的攻击方式及防御方法
前端
PythonFun2 小时前
Python技巧:如何避免数据输入类型错误
前端·python
hakesashou2 小时前
python交互式命令时如何清除
java·前端·python
天涯学馆2 小时前
Next.js与NextAuth:身份验证实践
前端·javascript·next.js
HEX9CF2 小时前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
ConardLi2 小时前
Chrome:新的滚动捕捉事件助你实现更丝滑的动画效果!
前端·javascript·浏览器
ConardLi3 小时前
安全赋值运算符,新的 JavaScript 提案让你告别 trycatch !
前端·javascript
凌云行者3 小时前
使用rust写一个Web服务器——单线程版本
服务器·前端·rust
华农第一蒟蒻3 小时前
Java中JWT(JSON Web Token)的运用
java·前端·spring boot·json·token
积水成江3 小时前
关于Generator,async 和 await的介绍
前端·javascript·vue.js