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:
<div class="angular-mfe"><p>这是会显示在React host项目的页面内容<p></p>
</div>
@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 {}
<!--全局样式: 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里面让其生效。
单独处理代码:
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';@Injectable({providedIn: 'root'
})
export class ThemeSwitchingService {//for Toggle StyleSheetsobserver: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-facethis.appendAdditionalAssets()//for Toggle Stylesheets//Step 2: Create a MutationObserver to detect changes in childList of <head> and move <link id="overall_style"> to bottomlet 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 bottomthis.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_idnew_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?.idif(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?.idnew_link.rel=assetObject?.relnew_link.href=assetObject?.hrefreturn new_link}public additionAdditionalAssets:any[]=[{//font-face assets: required for typography assets to be injected into hostid:"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 cssid:"mfe_angular-cdk",el:"link",rel:"stylesheet",href:`${this.siteUrl}/angular/overlay-prebuilt.css`}]
}
-
App Component:
<angular-remote-to-react></angular-remote-to-react>
import { Component} from '@angular/core';@Component({selector: 'angular-mfe',templateUrl: './app.component.html',styleUrl: './app.component.scss'
})
export class AppComponent {}
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
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:
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:
new ModuleFederationPlugin({name: "app",remotes: {},shared:{...deps,'react-dom': {singleton: true,eager:true},react: {singleton: true,eager:true},}}),
-
加载Angular组件:
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 parametersconst eventData = {// User and context informationtest:'test'};// Create and dispatch the eventconst 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 组件:
// 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 的配置中暴露组件
new ModuleFederationPlugin({name: 'reactApp',filename: 'remoteEntry.js',exposes: {'./ReactComponent': './src/ReactComponent'}
});
Angular 项目配置(作为主机)
-
在
webpack.config.js
中添加:
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {plugins: [new ModuleFederationPlugin({remotes: {reactApp: "reactApp@http://localhost:3001/remoteEntry.js",},}),],
};
-
动态加载组件,在 Angular 中动态加载 React 组件:
确保项目已安装 react
、react-dom
和 @types/react
:
npm install react react-dom @types/react
-
Angular 组件逻辑
// 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 发布事件
window.dispatchEvent(new CustomEvent('reactEvent', { detail: data }));
Angular 监听事件
@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-app
和react-app
) - 使用 Shadow DOM 或 CSS-in-JS 库(如 styled-components)