低代码平台前端的设计与实现(二)构建引擎BuildEngine切面处理设计
上一篇文章,我们介绍了如何设计并实现一个轻量级的根据JSON的渲染引擎,通过快速配置一份规范的JSON文本内容,就可以利用该JSON生成一个基础的UI界面。本文我们将回到低开的核心—页面拖拉拽,探讨关于页面拖拉拽的核心设计器Designer的一些基本前置需求,也就是构建引擎BuildEngine切面处理设计。
只要接触过低开平台的朋友都见过这样的场景,在设计器的画布中点击已经拖拉拽好的UI元素,会有一个边框,高亮显示当前的元素,还支持操作:

在上一篇文章我们介绍了创建的整个流程:由一个构建引擎(BuildEngine)通过读取JSON DSL的组件节点ComponentNode来匹配对应的节点类型来生成UI元素。
为了实现设计器画布选中边框的需求,首先想到的一个解决方案就是仿照BuildEngine做一个类似的DesignerBuildEngine,里面的流程和BuildEngine大致相同,只是在生成最终的ReactNode节点的时候,在其外围使用某个元素进行包裹,具备边框等功能:
// DesignerBuileEngine伪代码
class DesignerBuileEngine {
   innerBuild() {
     // 在返回某个ReactNode前,使用一个div包裹
     const reactNode = xxx;
     return React.createElement(
       'div', {
       // 边框样式等数据
       },
       reactNode);
     }
}
但是这并不是一个很优雅的设计,因为如果我们衍生出一个新的DesignerRenderEngine,那么我们需要同时维护一个设计态一个运行态两个Engine,尽管他们的处理流程大致相同。
切面设计
组件构建处理
为了避免功能代码的冗余,也更方便后续的扩展性。我们考虑采用切面的设计方案。将整个处理流程的某些环节加入切面,以达到灵活处理的目的。切面的实现可以有很多种形式,例如一个回调函数,又或者传入一个对象实例(本质上还是回调)。作为一个轻量级低开模块,我们暂时设计一个简单的回调customCreateElement(createElement自定义实现),来完成build过程中,最后一步生成ReactNode的自定义处理:

该自定义创建方法将作为build的一个参数传入到构造过程中来进行调用,形如:
// 伪代码
class BuildEngine {
  // customCreateElement作为参数传入
  build(componentNode, customCreateElement) {
    this.innerBuild(componentNode, '/' + componentNode.componentName, customCreateElement);
  }
  // innerBuild对应也需要将参数传入
  private innerBuild(componentNode, path, customCreateElement) {
    // ... ...
    if (typeof customCreateElement === 'function') {
      // 如果存在外部传入的customCreateElement,则调用之
      return customCreateElement(CompConstructor, {...props}, children);
    }
    // 否则,走默认的React.createElement
    return React.createElement(CompConstructor, {...props}, children);
  }
}
根据上述的流程,我们先定义CustomCreateElement:
import {ReactNode} from "react";
import {ComponentNode, ComponentNodePropType} from "../../meta/ComponentNode";
/**
 * CreateElement自定义实现方法参数上下文
 * @field componentNode 组件节点数据
 * @field path 组件节点的路径
 * @field ComponentConstructor 已知匹配到的组件构造器
 * @field props 从ComponentNode中取到的props
 * @field children 已经创建完成的ReactNode数组或undefined
 */
interface CustomCreateElementHandleContext {
    componentNode: ComponentNode;
    path: string;
    ComponentConstructor: any;
    props: {
        [propName: string]: ComponentNodePropType
    };
    children?: ReactNode[];
}
/**
 * 函数接口 CreateElement自定义实现方法类型定义
 */
export interface CustomCreateElementHandle {
    /**
     * CreateElement自定义实现方法类型定义
     * @param context
     */
    (context: CustomCreateElementHandleContext): ReactNode | undefined;
}
这里我们使用TS的函数接口,参数context包含的字段目前有:
- componentNode 组件节点数据。将该值传入,可以在后续的处理中,根据对应的ComponentNode原始数据方便的进行自定义扩展处理。
 - path 组件节点的路径。将该值传入,可以在后续的处理中,根据对应的path方便的进行扩展处理。
 - ComponentConstructor 已知匹配到的组件构造器。这里专门使用大驼峰,就是想指明是一个组件的构造器。
 - props 从ComponentNode中取到的props。注意,这里是从ComponentNode中取到的未经任何处理的原始props。
 - children 已经创建完成的ReactNode数组或undefined。
 
如此,我们将构建引擎的中对于ReactNode节点的处理通过切面的方式,允许交给外部调用者方便进行灵活的定制开发。
回顾整个构建的流程,假设在运行时模式下(RuntimeMode),我们可以都是按照JSON DSL通过映射到默认的组件构造器来直接创建对应的ReactNode;而当处于设计态(DesginMode)的时候,就可以通过CustomCreateElementHandle机制,让上一层进行一定的包裹,进而产生出设计态的效果。
BuildEngine集成
接下来,我们将上述的CustomCreateElementHandle集成到我们的BuildEngine中,考虑到后续还可能会有新的构建过程的一些上下文,我们先定义一个BuildOptions接口类型,方便后续构建过程中,扩展更多的功能。当然,现阶段定义如下:
/**
 * 构建参数
 */
export interface BuildOptions {
    /**
     * 允许外部使用者自定义组件的构建过程
     */
    onCustomCreateElement?: CustomCreateElementHandle;
}
然后,我们适当修改原来的BuildEngine.build方法的入参,暴露buildOptions:
// build方法和innerBuild均需要暴露
-    build(componentNode: ComponentNode) {
+    build(componentNode: ComponentNode, buildOptions?: BuildOptions) {
-    private innerBuild(componentNode: ComponentNode, path: string) {
+    private innerBuild(componentNode: ComponentNode, path: string, buildOptions?: BuildOptions) {
对于innerBuild内部的实现,关于最后返回ReactNode的部分,适配onCustomCreateElement:
// innerBuild内容
+        if (typeof buildOptions?.onCustomCreateElement === 'function') {
+            // 如果外部提供了对应的自定义创建实现,则使用之
+            return buildOptions.onCustomCreateElement({
+                componentNode,
+                path,
+                ComponentConstructor: componentConstructor,
+                props: {...props},
+                children: childrenReactNode.length > 0 ? childrenReactNode : undefined
+            })
+        }
				// 否则使用默认实现
        return React.createElement(
            componentConstructor,
            {...props, key: path},
            childrenReactNode.length > 0 ? childrenReactNode : undefined
        )
至此,我们针对构建引擎BuildEngine设计了一个关键点的切面处理,为后续构建引擎支撑开发设计态提供了技术上的可能性。
基本测试
接下来,我在样例代码的地方,我们编写一个添加了onCustomCreateElement构建参数的Demo,来展示切面的效果。首先照旧,核心库里面导出对应的类型:
  export * from './meta/ComponentNode';
  export * from './engine/BuildEngine';
+ export * from './engine/aspect/CustomCreateElementHandle';
然后在,在样例工程中添加了一个新的样例页面CustomCreateElementExample:
import {BuildEngine} from "@lite-lc/core";
import {ChangeEvent, createElement, useState} from "react";
import {Input} from 'antd';
export function CustomCreateElementExample() {
    // 使用构建引擎
    const [buildEngine] = useState(new BuildEngine());
    // 使用state存储一个schema的字符串
    const [componentNodeJson, setComponentNodeJson] = useState(JSON.stringify({
        "componentName": "page",
        "children": [
            {
                "componentName": "button",
                "props": {
                    "size": "small",
                    "type": "primary"
                },
                "children": [
                    {
                        "componentName": "text",
                        "props": {
                            "value": "hello, my button."
                        }
                    }
                ]
            },
            {
                "componentName": "input"
            }
        ]
    }, null, 2))
    let reactNode;
    try {
        const eleNode = JSON.parse(componentNodeJson);
        reactNode = buildEngine.build(eleNode, {
            onCustomCreateElement: (ctx) => {
                const {ComponentConstructor, props, path, children} = ctx;
                console.debug('path: ', path)
                console.debug('props: ', props)
                return createElement(ComponentConstructor, {
                    ...props,
                    key: path
                }, children)
            }
        });
    } catch (e) {
        // 序列化出异常,返回JSON格式出错
        reactNode = <div>JSON格式出错</div>
    }
    return (
        <div style={{width: '100%', height: '100%', padding: '10px'}}>
            <div style={{width: '100%', height: 'calc(50%)'}}>
                <Input.TextArea
                    rows={4}
                    value={componentNodeJson}
                    onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
                        const value = e.target.value;
                        // 编辑框发生修改,重新设置JSON
                        setComponentNodeJson(value);
                    }}/>
            </div>
            <div style={{width: '100%', height: 'calc(50%)', border: '1px solid gray'}}>
                {reactNode}
            </div>
        </div>
    );
}
这段代码和上一章中的SimpleExample的核心差别在于:
        const eleNode = JSON.parse(componentNodeJson);
-       reactNode = buildEngine.build(eleNode);
+       reactNode = buildEngine.build(eleNode, {
+           onCustomCreateElement: (ctx) => {
+               const {ComponentConstructor, props, path, children} = ctx;
+               console.debug('path: ', path)
+               console.debug('props: ', props)
+               return createElement(ComponentConstructor, {
+                   ...props,
+                   key: path
+               }, children)
+           }
+       });
原本直接调用buildEngine.build的地方,我们加入我们自定义的实现,并进行了打印。从下面的效果也能看出:

附录
本文的所有内容已经提交至github仓库
本章对应tag为chapter_02
低代码平台前端的设计与实现(二)构建引擎BuildEngine切面处理设计的更多相关文章
- 低代码平台--基于surging开发微服务编排流程引擎构思
		
前言 微服务对于各位并不陌生,在互联网浪潮下不是在学习微服务的路上,就是在使用改造的路上,每个人对于微服务都有自己理解,有用k8s 就说自己是微服务,有用一些第三方框架spring cloud, du ...
 - 使用WtmPlus低代码平台提高生产力
		
低代码平台的概念很火爆,产品也是鱼龙混杂. 对于开发人员来说,在使用绝大部分低代码平台的时候都会遇到一个致命的问题:我在上面做的项目无法得到源码,完全黑盒.一旦我的需求平台满足不了,那就是无解. ...
 - vivo 低代码平台【后羿】的探索与实践
		
作者:vivo 互联网前端团队- Wang Ning 本文根据王宁老师在"2022 vivo开发者大会"现场演讲内容整理而成.公众号回复[2022 VDC]获取互联网技术分会场议题 ...
 - 基于低代码平台(Low Code Platform)开发中小企业信息化项目
		
前言:中小企业信息化需求强烈,对于开发中小企业信息化项目的软件工作和程序员来说,如何根据中小企业的特点,快速理解其信息化项目的需求并及时交付项目,是一个值得关注和研讨的话题. 最近几年来,随着全球经济 ...
 - 2021年哪个低代码平台更值得关注?T媒体盘点国内主流低代码厂商
		
2020年圣诞前夜,国内知名创投科技媒体T媒体旗下的T研究发布了2020中国低代码平台指数测评报告.报告除了对国内低代码行业现状进行总结外,还对主流低代码厂商的市场渗透和曝光进行测评. 报告认为,低代 ...
 - OpenDataV低代码平台增加自定义属性编辑
		
上一篇我们讲到了怎么在OpenDataV中添加自己的组件,为了让大家更快的上手我们的平台,这一次针对自定义属性编辑,我们再来加一篇说明.我们先来看一下OpenDataV中的属性编辑功能. 当我们拖动一 ...
 - vivo 游戏中心低代码平台的提效秘诀
		
作者:vivo 互联网服务器团队- Chen Wenyang 本文根据陈文洋老师在"2022 vivo开发者大会"现场演讲内容整理而成.公众号回复[2022 VDC]获取互联网技术 ...
 - 干货!可以使用低代码平台代替Excel吗?
		
低代码开发平台可以代替Excel?不用惊讶,答案是肯定的,而且,低代码开发平台可以完全代替Excel.例如Zoho Creator低代码平台,可以围绕数据存储.管理和创建工作流程.期间不需要IT人员介 ...
 - 分析师机构发布中国低代码平台现状分析报告,华为云AppCube为数字化转型加码
		
摘要:Forrester指出,中国企业数字化转型过程中,有58%的决策者正在采用低代码工具进行软件构建,另有16%的决策者计划采用低代码. 华为消息,知名研究与分析机构Forrester Resear ...
 - 开源低代码平台开发实践二:从 0 构建一个基于 ER 图的低代码后端
		
前后端分离了! 第一次知道这个事情的时候,内心是困惑的. 前端都出去搞 SPA,SEO 们同意吗? 后来,SSR 来了. 他说:"SEO 们同意了!" 任何人的反对,都没用了,时代 ...
 
随机推荐
- 《ASP.ENT Core 与 RESTful API 开发实战》-- (第6章)-- 读书笔记(上)
			
第 6 章 高级查询和日志 6.1 分页 在 EF Core 中,数据的查询通过集成语言查询(LINQ)实现,它支持强类型,支持对 DbContext 派生类的 DbSet 类型成员进行访问,DbSe ...
 - offline 2 online | 重要性采样,把 offline + online 数据化为 on-policy samples
			
论文标题:Offline-to-Online Reinforcement Learning via Balanced Replay and Pessimistic Q-Ensemble CoRL 20 ...
 - python使用pandas库读写excel文件
			
操作系统 : Windows 10_x64 Python 版本 : 3.9.2_x64 平时工作中会遇到Excel数据处理的问题,这里简单介绍下怎么使用python的pandas库读写Excel文件. ...
 - CF1795
			
A 先判断初始行不行,再模拟加入. B 题意:数轴上给定一些线段,和点 \(t\).问能否删去一些线段,使得 \(t\) 变成唯一的覆盖次数最多的点. 差分 + 贪心. C 有 \(n\) 杯水,\( ...
 - JS Leetcode 179. 最大数 题解分析,sort a-b与b-a的区别,sort排序原理解析
			
壹 ❀ 引 今天的题目来自LeetCode179. 最大数,题目描述如下: 给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数. 注意:输出结果可能非常大,所以你 ...
 - NC14685 加边的无向图
			
题目链接 题目 题目描述 给你一个 n 个点,m 条边的无向图,求至少要在这个的基础上加多少条无向边使得任意两个点可达~ 输入描述 第一行两个正整数 n 和 m . 接下来的m行中,每行两个正整数 i ...
 - NC16619 [NOIP2008]传球游戏
			
题目链接 题目 题目描述 上体育课的时候,小蛮的老师经常带着同学们一起做游戏.这次,老师带着同学们一起做传球游戏. 游戏规则是这样的:n个同学站成一个圆圈,其中的一个同学手里拿着一个球,当老师吹哨子时 ...
 - Qt5.15.0 升级至 Qt5.15.9 遇到的一些错误
			
按照之前我写的文章教程,可以很简单的编译出静态库(仅供学习交流) 编译 windows 上的 qt 静态库 编译出静态库后,替换旧版本的库,见我另一篇文章教程 VS2019 配置 QT 库 之所以没有 ...
 - Vue框架设计:性能权衡的艺术
			
"框架设计里到处都体现了权衡的艺术." 当我们设计一个框架的时候,框架本身的各个模块之间并不是相互独立的,而是相互关联.相互制约的.因此作为框架设计者,一定要对框架的定位和方向拥有 ...
 - python实用模块之netifaces获取网络接口地址相关信息
			
文档 https://pypi.org/project/netifaces/ 安装 pip install netifaces 使用 import netifaces netifaces.interf ...