注:本文没有借助微前端的方案

一、技术选型

|

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 入口

  1. {
  2. path: 'react-entry',
  3. loadChildren: () => import('./react/react.module').then(m => m.ReactModule),
  4. }

b、ReactModule 里创建子路由 ReactRoutingModule

  1. const routes: Routes = [{ path: '**', component: NgReactBridgeComponent, pathMatch: 'full' }];
  2. export const ReactRoutingModule = RouterModule.forChild(routes);

c、ReactRoutingModule 里渲染NgReactBridgeComponent。 NgReactBridgeComponent 作为 Angular 和 React 的桥梁,是一个模版为空,且作为 React 的 root 挂载点组件

  1. import { ElementRef, NgZone } from '@angular/core';
  2. import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
  3. import * as React from 'react';
  4. import * as ReactDOM from 'react-dom';
  5. import { RoutesRootComponent } from './routes';
  6. window.React = React;
  7. @Component({
  8. selector: 'app-react',
  9. template: '',
  10. changeDetection: ChangeDetectionStrategy.OnPush,
  11. })
  12. export class NgReactBridgeComponent implements OnInit {
  13. constructor(private readonly ngZone: NgZone, private readonly host: ElementRef<HTMLElement>) {}
  14. ngOnInit(): void {
  15. this.ngZone.runOutsideAngular(() => {
  16. ReactDOM.render(<RoutesRootComponent />, this.host.nativeElement);
  17. });
  18. }
  19. }

d、从 RoutesRootComponent 开始就进入了 React 的开发模式

  1. export function RoutesRootComponent() {
  2. return (
  3. <Provider store={store}>
  4. <HashRouter>
  5. <Routes>
  6. <Route path="/react-entry/">
  7. <Route path="/login" element={<LoginLayout />} />
  8. <Route path="/" element={<MainLayout />} />
  9. {/* 更多 layout */}
  10. </Route>
  11. </Routes>
  12. </HashRouter>
  13. </Provider>
  14. );
  15. }

2、技术方案B(MPA)

重新起一个以 React 为基础框架的项目(比如叫 electron-student-react),完全脱离 Angular 环境,在打包的时候把两个应用合成一个,主进程控制哪个页面用哪个项目的路由
a、假设已经有个 react 项目 electron-student-react,且磁盘存储位置与 electron-student 平级(启动时可以通过localhost:8080 访问;打包时打包目录名称为 react-app)
b、修改 signin-window-manager

  1. constructor() {
  2. super();
  3. // 设置窗口 Url
  4. this.url = new URL(environment.indexReactHtmlUrl); // 开发模式:indexReactHtmlUrl = 'http://localhost:8080';打包模式:indexReactHtmlUrl: format({ pathname: join(__dirname, '../app-react/index.html'), protocol: 'file:', slashes: true }),
  5. }

c、修改打包配置 build.sh

  1. mkdir dist/react-app
  2. cd ..
  3. cd electron-student-react
  4. yarn build
  5. cp -r react-app ../electron-student/dist/
  6. cd ../electron-student

三、实施遇到的问题

方案A适合细粒度的迁移,比如组件级别、模块级别都可以;方案B适合粗粒度迁移,比如页面级。

  • 打包问题

react 组件中写的样式,无法被 ng 默认的 webpack 配置处理,需要对 webpack 配置进行修改,大致需要这么改下:

  1. const reactScssPath = path.resolve(__dirname, '../../src/app/react');
  2. module.exports = (config, options, targetOptions) => {
  3. config.target = 'electron-renderer';
  4. config.module.rules.forEach(x => {
  5. if (x.test.toString().includes('scss')) {
  6. if (x.include) {
  7. x.include.splice(1, 1);
  8. }
  9. if (!x.exclude) {
  10. x.exclude = [];
  11. }
  12. x.exclude = x.exclude.concat(reactScssPath);
  13. }
  14. });
  15. config.module.rules.unshift({
  16. test: /\.module\.scss$/,
  17. use: [
  18. 'style-loader',
  19. {
  20. loader: 'css-loader',
  21. options: {
  22. modules: true,
  23. },
  24. },
  25. 'sass-loader',
  26. ],
  27. include: [reactScssPath],
  28. });
  29. }
  • ng 和 react 通信问题

最主要的问题是 react 如何复用 ng 的 service,目前是通过 React Context 来解决这个问题

  1. import React from 'react';
  2. import { NgReactBridgeService } from './ng-react-bridge.service';
  3. export const BridgeServiceContext = React.createContext<NgReactBridgeService>(null);
  4. this.ngZone.runOutsideAngular(() => {
  5. ReactDOM.render(
  6. <BridgeServiceContext.Provider value={this.bridgeService}>
  7. <xxxComponent destroy$={this.destroy$} />
  8. </BridgeServiceContext.Provider>,
  9. this.host.nativeElement,
  10. );
  11. });
  1. @Injectable({
  2. providedIn: 'root',
  3. })
  4. export class NgReactBridgeService {
  5. // 注入 angular service
  6. constructor(
  7. readonly store: Store,
  8. readonly clog: ClogService,
  9. readonly toast: TutorToastService,
  10. readonly http: HttpDnsService,
  11. ) {
  12. }

react 使用方 NgReactBridgeService:

  1. const xxxComponent: React.FC<{ destroy$: Subject<any> }> = props => {
  2. const bridgeService = useContext(BridgeServiceContext);
  3. return xxx;
  4. };

angular 使用方 NgReactBridgeService:

  1. export class NgReactBridgeComponent implements OnInit, OnDestroy {
  2. constructor(
  3. private readonly ngZone: NgZone,
  4. private readonly host: ElementRef<HTMLElement>,
  5. private readonly bridgeService: NgReactBridgeService,
  6. private readonly store: Store,
  7. private readonly inspirationV2Service: InspirationV2Service,
  8. ) {}
  9. }

本质上是共享了一个 Service 单例,解决两个框架之间的通信问题