MFE: React + Angular 混合demo

前言

技术栈选择

  • React :使用 create-react-app 快速搭建应用
  • Angular:使用 Angular CLI 初始化项目
  • 集成工具 :选择 module-federation(Webpack 5 原生支持)

使用 Module Federation 实现混合

Angular Remote 和 React Host 示例:

Angular Main code:(作为远程模块)

Main Component:
html 复制代码
<div class="angular-mfe">
    <p>这是会显示在React host项目的页面内容<p></p>
</div>
javascript 复制代码
@Component({
  standalone: true,
  selector: 'angular-remote-to-react',
  templateUrl: './angular-remote-to-react.component.html',
  styleUrls: ['./angular-remote-to-react.component.scss'],
  imports: [BrowserAnimationsModule],
  encapsulation: ViewEncapsulation.None,
})

export class AngularRemoteToReactComponent {}
css 复制代码
<!--全局样式: node_module里面的,或者自己写的-->
.angular-mfe {
    @import "../../../node_modules/XXXX/main-crimson.scss";
    @import "../../../node_modules/XXXX/_icons_direct_url.scss";
    @import "../../../node_modules/XXXX/_font_face_direct_url_10px.scss";
    @import "../../assets/widgets.style.scss";
}

特别注意,因为@angular/cdk这个包的元素独立在了主component外面,像下面这样,所以它的样式要单独处理,插入到html里面让其生效。

单独处理代码:

javascript 复制代码
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class ThemeSwitchingService {

  //for Toggle StyleSheets
  observer:MutationObserver
  //for Toggle Stylesheets end
 
//   siteUrl=environment.assetsUrl()//'http://localhost:4211/','UAT env'
    siteUrl= '/assets'; //相对路径

  //null, "test"
   public styleSheets=()=>[
     {
       name:"test",
       link:`${this.siteUrl}/assets/style-dbs-DLS-3-1-dialog-only.css`,
       label:"test",
       value:"test"
     },
   ]
  
  constructor(
    @Inject(DOCUMENT) private _document: Document,
  ) { 

  }
  set assetUrl(siteUrl){
    this.styleSheets()
  }

  initialiseObserver(selectedStyle:string="test"){
    //Step 1: append font-face
    this.appendAdditionalAssets()
    //for Toggle Stylesheets
    //Step 2: Create a MutationObserver to detect changes in childList of <head> and move <link id="overall_style"> to bottom
    let targetEl = this._document.getElementsByTagName("head")[0];
    let target_id='overall_style'
    this.moveStyleSheet(target_id)
    this.selectStyleSheet(selectedStyle)
  }

  disconnectObserver(){
  }


  selectStyleSheet(selectedName:string){
    let target_id='overall_style'
    //Move style sheets to the bottom
    this.moveStyleSheet(target_id);
    let selectedStyles=this.styleSheets().find(obj=>obj.value===selectedName)
    if(selectedStyles==undefined){
      selectedStyles=this.styleSheets()[0];
    }
    this._document.getElementById(target_id).setAttribute('href',selectedStyles.link)
  }

  moveStyleSheet(target_id:string){
    let head_element = document.getElementsByTagName("head")[0];
    let link_element:Element;
    if(this._document.getElementById(target_id)!=null){
        link_element=this._document.getElementById(target_id)
        if(head_element!=undefined && head_element.children[head_element.children.length-1].id!==target_id){
            head_element.removeChild(link_element)
            head_element.appendChild(link_element);
        }
    }else{
        let new_link=this._document.createElement("link")
        new_link.id=target_id
        new_link.rel="stylesheet"
        new_link.href=`${this.siteUrl}/assets/style-dbs-DLS-3-1-dialog-only.css`
        head_element.appendChild(new_link);
    }
  }

  appendAdditionalAssets(){
    this,this.additionAdditionalAssets.forEach((assetObject)=>{
      console.log("check assets", assetObject, assetObject?.id)
      this.appendAsset(assetObject)
    })
  }


  appendAsset(assetObject:any){
    let target_id=assetObject?.id
    if(document.getElementById(target_id)==null){
      let head_element = document.getElementsByTagName("head")[0];
      let new_link=this.createAssetEl(assetObject)
      head_element.appendChild(new_link);
    }
  }

  createAssetEl(assetObject:any){
    let new_link=this._document.createElement(assetObject?.el)
    new_link.id=assetObject?.id
    new_link.rel=assetObject?.rel
    new_link.href=assetObject?.href
    return new_link
  }


  public additionAdditionalAssets:any[]=[
    {
      //font-face assets: required for typography assets to be injected into host
      id:"mfe_font-face",
      el:"link",
      rel:"stylesheet",
      href:`https://fonts.googleapis.com/css2?family=Public+Sans:wght@100;300;400;500;600;700&family=Open+Sans:wght@300;400;500;600;700;800&display=swap`
    },
    {
      //@angular/cdk: required for @angular/cdk assets to be injected into host. library package removed prebuild css
      id:"mfe_angular-cdk",
      el:"link",
      rel:"stylesheet",
      href:`${this.siteUrl}/angular/overlay-prebuilt.css`
    }
  ]
}
App Component:
html 复制代码
​<angular-remote-to-react></angular-remote-to-react>
javascript 复制代码
import { Component} from '@angular/core';

@Component({
  selector: 'angular-mfe',
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss'
})
export class AppComponent {}
javascript 复制代码
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { HttpClient,provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { CommonModule, DatePipe } from '@angular/common';
import { AngularRemoteToReactComponent } from './angular-remote-to-react.component';

export function HttpLoaderFactory(http: HttpClient) {
    return new TranslateHttpLoader(http);
}

@NgModule({
  declarations: [AppComponent],
  bootstrap: [AppComponent],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    CommonModule,
    AngularRemoteToReactComponent,
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: HttpLoaderFactory,
        deps: [HttpClient]
      }
    })
  ], 
  providers: [
    DatePipe,
    provideHttpClient(withInterceptorsFromDi())
  ]
})

export class AppModule {}
导出给React挂载的Component: loadApp.ts
javascript 复制代码
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import "zone.js";
import { AppModule } from "./app/app.module";

let appRef: any = null;

const mount = async () => {
  appRef = await platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));
};

const unmount = () => {
  if (appRef) {
    appRef.destroy();
    appRef = null;
  }
};

export { mount, unmount };
webpack.config.js:
javascript 复制代码
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  output: {
    publicPath: '/',
    uniqueName: 'remoteApp'
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remoteApp',
      filename: 'remoteEntry.js',
      exposes: {
        './LoadAngularApp': './src/loadApp.ts'
      },
      shared: {
        '@angular/core': { singleton: true, strictVersion: true },
        '@angular/common': { singleton: true, strictVersion: true },
        '@angular/router': { singleton: true, strictVersion: true }
      }
    })
  ]
};

React Main code:(作为主机)

webpack.config.js:
javascript 复制代码
new ModuleFederationPlugin({
  name: "app",
  remotes: {},
  shared:{
         ...deps,
       'react-dom': {
             singleton: true,
             eager:true
        },
        react: {
              singleton: true,
              eager:true
         },
    }
 }),
加载Angular组件:
javascript 复制代码
import { useEffect, useState } from 'react';
import { BrowserRouter, Routes, Route, useNavigate } from 'react-router-dom';
import { loadRemoteModule } from '@angular-architects/module-federation';
import styled from 'styled-components';

// Styled AngularContainer component
export const AngularContainer = styled.div`
  /* Apply scaling to the entire content */
  // transform: scale(0.625);
  // transform-origin: top left;
  // width: 160%;
  
  /* Font size normalization - primary requirement */
  // * {
  //   font-size: 62.5%;
    // zoom: 0.625; /* 使用zoom属性代替transform:scale */
    // -moz-transform: scale(0.625); /* Firefox不支持zoom,使用transform */
    // -moz-transform-origin: top left;
    // display: block;
  // }
  
`;

//HOME
function Home() {
  const navigate = useNavigate();
  return (
    <div>
      <button onClick={() => navigate('/angular-mfe')}>Go to Angular MFE</button>
      <br />
    </div>
  );
}

//Angular MFE
function AngularMfe() {
  const [AngularComponent, setAngularComponent] = useState(null);
  let unmountFunction = null;

  useEffect(() => {
    const loadModule = async () => {
      const { mount, unmount } = await loadRemoteModule({
        type: 'module',
        remoteEntry: '/remoteEntry.js',
        remoteName: 'remoteApp',
        exposedModule: './LoadAngularApp'
      });

      unmountFunction = unmount;

      setTimeout(() => {
        setAngularComponent(mount);
        setTimeout(() => {
          dispatchData();
        }, 1000)
      }, 200);
    };

    loadModule();

    return () => {
      if (unmountFunction) {
        unmountFunction();
      }
    };
  }, []);

  /**
   * Dispatches initialization data to the Angular micro-frontend
   *
   * Required parameters:
   */
  const dispatchData = () => {
    try {
      // Prepare event data with required parameters
      const eventData = {
        // User and context information
        test:'test'
      };
      
      // Create and dispatch the event
      const passDataEvent = new CustomEvent('PassDataEvent', { detail: eventData });
      window.dispatchEvent(passDataEvent);
    } catch (error) {
      console.error('Failed to dispatch data:', error);
    }
  };

  if (!AngularComponent) {
    console.log(AngularComponent);
    return <div>Loading...</div>;
  }

 return (
    <div>
      <h4 className='test'>React Host Container</h4>
      <hr/>
      <AngularContainer>
        <angular-mfe />
      </AngularContainer>
    </div>
  );
}

//APP
const MyReactComponent = () => {

  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />         {/* Home */}
        <Route path="/angular-mfe" element={<AngularMfe />} />   {/* Angular MFE 路由*/}
      </Routes>
    </BrowserRouter>
  );
};

export default MyReactComponent;

Angular Host 和 React Remote 示例:

React 项目配置(作为远程模块)

创建可导出的 React 组件:
javascript 复制代码
// src/ReactComponent.jsx
import React from 'react';

export default function ReactComponent() {
  return <h1>This is a React component in Angular</h1>;
}
 
修改 webpack.config.js:在 Webpack 5 的配置中暴露组件
javascript 复制代码
new ModuleFederationPlugin({
  name: 'reactApp',
  filename: 'remoteEntry.js',
  exposes: {
    './ReactComponent': './src/ReactComponent'
  }
});

Angular 项目配置(作为主机)

webpack.config.js中添加:
javascript 复制代码
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      remotes: {
        reactApp: "reactApp@http://localhost:3001/remoteEntry.js",
      },
    }),
  ],
};
动态加载组件,在 Angular 中动态加载 React 组件:

确保项目已安装 reactreact-dom@types/react

javascript 复制代码
npm install react react-dom @types/react
Angular 组件逻辑
javascript 复制代码
// react-wrapper.component.ts
@Component({
  selector: 'app-react-wrapper',
  template: '<div id="react-container"></div>'
})
export class ReactWrapperComponent implements OnInit {
  async ngOnInit() {
    const module = await import('reactApp/ReactComponent');
    const ReactDOM = await import('react-dom');
    ReactDOM.render(module.default(), document.getElementById('react-container'));
  }
}

通信机制

通过自定义事件实现跨框架通信:

React 发布事件

javascript 复制代码
window.dispatchEvent(new CustomEvent('reactEvent', { detail: data }));

Angular 监听事件

javascript 复制代码
@HostListener('window:reactEvent', ['$event'])
onReactEvent(event: CustomEvent) {
  console.log(event.detail);
}
详情可以参考这篇博客: https://blog.csdn.net/qq_44327851/article/details/148713528?spm=1011.2124.3001.6209

样式隔离

  • 为根组件添加框架特定的 CSS 命名空间(如 angular-appreact-app
  • 使用 Shadow DOM 或 CSS-in-JS 库(如 styled-components)
相关推荐
Asort4 小时前
JavaScript设计模式(十七)——中介者模式 (Mediator):解耦复杂交互的艺术与实践
前端·javascript·设计模式
linda26184 小时前
String() 和 .toString()的区别
前端·javascript·面试
拜晨4 小时前
初探supabase: RLS、trigger、edge function
前端
拖拉斯旋风4 小时前
零基础学JavaScript,简单学个设计模式吧
javascript
wyzqhhhh4 小时前
webpack
前端·javascript·webpack
Lorin洛林4 小时前
多 Web 端子系统共享会话:原理与实践
前端
AI智能研究院4 小时前
TypeScript 快速入门与环境搭建
前端·javascript·typescript
Zyx20074 小时前
接前文细分JavaScript的六种数据类型中的null和undefined
javascript
golang学习记4 小时前
从0死磕全栈之Next.js 本地开发环境优化最佳实践
前端