low code平台建设的一些设计和思考

 

此篇来自我在团队内部做技术分享的文稿。

前言

国内已经有不少的低代码平台:例如墨刀、云凤蝶、爱速搭等等,低代码平台面临的差异性问题主要有:

  1. 场景不同,配运营活动与配流程表单,使用的组件几乎完全不同,若组件库不够用,还需要自定义业务组件;
  2. 用户不同,运营同学希望简单拖拽完成,研发希望二次开发能力,所以既要考虑面向非研发 零代码,再考虑面向非前端研发低代码,还有面向前端的pro code;
  3. 开发者偏好不同,有人偏向react开发,有人偏向vue,使用组件库也不尽相同;
  4. 设计器要求不同,不同系统对设计器界面要求不同,面板能力也不同;
  5. 根据场景不同,有些需要一个局部表单,有些需要整个页面;

由于以上某些原因,导致一些开源工具不能很好的在实际业务落地,很多时候就只能自己开发,或基于开源二次改造。

设计器画布

待补充ing

DSL

概念

DSL是一个比较大的概念,在low code这里,我们把它限定在对页面配置描述的信息记录,这种记录通常是json结构,由前端设计器生产,在编译/渲染时被还原。一个好的DSL设计至少得满足一下特性:

    1. 全面性,dsl记录的信息得能完整完全还原页面理想使用时的排版,样式和交互
    2. 要有原子性。数据足够原子,结构足够清晰。一个大的DSL可能由几组元dsl组成,每组元dsl可能又有几个元数据组,最底层的元dsl就是一个基础组件/标签,这样对取数据很友好,最好能达到用多少取多少的目的。原子性除了对组件划分scope,也可以对功能职责划分。
    3. 扩展性强,如果想增加功能,新输入的dsl描述能够不影响现有功能下插入旧dsl,且不能增加冗余。

方案

基于以上,我提出一种MVC的DSL设计方案。view层存储页面组件的基本描述信息,比如包含这个组件的类型信息,样式信息等,且描述是无“状态”的,类似你可以认为就是视图层。;至于这个组件运行时要发生的交互行为和动态数据等放在Model层;交互行为和组件之间的联系映射关系数据放在Controller层;

如此,在view层我们可以详尽用信息来描述一个组件的样子,用嵌套结构来表达组件的层级关系和原子包含关系。举一个场景,如果在平台上我们只想预览实时的页面排版效果,这时候就只用取view层的dsl数据即可,model组件运行时的交互表现数据我不关心所以就不用取,这样就不会给其他模块和步骤造成冗余的处理和计算。

 

例子

举一个情景例子:

 

 

 

 

如图一个表单,从视图上看它位于一个页面的某个位置,包含一个选择框组件和一个输入框组件,并排展示,从功能上看,下拉框只有在选择id的时候id输入框才会显示。对于这样一个功能,id输入框在MVC三层记录的dsl描述应该分别为:

  view层:描述当前这个逻辑规则的静态信息,即描述当下拉选择框的值===id的时候,id选择框要显示,注意这层数据是“静态”的,只是对配置和规则的描述,在设计器中的表现可能就是给组件配置区域信息回填和展示。

  model层:放着这这个动态数据的运行时的过滤条件,可能是一个函数,这个会与状态管理设计有关。

  controller层: 把计算后的结果/动态数据传递给view层

模型

上面说的还是比较抽象,具体怎么实现这个设计,比如如何传递,组件状态如何管理,如何响应式等等;这里我们可以借鉴mobx/vueX的这种响应式状态管理方式,和redux这种合成/传递规范。这里再引进几个偏前端的字段概念,layout,store,mapper,来分别对应view,controller,model层的概念。

基于此,我们可以先设计确定store dsl的几个字段,

  a. state,统一的存放组件动态数据的地方

  b. action,组件运行时调用的方法,由此可以扩展组件的交互能力

  c. mutation,对state的原子性操作,改变state的规范方式

  d.getters,对state操作的的组合/计算属性方法。

  e.context,扩展属性,声明 Store 内部的各种常量、工具方法及三方库的注入,以及运行时相关的变量方法。 

 

在上面选择框输入触发输入框显示隐藏例子中,此模型下就可以描述为:

layout:

 

store:

 

mapper:

state是要改变的store的值的联系

 

 

编译能力

概念

  编译过程做的就是将上图中dsl编译能用的代码片段。编译过程和渲染过程对于low code平台来说是是必须同时存在至少一个的。编译如果是输入代码片段在浏览器里执行渲染过程,这时候的渲染过程逻辑相对少一些,可能做得更多的只是对业务的包装和处理。如果没有编译过程,那么渲染过程就要处理dsl问题,渲染过程抽出来还能解决实时预览的问题。

  对于编译输出代码片段的过程,希望能够分成两个过程,第一个过程是把原始的dsl加工成框架组件代码和数据代码,第二个过程则是根据中间的某种联系规则,能够将二者合并成更成熟的代码片段。

 

方案

编译代码片段

  针对视图层的编译,在dsl的广搜和深搜中,重要的一个环节除了还原dsl 标签tag数据到组件标签,还有就是把每层dsl数据的关键信息key加在组件标签上,后面传递数据时,就通过props的key一一对应。

  还有就是针对一些一些扩展的方法,统一挂在根组件上, 组件库内部通过mixin调用

 

  针对数据层的编译,因为数据都是通过props传入,所以在编译成目标数据代码片段的时候,需要同时收集dsl的静态数据和动态数据,最后通过动态数据优先级更高的方式覆盖一起传递。

  针对映射层的编译,我们要做的就是数据关系的合成和交互关系的合成,数据关系映射这里的key就是我们的组件key,对于value,先解构所有的静态数据,后面再把动态数据插入覆盖,最终通过所有的数据key和视图层的props名一一传递对应。

dsl:

  编译后代码片段:

 

  中间还有一个比较有争议的操作就是,dsl里的静态数据是不是在编译过程中就在视图层合成了,比如样式,在视图层编译的时候就把对应的样式属性还原成style样式,而不是作为数据再编译合并到数据代码片段(比如store文件)里以props数据key形式一一传递。这样看上去一个好处貌似是做了动静的区分,但在数据流传递上不够纯粹(store)。

 

状态合成

  编译过程的重要的第二个操作,就是如何真正的利用这层映射关系把数据传递给组件内部。

  这里差不多就是要解决两个问题,一个是store数据怎么传递,第二个就是action动作怎么引发rerender。

  第一个问题我们可以接收上述store片段的的state和action,将两个js合成为一个HOC组件,根据映射关系,将state的数据保存在HOC的data里,再根据props传递。

  第二个问题是因为store只是一个静态数据结构,需要我们包装成有生命周期的响应式对象,我们可以给store包装成发布订阅模式的类,store的所有数据存在构造器里,实现一个订阅函数,定义好action触发的数据流,比如action只能由commit去触发原子操作,每次操作时,依次执行订阅器里存储的函数。然后我们在HOC组件的挂载时期去订阅该组件的setData操作,setData会重新根据映射关系读取赋值HOC的data,如果有变化,HOC的子组件的props传递就会引起rerender。(当然中间就可以做componentShouldUpdate之类的优化操作)

 

store类

state: store.getState(),
commit: store.commit,
dispatch: store.dispatch,
getters: store.getters,

context: store.context

 

HOC内部:

  在HOC内部订阅了hoc的setState方法,那么每次触发交互事件commit的时候都会setHOC的data,就会引发响应式渲染。

action扩展

映射关系:

  要增加的action方法我们可以在设计器创建工具初始化schema的时候就创建和插进去。要插入的这些方法,默认带有{ state, commit, dispatch, getters, context } 和config参数等。第一个对象参数是在HOC合成的时候,默认给所有的action方法注入的,第二个参数是组件库中组件真正执行的时候传递的参数,这里面可以传入组件库的this等东西。

这块既然讲到扩展,其实能引起交互的变化,我们统称为事件,这块我们想到的有事件处理的几种设计:

  • 弹窗的这种交互,在action里动态改变弹窗的显隐相关属性
  • 表单查询,通过触发targetID,传递给组件信息和type 
vm.form.handleActionBefore({ actionType: "formSearch", targetWidget: item.options.target })

 

  • request请求,外部的请求方法,要emit的方法等,这种场景就是编译生成的组件外面如果还有渲染能力包裹了组件,那么在编译HOC状态合成的的时候就要注意把这层props再传递下去。(幻视当前缺失的,因为幻视输出的是页面,就没有考虑编译生成的组件外面还有这层组件的这个需求)

 

工程编译

  上面一般编译完了之后只会输出个别几个关键js片段或者文件。我们可以提前准备一个模板工程,每次编译除了输出上面信息,还拷贝这个模板工程并和输入的js片段合并,形成一个真正的前端工程。在这个模板工程除了方便压缩打包,还可以放一些公共样式,版本信息,预览信息(方便用户下载工程代码直接调试),存放node_modules(拷贝的工程的node_module都软链到模板工程的node_module,实现node_module共享一份,节省空间)

 

 

渲染能力

  如果没有编译能力,不需要打包流程,那我们做的渲染包可能就是一个adapter适配器组件。

  上面编译的第一步输出的就是就可以输createElement的那几个参数信息,处理和第一步处理一样

  状态合成的操作还是得做,最终状态合成后导出的就是一个组件,然后最后通过动态组件<component is > 来显示这个组件。没有编译过程只要渲染过程其实就是没有编译能力中的工程编译过程。无编译流程的渲染过程其实就是把dsl->template->connect->组件变成了dsl->connect->组件。

  在现有dsl的这种设计下,这些处理避免不了,无法避免。业界formily的思路是一样,基于MVVM模式,只是他们基于mobx自己实现了一个状态管理库。

   epage的渲染思路,就是每个widget 操作自己的schema就可以了。

{
  "key": "kuEXemrCZ",
  "widget": "grid",
  "hidden": false,
  "option": {
    "gutter": 0,
    "align": "top",
    "justify": "start"
  },
  "style": {
    "margin-right": "auto",
    "margin-left": "auto",
    "background": [],
    "container": {
      "background-color": "",
      "background": []
    }
  },
  "label": {
    "width": 80,
    "position": "right",
    "colon": false
  },
  "container": true,
  "children": [
    {
      "span": 24,
      "list": [
        {
          "key": "kab0Cd9ZP",
          "widget": "select",
          "hidden": false,
          "option": {
            "type": "static",
            "url": "",
            "adapter": "return data",
            "dynamicData": [],
            "data": [
              {
                "key": "A",
                "value": "A"
              },
              {
                "key": "B",
                "value": "B"
              }
            ],
            "multiple": false,
            "clearable": true
          },
          "style": {},
          "name": "kab0Cd9ZP",
          "type": "string",
          "label": "下拉框",
          "description": "",
          "help": "",
          "disabled": false,
          "rules": [
            {
              "required": false,
              "message": "必填",
              "trigger": "change",
              "type": "string"
            }
          ],
          "placeholder": "请选择",
          "default": ""
        },
        {
          "key": "k5G7mDbx3",
          "widget": "button",
          "hidden": false,
          "option": {
            "text": "提交",
            "type": "primary",
            "icon": "",
            "long": false,
            "ghost": false,
            "shape": "square",
            "script": ""
          },
          "style": {},
          "disabled": false
        }
      ]
    }
  ],
  "title": "",
  "description": "",
  "size": "default",
  "logics": [
    {
      "type": "value",
      "key": "kab0Cd9ZP",
      "action": "=",
      "value": "1",
      "relation": "or",
      "trigger": "prop",
      "script": "",
      "effects": [
        {
          "key": "k5G7mDbx3",
          "properties": [
            {
              "key": "hidden",
              "value": true
            },
            {
              "key": "disabled",
              "value": false
            }
          ]
        }
      ]
    }
  ],
  "store": {
    "dicts": []
  }
}
View Code

  epage渲染还原创建组件的时候,会同时遍历logic数组逻辑,logic记录了动作组件和触发组件的key,在还原触发组件的时候,给触发组件标签上加disable和给这个组件加上相应的disable和v-if标签,disable和v-if依赖的数据流存在vuex里,动作组件 都是通过onchange事件去连接vuex的数据流。

  一个简单的递归渲染demo:

const render=(schema,params)=>{
  schema=Array.isArray(schema)?schema:[schema];
  const dom=schema.map((item,i)=>{
    let {type,props,children}=item;
    type=(type||'div').trim();
    const first=type.charAt(0);
    type=first.toUpperCase()===first?(components[type]||'div'):type;
    props={
      key:i,
      ...formatProps(props,params),
    };
    children=Array.isArray(children)?render(children,params):[formatChildren(children||props.children,params)??null];
    return createElement(type,props,...children);
  });
  return dom;
};
 

 

<template>
  <div class="opes-form">
    <component
      v-for="(formItem, index) in FormItems"
      :key="index + (formItem.gid ? formItem.gid : Math.random())"
      :is="formItem.type"
      :info="formItem"
      :taskId="taskId"
      :nodeId="nodeId"
      :processInstanceId="processInstanceId"
      :extraData="extraData"
      :commitInfoList="commitInfoList"
      @msg="$_msg(arguments[0], index, arguments[1])"
    ></component>
  </div>
</template>
View Code

 

 

 

 

 

 

 

 

 

 

 

posted @ 2021-08-23 14:34  Lawliet__zmz  阅读(627)  评论(0编辑  收藏  举报