基于microApp + Angular +React +LowcodeEngine实现将低代码引擎引入到Angular项目中

567 阅读8分钟

背景:

公司希望将以阿里低代码引擎为基础开发编辑器项目并以微前端的方式引入到Angular项目中,以使得两个项目可以独立开发运行。

本文整理了使用低代码引擎创建编辑器项目及如何引入基座项目中的过程要点,以及遇到的问题和解决办法。

整体过程:

首先创建基座项目(Angular)

安装angular/cli脚手架

npm install -g @angular/cli

本次开发选用的是angular/cli@13.0.0版本

  • 创建项目ng new main-project 选择默认--routing模式会自动创建一个app.routing.module.ts

  • 依次更改项目的app.component.html、src/styles.less、添加layout布局组件等内容。

  • 将文件中的代码删除,只留路由出口(router-outlet)创建React项目,配置相关文件

  • 配置angular项目基座相关文件,安装依赖

  • 使用React项目作为子应用,设置跨域支持、基础路由、publicPath

  • 引入create-react-app为基础搭建的editor

具体过程:

修改初始项目

修改app.component.html

将文件中的代码删除,只留路由出口(router-outlet)

<router-outlet></router-outlet>

修改src/styles.less

添加初始样式

/* You can add global styles to this file, and also import other style files */
*{
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html,body{
  height: 100%;
}

添加layout布局组件

在app文件夹下执行,终端的路径在app下

ng g c layout

<div class="main-container">
  <aside>
    <div class="menu">
      <div class="menu-item" [class.selected]="selected==='react'" (click)="jump2React()">跳转到React</div>
      <div class="menu-item" [class.selected]="selected==='angular'" (click)="jump2Angular()">跳转到Angular</div>
      <div class="menu-item" [class.selected]="selected==='href'" (click)="openReact()">打开页面</div>
    </div>
  </aside>
  <main>
    <router-outlet></router-outlet>
  </main>
</div>


.main-container{
  height: 100%;
  display: flex;
  background: #f5f5f9;
  aside{
    width: 300px;
    height: calc(100% - 40px);
    border: 1px dashed blue;
    padding: 20px;
    background: #FFFFFF;
    border-radius: 20px;
    margin: 20px 0 20px 20px;

    .menu-item{
      background: #f0f0f0;
      border-radius: 20px;
      padding: 8px;
      margin-top: 20px;
      cursor: pointer;
      &:first-child{
        margin-top: 0;
      }
      &:hover{
        background: #e6f7ff;
      }

      &.selected{
        background: #e6f7ff;
      }

    }
  }
  main{
    flex: 1;
    height: calc(100% - 40px);
    border: 1px dashed pink;
    background: #FFFFFF;
    border-radius: 20px;
    margin: 20px;
    padding: 20px;
  }
}


import {Component, OnDestroy} from '@angular/core';
import {ActivatedRoute, Router} from "@angular/router";
import {Subject, takeUntil} from "rxjs";

@Component({
  selector: 'app-layout',
  templateUrl: './layout.component.html',
  styleUrls: ['./layout.component.less']
})
export class LayoutComponent implements OnDestroy{
  selected = 'angular';

  destroy$: Subject<void> = new Subject<void>();

  constructor(
    private router: Router,
    private activatedRoute: ActivatedRoute
  ) {
    this.activatedRoute.url.pipe(takeUntil(this.destroy$)).subscribe(v => {
      const path = window.location.pathname;
      this.selected = path.includes('react-app')? 'react' : 'angular';
    });
  }

  jump2React() {

    this.selected = 'react';
    this.router.navigateByUrl('layout/react-app');
  }

  jump2Angular() {
    this.selected = 'angular';
    this.router.navigateByUrl('layout/angular-app');
  }

  openReact() {
    this.selected = 'href';
    window.open('http://localhost:3000/')
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

添加angular页面组件

在app文件夹下执行,终端的路径在app下

ng g c angular

在assets文件夹下引入angular.svg(把下面的图片放入assets文件下)

<div class="container">
  <div class="logo">
    <img src="assets/angular.svg"/>
    <span>Angular</span>
  </div>
</div>

.container{
  height: 100%;
  background: #fff2e8;
  display: flex;
  justify-content: center;
  align-items: center;

  .logo{
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    img{
      width: 150px;
    }
  }
}

创建react页面组件(子应用的容器)在app文件夹下执行,终端的路径在app下

ng g c react

<div class="react-container"></div>

.react-container{
  height: 100%;
  .app{
    height: 100%;
  }
  micro-app-body{
    height: 100%;
  }
}

修改app-routing.module.ts (添加路由)

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {ReactComponent} from "./react/react.component";
import {LayoutComponent} from "./layout/layout.component";
import {AngularComponent} from "./angular/angular.component";

const routes: Routes = [
  {
    path: '', pathMatch: 'full', redirectTo: 'layout'
  },
  {
    path: 'layout',
    component: LayoutComponent,
    children: [
      {
        path: 'react-app',
        title: 'react',
        children: [{
          path: '**',
          component: ReactComponent
        }]
      },
      {
        path: 'angular-app',
        title: 'angular',
        component: AngularComponent
      },
      {
        path: '', pathMatch: 'full', redirectTo: 'angular-app'
      },
    ]
  },

];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

创建React项目创建项目

npx create-react-app react-app --template typescript

安装react-router-dom

npm i react-router-dom

修改初始项目

修改index.css

*{
  margin: 0;
  padding: 0;
}

html,body,#root{
  height: 100%;
}


.test-container{
  width: 100%;
  height: 100%;
  background: #e6fffb;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.img{
  display: block;
  width: 200px;
}

修改index.tsx

import React from 'react';
import './index.css';
import App from './App';
import './App.css';
import reportWebVitals from './reportWebVitals';
import ReactDOM from 'react-dom/client';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

添加main页面

  1. 在src下创建pages文件夹

  2. 在pages下创建main文件(main.tsx)

    import React from "react"; import logo from '../logo.svg';

    export const Main: React.FC = () => { return (

    react
    ) }

修改App.tsx

import React from 'react';
import {BrowserRouter, Route, Routes} from 'react-router-dom';
import { Main } from './pages/main';

function App() {
  return (
    // @ts-ignore
    <BrowserRouter basename={window.__MICRO_APP_BASE_ROUTE__ || '/'}>
      <Routes>
        <Route path='/' element={<Main/>}></Route>
      </Routes>
    </BrowserRouter>
  );
}

export default App;

使用angular项目为基座应用

我们采用micro-app的微前端方案

1、安装依赖

在angular项目中安装micro-app的npm包

npm i @micro-zoe/micro-app --save

2、在main.ts中引入

// entry
import microApp from '@micro-zoe/micro-app'

microApp.start();

3、增加对Webcomponent的支持

在app.module.ts中添加CUSTOM_ELEMENTS_SCHEMA 到 @NgModule.schemas

// app/app.module.ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

@NgModule({
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})

4、分配一个路由给子应用

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {ReactComponent} from "./react/react.component";
import {LayoutComponent} from "./layout/layout.component";
import {AngularComponent} from "./angular/angular.component";

const routes: Routes = [
  {
    path: '', pathMatch: 'full', redirectTo: 'layout'
  },
  {
    path: 'layout',
    component: LayoutComponent,
    children: [
      {
         // 👇 非严格匹配,/react-app/* 都指向 ReactComponent 页面
        path: 'react-app',
        title: 'react',
        children: [{
          path: '**',
          component: ReactComponent
        }]
      },
      {
        path: 'angular-app',
        title: 'angular',
        component: AngularComponent
      },
      {
        path: '', pathMatch: 'full', redirectTo: 'angular-app'
      },
    ]
  },

];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

5、在页面中嵌入子应用(react.component.html)

<div class="react-container">
  <micro-app class="app" name='reactApp' url='http://localhost:3000/' baseroute='layout/react-app'></micro-app>
</div>
<!-- 
<div class="react-container">
  <micro-app class="app" name='reactApp' [attr.url]='url' baseroute='/react-app'></micro-app>
</div> -->

url改为动态可以使用attr.url,在component.ts中声明url地址

使用React项目作为子应用

1.设置跨域支持

在webpack-dev-server配置headers(可不设置)

headers: {
  'Access-Control-Allow-Origin': '*',
}

2.设置基础路由

import React from 'react';
import './App.css';
import {BrowserRouter, Route, Routes} from 'react-router-dom';
import { Main } from './pages/main';

function App() {
  return (
    // 👇 设置基础路由,如果没有设置baseroute属性,则window.__MICRO_APP_BASE_ROUTE__为空字符串
    // @ts-ignore
    <BrowserRouter basename={window.__MICRO_APP_BASE_ROUTE__ || '/'}>
      <Routes>
        <Route path='/' element={<Main/>}></Route>
      </Routes>
    </BrowserRouter>
  );
}

export default App;

3.设置publicPath

这一步借助了webpack的功能,避免子应用的静态资源使用相对地址时加载失败的情况,详情参考webpack文档 publicPath

如果子应用不是webpack构建的,这一步可以省略。

步骤1: 在子应用src目录下创建名称为public-path.js的文件,并添加如下内容

// __MICRO_APP_ENVIRONMENT__和__MICRO_APP_PUBLIC_PATH__是由micro-app注入的全局变量
if (window.__MICRO_APP_ENVIRONMENT__) {
  // eslint-disable-next-line
  __webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}

步骤2: 在子应用入口文件的最顶部引入public-path.js (index.tsx文件的第一行引入)

一定要在文件最顶部引入public-path.js,不然静态资源引入就会路径错误

// entry
import './public-path'

4.监听卸载

import './public-path';
import React from 'react';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import ReactDOM from 'react-dom/client';


const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

window.addEventListener('unmount', function () {
  root.unmount()
});
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

主子应用收发信息

基座应用收发信息

发送信息

import microApp from '@micro-zoe/micro-app'

// 发送数据给子应用 my-app,setData第二个参数只接受对象类型
microApp.setData('react-app', {type: '新的数据'})

接收信息

import microApp from '@micro-zoe/micro-app'

function dataListener (data) {
  console.log('来自子应用my-app的数据', data)
}

/**
 * 绑定监听函数
 * appName: 应用名称
 * dataListener: 绑定函数
 * autoTrigger: 在初次绑定监听函数时如果有缓存数据,是否需要主动触发一次,默认为false
 */
microApp.addDataListener(appName: string, dataListener: Function, autoTrigger?: boolean)

// 解绑监听my-app子应用的函数
microApp.removeDataListener(appName: string, dataListener: Function)

// 清空所有监听appName子应用的函数
microApp.clearDataListener(appName: string)

子应用收发信息

发送信息

// dispatch只接受对象作为参数
window.microApp.dispatch({type: '子应用发送的数据'})

接收信息

function dataListener (data) {
  console.log('来自基座应用的数据', data)
}

/**
 * 绑定监听函数,监听函数只有在数据变化时才会触发
 * dataListener: 绑定函数
 * autoTrigger: 在初次绑定监听函数时如果有缓存数据,是否需要主动触发一次,默认为false
 * !!!重要说明: 因为子应用是异步渲染的,而基座发送数据是同步的,
 * 如果在子应用渲染结束前基座应用发送数据,则在绑定监听函数前数据已经发送,在初始化后不会触发绑定函数,
 * 但这个数据会放入缓存中,此时可以设置autoTrigger为true主动触发一次监听函数来获取数据。
 */
window.microApp.addDataListener(dataListener: Function, autoTrigger?: boolean)

// 解绑监听函数
window.microApp.removeDataListener(dataListener: Function)

// 清空当前子应用的所有绑定函数(全局数据函数除外)
window.microApp.clearDataListener()

MicroApp环境变量

__MICRO_APP_ENVIRONMENT__:判断应用是否在微前端环境中

__MICRO_APP_NAME__:获取应用的name值,即标签的name值。

__MICRO_APP_PUBLIC_PATH__:子应用的静态资源前缀

__MICRO_APP_BASE_ROUTE__:子应用的基础路由

__MICRO_APP_BASE_APPLICATION__: 判断应用是否是基座应用

基于lowcode-demo二次开发的编辑器项目

1、引入lowcodeEditor插件

2、将相关资源引入项目中(考虑到公司项目需要支持内网环境,所以将引擎的cdn链接改为静态文件引入到项目public文件夹中)

3、在App.tsx中引用

配置微前端过程中所遇到的问题记录:

1、基座项目启动过程中出现低代码引擎静态资源文件找不到问题?

目前解决方法是:

  • 在基座应用index.html中引入低代码引擎所需要的js和css文件

    MainProject
  • 在基座angular项目的angular.json中配置静态资源链接

2、编辑器预览跳转需要区分是否是microApp环境?

microApp虚拟路由系统和history,实现了一套虚拟路由系统,子应用运行在这套虚拟路由系统中,和主应用的路由进行隔离,避免相互影响。

通过虚拟路由系统,我们可以方便的进行跨应用的跳转,如:

1、主应用控制子应用跳转

2、子应用控制主应用跳转

3、子应用控制其他子应用跳转

主应用

控制子应用跳转,并向路由堆栈添加一条新的记录

子应用的路由API和主应用保持一致,不同点是microApp挂载在window上

例如:

// ?app-nextjs11=%2Fnextjs11%2F
localhost:3000/main-vue2/app-nextjs11/page2?app-nextjs11=%2Fnextjs11%2F

microApp通过自定义location
针对microApp环境下,子应用控制主应用跳转

window.microApp.router.push({name: 'my-app', path: '/page1'});

非microApp环境下,使用window.open('')直接跳转

3、低代码引擎lowcode-materials二次开发并引入编辑器项目中?

将官方lowcode-materials仓库下载下来,我们选取的是packages中的基于Antd组件的antd-lowcode-materials包。

其中有个lowcode文件夹,以如下radio为例,分为_screenshots(展示在低代码编辑器物料库中的示例图)、meta.ts、snippets.ts三个文件,可以通过修改文件达到物料修改作用,执行yarn run lowcode:build可生成新包。

4、如何在lowcode编辑器外部拿到表单实例,获取表单提交内容?

在渲染模块中,使用onComGetCtx可以拿到表单实例等信息,

 <ReactRenderer          style={{ marginTop: '30px' }}          schema={schema}          components={components}          onCompGetCtx={(schema, ctx) => {            const currentFormRef = schema?.children[0]?.props?.ref;            setFormRef(currentFormRef);            setCurrentRef(ctx);            let handleDetailInfo: any = {};            detailInfo && Object.keys(detailInfo).forEach(key => {              const value = detailInfo[key];              if (isTimeFormat(value)) {                handleDetailInfo[key] = dayjs(value)              } else {                handleDetailInfo[key] = value;              }            });  

              // 给表单赋初始值            ctx[currentFormRef]?.formRef?.current.setFieldsValue(handleDetailInfo);          }}          // onCompGetRef={(schema, ref) => {          //   setCurrentRef(ref);          // }}          appHelper={{            requestHandlersMap: {              fetch: createFetchHandler()            },            // 通过id来控制表单是否回显数据            // constants: {            //   detailInfo            // }          }}        />

“本文正在参加阿里低代码引擎征文活动”