描绘现实世界的桥梁--Formily

描绘现实世界的桥梁--Formily

引言

在过去,大家应该对Formily的定位不太陌生,核心是为了解决表单复杂问题,提供专业性解决方案而存在的。但是,直到最近我在我们团队思考低代码搭建方案的时候,我逐渐发现,Formily已经慢慢超越了我的认知范围,它已经不再局限在表单层面,它完全可以描绘整个视图模型,我们暂且可以称之为 "领域视图模型驱动的低代码渲染引擎",为什么可以做到这样,我会分下面几个部分来详细推导:

  • DSL的完备性
  • 内核完备性
  • 响应式机制完备性
  • 搭建器融合可行性

DSL的完备性

Formily DSL,也就是我们现在正在用的Formily JSON Schema格式,咱们可以先看看它基本的类型定义:

interface SchemaBase {
  title?:string
  description?:string
  ...
}

interface UIExtends {
  x-component?:string
  x-component-props?:Reacord<string,any>
  x-decorator?:string
  x-decorator-props?:Reacord<string,any>
  x-visible?:boolean
  ...
}

interface SchemaPrimitiveType extends SchemaBase,UIExtends {
  type: 'string' | 'number' | 'boolean'
}

interface SchemaObjectType extends SchemaBase,UIExtends {
  type: 'object'
  properties: Reacord<string,ISchema>
}

interface SchemaArrayType extends SchemaBase,UIExtends {
  type: 'array'
  items: ISchema
}

interface SchemaVoidType extends SchemaBase,UIExtends {
  type: 'void'
  properties: Reacord<string,ISchema>
}

type ISchema = SchemaPrimitiveType | SchemaObjectType | SchemaArrayType | SchemaVoidType

我们可以看到,这份DSL,不仅描述了数据,还把UI相关的也描述进去了,比如用的什么组件,组件属性是什么,组件的展示行为是什么等等。更重要的是,我们看到它扩展了一个Void类型,用于描述一个虚拟容器,它是不占用数据空间的,更多的只是用于承载UI。

那这份DSL能描述业务逻辑吗?当然能,它是支持了JS表达式用法的DSL,我们看个例子:

{
  "type":"object",
  "properties":{
    "card":{
      "type":"void",
      "x-component":"Card",
      "properties":{
         "input":{
            "type":"string",
            "x-component":"Input"
          },
          "text":{
            "type":"string",
            "x-component":"Text",
            "x-component-props":{
               "style":{
                 "color":"red"
               }
            },
            "x-content":"{{$values.input}}"
          }
      }
    }
  }
}

咱们暂且不看Formily内部是否能够实现这样的逻辑,单纯看DSL,如果我们想要实现输入框输入,控制旁边文本实时展示输入内容,最自然的写法应该就是这样的吧。我们仔细分析一下这个DSL,它都做了哪些事情:

  1. 任何UI容器都能用VoidType描述
  2. 用户不需要关心底层是用什么框架,React/Vue whatever
  3. 用户不再需要关注事件-->数据收集的过程,因为在DSL中的所有输入控件输入数据都会流到顶层Form,用户只需关注form的值即可
  4. 变量绑定,一句简单的表达式即可实现任意一个组件属性的变量绑定。同时达到响应式更新
  5. 限制任何一个节点元素,都必须要有一个路径名称,方便逻辑索引
  6. 语法限制不允许描述循环,条件,复杂UI交互,统一收敛到自定义组件层
  7. 语法限制不允许描述css stylesheet,只能在组件属性上简单描述内联style

继续推导:

  1. 描述UI容器的能力 --> 解决了UI元素与数据节点的组合关系问题
  2. 内置了数据收集 --> 解决了我们需要写大量事件监听器的问题
  3. 表达式变量绑定 --> 解决了数据与UI的关联关系问题
  4. 限制节点必须有路径 --> 自动生成数据上的层级关系,方便复杂逻辑直接索引式更新
  5. 限制不允许描述循环,条件 --> 强制收敛标准化UI组件
  6. 限制不允许描述css stylesheet --> 强制收敛标准化UI组件

最终,我们可以推导出来,只要我们有一套标准化的组件体系,这份DSL就能描述一切UI界面,包含界面中的数据交互,整个DSL更像在描述一个抽象领域视图模型,上面的信息,更多的体现的是业务数据与UI的关联关系,而非描述具体某个组件交互样式细节,比如:某个Table内部的逻辑

当然,你可能会有疑问:

  • 我想描述一个落地页
  • 我想描述一个按钮点击唤起弹窗表单提交的逻辑流程

这些问题,如果按照以上推导的思路来看的话,完全不是问题,就拿落地页来说,基本上都是由几个部分组成:

  • Header/Navigator
  • Section/Banner
  • Footer

如果我们把导航部分抽象成标准组件,可以扩展的就是具体导航项的配置即可,样式什么的全部封装在组件内,这样就能用DSL描述

对于Section内容的话,同样的,只要我们抽象出一系列的模板组件,通过组件化拼装即可,Footer也一样。

但是,你可能会发现,完全没用到表单的能力了。是不是有点大炮打蚊子呢?放心,你不用,不代表未来不会用,举个例子,比如我们的表单详情页,它是很有可能复用Header/Footer的,那其实就和落地页的组件体系天然打通了,也就是说,落地页不一定依赖表单,但是依赖表单的页面是有可能依赖落地页相关组件的。

下面我们再看看下钻场景,比如弹窗,抽屉,如何用这份DSL来描述:

{
  "type":"object",
  "properties":{
    "button":{
      "type":"void",
      "x-component":"Button",
      "x-component-props":{
         "onClick":"{{()=>$form.query('dialog').take(dialog=>dialog.componentProps.visible = true)}}" //pro code模式维护逻辑
      },
    },
    "dialog":{
      "type":"void",
       "x-designable-id":"xxx"
       "x-component":"Dialog",
       "properties":{
          "form":{
            "type":"object",
            "x-component":"Form",
            "properties":{
               "input":{
                  "type":"string",
                  "x-component":"Input"
                },
                "submit":{
                  "type":"string",
                  "x-component":"Submit",
                  "x-component-props":{
                     "onSubmit":"{{(values)=>axios.post(url,values).then('xxx')}}", //pro code模式维护逻辑
                     "style":{
                       "color":"red"
                     }
                  }
                }
            }
        }
    }
  }
}

从以上伪代码我们可以看到,这份DSL将根节点的form数据当成了一个中心化的store来维护状态,具体数据提交,则是使用的子表单数据来提交,当然,你可能会发现,我们写的表达式逻辑有点重,有没有办法进一步降低我们的逻辑编写成本呢?看下面的DSL:

{
  "type":"object",
  "properties":{
    "button":{
      "type":"void",
      "x-component":"Button",
      "x-actions":{
        "onClick":{
          "type":"OpenDialog", //OpenDialog or OpenPage or LogicFlow
          "target":"dialog"
        }
      }
    },
    "dialog":{
       "type":"void",
       "x-component":"Dialog",
       "properties":{
          "form":{
            "type":"object",
            "x-component":"Form",
            "properties":{
               "input":{
                  "type":"string",
                  "x-component":"Input"
                },
                "submit":{
                  "type":"string",
                  "x-component":"Submit",
                  "x-component-props":{
                     "style":{
                       "color":"red"
                     }
                  },
                  "x-actions":{
                    "onSubmit":{ //逻辑编排
                      "type":"LogicFlow",
                      "id":"ef572dabce",
                      "params":"{{$values.form}}"
                    }
                  }
                }
            }
        }
    }
  }
}

上面的x-actions属于Formily DSL的扩展协议属性,其实它就是会将x-actions转成具体的x-component-props事件处理器,用户基于它,可以跟一个独立的逻辑编排引擎做对接

总之,只要我们有足够完备的组件体系,Formily DSL是一个可以完全描述领域视图模型的DSL,它的视图描述能力不会像React/Vue那样灵活,但就是因为通过限制,我们就可以更加清晰的表达业务,所以FormilyDSL,你甚至可以把它称之为一门业务语言!

需要说明一下:目前Formily DSL还没支持普通属性上的表达式响应式计算(现在是需要在x-reactions上单独声明响应式计算逻辑),不过很快会支持

内核完备性

目前Formily内核最核心的能力就3个:

  • 输入数据自动收集
  • 自动校验
  • 虚字段与数据字段的关联管理能力

前面我们讲到JSON Schema的Void类型,其实就是强依赖第三点能力,所以借助Formily内核,是可以轻松支撑我们的DSL,

但是,唯一目前还没实现的就是,子表单能力

借助子表单的能力,我们就能将顶层表单数据作为一个核心状态容器,所有业务状态最终都会流向顶层表单,这样就是一个标准的单向数据流模型了,借助单向数据流,一下子提升了我们整个应用的灵活性与理论完备性

响应式机制完备性

因为Formily的响应式是完全follow的Mobx这套响应式机制,所以它的完备性是完全没有任何问题的。

搭建器融合可行性

搭建器融合,目前formily设计器,就已经做到了完美融合,所以我们也不需要担心太多。



总结

整体看下来,其实我们核心担心的还是DSL这层的完备性,从以上推导,基本上也是证明了通过Formily DSL是可以完整描述UI,同时如果我们借助一个逻辑编排器还能实现更加简单和高复用的逻辑编排能力,进一步降低页面搭建成本。

发布于 2021-09-06 13:32