搭建工具的低代码编辑器,关注重点在于数据方面物料的拓展性不同物料的代码复用

数据流向设计

编辑器具备三个UI模块以及一个数据模块

UI模块有:

  • 配置模块
  • 页面展示模块
  • 物料属性编辑模块

数据存储模块有:

  • store数据存储模块

为了保证代码运行时,各UI模块之间能够实现状态共享数据一致性,因此采用了中央状态管理的的设计模式来保证单一数据源统一调度

各模块之间的关系如下图所示

低代码6

我们的技术栈为React,为了实现中央状态管理,通常会引入store进行数据管理,在我们团队日常使用的store库中,有mobx和redux,分别代表了面向对象以及函数式两种编程范式,两者都是经过前端社区检验过的store实践

mobx和redux均可以进行状态管理和模块间通信,下表是其差异点的对比:

store库 范式 数据响应 数据 代码规范 学习曲线
redux 函数式 手动调用函数触发状态更新 可变数据,即store对象的属性 由于redux严格规范和纯函数的限制,redux的代码更可预测,但是会多出不少样板待啊吗 普遍反馈需要掌握较多概念(action,reducer)
mobx 面向对象 观察者模式 不可变数据,创建新的状态来更新数据 相对灵活,可以更自由地编写,单可能会导致代码结构不够清晰 普遍反馈更易上手

结合我们低代码编辑器的具体使用场景,使用mobx更加合理,理由如下

  • 低代码编辑器通常需要处理大量的动态数据和交互,而mobx具有较高的数据响应性灵活性,能够自动追踪状态的变化并实时更新相关组件。这样可以使得低代码编辑器的开发更加高效和便捷。
  • 低代码编辑器通常需要支持实时预览和动态配置,这就需要能够灵活地修改和更新状态。MobX使用可变数据的方式,可以直接修改状态对象的属性来更新状态,这与低代码编辑器的需求相符。
  • 此外mobx的上手成本更低,后续新同学接手时也更容易上手

mobx在代码规范上,因为更加灵活,如果不作约束,就可能导致代码结构不够清晰,这方面参考mobx的最佳实践进行以下规范:

  • 使用装饰器(Decorators),尽管装饰器一直未能列入es标准,mobx中也支持不使用装饰器,但使用装饰器可以更清晰地标识出状态的依赖关系和动作的作用范围

低代码7

如上述代码,observable是一个响应式的数据,computed为根据响应式的数据作出的计算属性,action用于改变响应式的数据,其中所有响应式数据的变化都通过action调用

  • ts类型定义规范,各物料,配置信息都必须严格遵循规范,不允许使用any
  • 通过工厂模式来约束好初始化的数据信息:比如活动的初始化信息,不同物料的初始化信息

数据结构设计

一个活动的配置类别应需包含两项

配置类别 类型 作用
基础信息 根据具体需要决定类型 活动的基础配置信息,如背景图、上线时间等
物料信息 Array 物料的数组,可重复添加和使用的

根据功能,我们可以把物料抽象为最基础的两种:纯展示型和展示型兼备逻辑型

将配置信息简化,以最基本的按钮物料(纯展示型)和文字物料(展示型兼备逻辑型)为例,其关系可以表示为下图

低代码8

ITextMaterial、IButtonMaterial以及其他物料共同组合成了materialList,每个物料具备单一职责原则,即只具备一个功能的实现

拓展性和可维护性方面,后续的开发迭代中如果需要拓展物料类型,根据这套数据结构。只需创建一个新的物料接口并继承自IMaterialItem接口,遵守IMaterialItem结构保持统一即可

IButtonMaterial相对于ITextMaterial具有逻辑属性,除目前定义的几种逻辑行为外,当有定制化的功能,只需要再实现一个IButtonMaterialTask即可,运行时会根据type来决定按钮具备的能力

解析函数最终会对物料进行列表渲染

数据类型管理方式

数据结构描述了页面配置及物料配置,决定页面如何渲染

除编辑器外,面向用户的活动页面中也具有相同的数据结构

我们项目中均使用TypeScript,为了保持统一性,数据结构的类型定义使用git subtree对数据定义进行统一管理:

git subtree可以实现一个仓库作为其他仓库的子仓库

低代码9

通过git subtree,可以方便的在活动页和编辑器项目中切换,不同的开发人员进行开发时,也可以通过封装的命令更新type子仓库,利用TypeScript的类型检查,快速发现数据结构问题

不同物料的代码复用

在项目中定义已有物料后,后期难免会因为前方反馈等原因添加新的物料,所以如何快速写一段新物料的代码,提升拓展性,就需要考虑到代码复用

目前物料的复用的主要代码量表现在两个模块上:

  • 页面展示模块:展示物料
  • 属性编辑模块:表单物料

不同的模块之间具备相似点,例如文字物料和按钮物料都存在文案、大小、位置等,为此需要提供一种机制来复用这些相似点

首先提炼物料作用在两个模块上代码的共性

  • 展示物料:支持拖拉改变位置信息、支持选中
  • 表单物料:都需要form表单进行编辑、编辑时都会更新数据

React中常见的代码复用方式有Hooks、高阶组件(HOC)、Mixins等

其中mixin已经从React 16.3开始,Mixins已经被废弃,不再推荐使用。

Hooks和HOC两者对比如下表

方法 方式 限制条件 低代码编辑器项目中的缺陷
Hooks 封装自定义hooks处理上述提到的逻辑 只可用于函数组件 多个高阶组件同时使用时可能导致冲突生命周期冲突
高阶组件(HOC) 将上述提到的逻辑封装在高阶组件中,接受一个物料的独有组件,返回新的可用组件 基本无 过度灵活低代码中状态变化较多,可能导致useEffect之类的依赖副作用不好掌控和预测

作为一个新的项目,我们会完全使用函数组件去实现,所以不需要关注hooks的限制条件,重点关注两者应用在编辑器项目中的缺陷,结合使用场景,认为使用HOC更加合理,理由如下:

  • HOC的多个高阶组件使用时可能导致冲突:对于可预见的未来中,很难出现3个以上高阶组件包裹的场景,以上提到的共性目前只需使用一个(展示物料和表单物料各一个)高阶组件即可实现
  • HOC生命周期冲突:通常在生命周期中我们会做逻辑操作,而在我们物料基础组件(被HOC函数接受的组件)中通常不会有(也不应该有太多的逻辑操作)有太多的逻辑操作,相反我们要约束物料基础组件尽可能的只进行展示,来保证数据与展示的分离
  • Hooks过度灵活:作为物料,我们都期望有规范的定义和约束,当然过度灵活的问题也是可以通过人工约束解决的,但是不如HOC将数据和展示分离的基础约束来得简单直接
  • Hooks在低代码中状态变化较多,可能导致useEffect之类的依赖副作用不好掌控和预测:对于低代码编辑器:数据的掌控显然是期望越能掌控越好,越有约束越好,越可预测越好

综上,使用HOC来实现该部分代码的复用,物料组件导出时都会经过HOC的包裹

低代码10

使用materialHOC实现拖拉功能的简化版

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const materialHOC = <P extends { material: IMaterialItem }>(
  Comp: React.FC<P>,
  type: IMaterialType,
) => {
  return observer((props: P) => {
    const { material } = props;

    const isActive = activityConfigStore.selectedMaterial?.id === material.id;

    const [{ opacity }, dragRef] = useDrag(
      () => ({
        type,
        collect: (monitor) => {
          if (monitor.isDragging()) activityConfigStore.selectMaterialById(material.id);
          return {
            opacity: monitor.isDragging() ? 0.4 : 1,
          };
        },
      }),
      [],
    );

    const handleClick = () => {
      return activityConfigStore.selectMaterialById(material.id);
    };

    return (
      <div
        onClick={handleClick}
        style={{
          opacity,
        }}
        className={`material-wrap ${isActive ? 'active' : ''}`}
      >
        <Comp ref={dragRef} {...props} />
      </div>
    );
  });
};

效果

低代码11