一、技术选型
|
| Angular 体系 | React 体系 | |
|---|---|---|
| 视图层 | Angular 10 | react v17 |
| 路由层 | @angular/router | react-router v5 |
| 状态管理层 | ngrx/store ngrx/effect | redux,react-redux,redux-observable, reselect,immutable |
| 请求层 | @angular/common/http | axios |
二、技术方案
1、技术方案A(SPA)
将 React 应用作为 Angular 的一个子组件,React 应用内部完全自治:自己的状态管理、自己的路由等
a、angular 应用中开辟一个一级路由用作 React 入口
{path: 'react-entry',loadChildren: () => import('./react/react.module').then(m => m.ReactModule),}
b、ReactModule 里创建子路由 ReactRoutingModule
const routes: Routes = [{ path: '**', component: NgReactBridgeComponent, pathMatch: 'full' }];export const ReactRoutingModule = RouterModule.forChild(routes);
c、ReactRoutingModule 里渲染NgReactBridgeComponent。 NgReactBridgeComponent 作为 Angular 和 React 的桥梁,是一个模版为空,且作为 React 的 root 挂载点组件
import { ElementRef, NgZone } from '@angular/core';import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';import * as React from 'react';import * as ReactDOM from 'react-dom';import { RoutesRootComponent } from './routes';window.React = React;@Component({selector: 'app-react',template: '',changeDetection: ChangeDetectionStrategy.OnPush,})export class NgReactBridgeComponent implements OnInit {constructor(private readonly ngZone: NgZone, private readonly host: ElementRef<HTMLElement>) {}ngOnInit(): void {this.ngZone.runOutsideAngular(() => {ReactDOM.render(<RoutesRootComponent />, this.host.nativeElement);});}}
d、从 RoutesRootComponent 开始就进入了 React 的开发模式
export function RoutesRootComponent() {return (<Provider store={store}><HashRouter><Routes><Route path="/react-entry/"><Route path="/login" element={<LoginLayout />} /><Route path="/" element={<MainLayout />} />{/* 更多 layout */}</Route></Routes></HashRouter></Provider>);}
2、技术方案B(MPA)
重新起一个以 React 为基础框架的项目(比如叫 electron-student-react),完全脱离 Angular 环境,在打包的时候把两个应用合成一个,主进程控制哪个页面用哪个项目的路由
a、假设已经有个 react 项目 electron-student-react,且磁盘存储位置与 electron-student 平级(启动时可以通过localhost:8080 访问;打包时打包目录名称为 react-app)
b、修改 signin-window-manager
constructor() {super();// 设置窗口 Urlthis.url = new URL(environment.indexReactHtmlUrl); // 开发模式:indexReactHtmlUrl = 'http://localhost:8080';打包模式:indexReactHtmlUrl: format({ pathname: join(__dirname, '../app-react/index.html'), protocol: 'file:', slashes: true }),}
c、修改打包配置 build.sh
mkdir dist/react-appcd ..cd electron-student-reactyarn buildcp -r react-app ../electron-student/dist/cd ../electron-student
三、实施遇到的问题
方案A适合细粒度的迁移,比如组件级别、模块级别都可以;方案B适合粗粒度迁移,比如页面级。
- 打包问题
react 组件中写的样式,无法被 ng 默认的 webpack 配置处理,需要对 webpack 配置进行修改,大致需要这么改下:
const reactScssPath = path.resolve(__dirname, '../../src/app/react');module.exports = (config, options, targetOptions) => {config.target = 'electron-renderer';config.module.rules.forEach(x => {if (x.test.toString().includes('scss')) {if (x.include) {x.include.splice(1, 1);}if (!x.exclude) {x.exclude = [];}x.exclude = x.exclude.concat(reactScssPath);}});config.module.rules.unshift({test: /\.module\.scss$/,use: ['style-loader',{loader: 'css-loader',options: {modules: true,},},'sass-loader',],include: [reactScssPath],});}
- ng 和 react 通信问题
最主要的问题是 react 如何复用 ng 的 service,目前是通过 React Context 来解决这个问题
import React from 'react';import { NgReactBridgeService } from './ng-react-bridge.service';export const BridgeServiceContext = React.createContext<NgReactBridgeService>(null);this.ngZone.runOutsideAngular(() => {ReactDOM.render(<BridgeServiceContext.Provider value={this.bridgeService}><xxxComponent destroy$={this.destroy$} /></BridgeServiceContext.Provider>,this.host.nativeElement,);});
@Injectable({providedIn: 'root',})export class NgReactBridgeService {// 注入 angular serviceconstructor(readonly store: Store,readonly clog: ClogService,readonly toast: TutorToastService,readonly http: HttpDnsService,) {}
react 使用方 NgReactBridgeService:
const xxxComponent: React.FC<{ destroy$: Subject<any> }> = props => {const bridgeService = useContext(BridgeServiceContext);return xxx;};
angular 使用方 NgReactBridgeService:
export class NgReactBridgeComponent implements OnInit, OnDestroy {constructor(private readonly ngZone: NgZone,private readonly host: ElementRef<HTMLElement>,private readonly bridgeService: NgReactBridgeService,private readonly store: Store,private readonly inspirationV2Service: InspirationV2Service,) {}}
本质上是共享了一个 Service 单例,解决两个框架之间的通信问题
