前言
技术栈选择
- 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 组件:
确保项目已安装 react
、react-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-app
和react-app
) - 使用 Shadow DOM 或 CSS-in-JS 库(如 styled-components)