Formily思想理解(小白-白话文)

1,329 阅读5分钟

表单场景的复杂度

首先我们需要思考:表单场景的复杂度在哪里

  • 字段数量多,性能随字段数增加变差
  • 字段关联时逻辑复杂
  • 表单数据管理复杂
  • 表单状态管理复杂
  • 表达场景化复用性

核心思想

精确渲染

在react下实现一个表单,如果想要手机表单数据,实现一些联动需求,大多数都是通过setState实现字段收集,虽然实现简单,但是却引入了性能问题:每次输入都会导致全量渲染,虽然有diff算法,但是diff也有计成本,从时间复杂度看的话,初次渲染表单时O(n) ,字段输入时也是 O(n),这样是明显不合理的。 所以,真正想要实现精确渲染,必须要学习 mobx的依赖追踪机制和响应式模型的抽象能力(@formily/reactive)

领域模型(DomainModel)

domainModel是对 特定问题领域的概念类、现实世界中的实体类 以及 它们之间关系的可视化表示。“主要用于详细描述问题领域本身”。(就像:数据EQ图中的实体)

通过常见的范例,抽象出一个最简单的field模型: 例如:

  • 需求为根据某些字段的值,去隐藏或者禁用某些字段(需要 value、disabled、visible)
  • 在一个表单树中,我们要索引一个字段,字段的路径一定要有(需要 path)
  • 字段对应的UI组件,以及组件的配置化参数 要可以管理吧(需要 Component,ComponentProps)
  • 还有很多
interface Field {
   path:string[],
   value:any,
   visible:boolean,
   disabled:boolean,
   component:[Component,ComponentProps]
}

为了解决联动问题,fomrily需要抽象出字段模型,包含字段所有的状态,只要操作这些状态就可以引发联动; 经过formily的不断试错和纠正,设计出了真正优雅的表单模型; 有了这样的领域模型,就可以让表单的联动变得可枚举可预测;并为后面得协议描述打下坚实基础。

路径系统

想要设计更完备的话,其实不止需要字段模型,还要有一个表单模型作为顶层模型,顶层模型管理着所有字段模型,每个字段就可以有自己的路径,formily为了大家可以优雅的查找某个字段,独创了自己的路径系统:@formily/path (有学习成本,想用就用)

field.query('.aa').value() //直接读取同级别aa字段值 
field.query('..aa').value() //读取父级aa字段值 
field.query('..[+].aa') //读取跨级相邻aa字段值

生命周期

借助mobx和路径系统,已经打造了一个比较完备的表单方案了,但是这个方案就像一个黑盒,外界无法感知方案内部状态流转的过程,想在某些过程阶段内实现一些逻辑无法实现,因此!要引入另一个概念:生命周期

将表单的生命周期作为事件钩子暴露给外界,就能做到既有抽象,但又灵活的表单方案。

协议驱动

如果要实现动态可配置表单,那必然是需要将 表单结构 变得 可序列化

序列化方式可以是:以UI为思路的UI描述协议、也可以是以数据纬四路的数据描述协议

因为表单本身就是为了维护一份数据,所以 数据描述协议是最合适不过的。而想要描述数据结构,最合适的就是 JSON-schema(一种描述JSON数据的声明性格式,可用来:简单描述数据的表面结构、根据它自动验证数据)。

但是光描述数据还不够,因为我们要输出的是实际业务可用的表单页面,所以formily的表单协议在 JSON-Schema上做了扩展,以 x-* 格式来表达扩展属性。

{
  "type": "string",
  "title": "字符串",
  "description": "这是一个字符串",
  "x-component": "Input",
  "x-component-props": {
    "placeholder": "请输入"
  }
}

这样看来,虽然 UI协议和数据协议混合在一起,但是只要有统一的扩展约定,也还能保证两种协议职责单一。

  • 然后,我们日常开始为了实现UI层面的要求,会有一些单纯的UI组件怎么描述呢?

formily定义了一个新的schema type:void,在formily中,void代表一个虚拟数据节点,该节点不占用实际数据结构。

{
  "type": "void",
  "title": "卡片",
  "description": "这是一个卡片",
  "x-component": "Card",
  "properties": {
    "string": {
      "type": "string",
      "title": "字符串",
      "description": "这是一个字符串",
      "x-component": "Input",
      "x-component-props": {
        "placeholder": "请输入"
      }
    }
  }
}
  • 再然后,日常开发中有些表单联动的逻辑怎么用 JSON-shema 描述呢? formily扩展了 x-reactions,使用方式如下:

比如一个字段要控制另一个字段的显示隐藏

{
  "type": "object",
  "properties": {
    "source": {
      "type": "string",
      "title": "Source",
      "x-component": "Input",
      "x-component-props": {
        "placeholder": "请输入"
      }
    },
    "target": {
      "type": "string",
      "title": "Target",
      "x-component": "Input",
      "x-component-props": {
        "placeholder": "请输入"
      },
      "x-reactions": [
        {
          "dependencies": ["source"],
          "when": "{{$deps[0] == '123'}}",
          "fulfill": {
            "state": {
              "visible": true
            }
          },
          "otherwise": {
            "state": {
              "visible": false
            }
          }
        }
      ]
    }
  }
}

仔细观察,formily干净的描述了联动的几个关键点:

  • 依赖方 dependencies
  • 条件 when
  • 满足条件-做什么 fulfill
  • 不满足条件-做什么 otherwise

并且 x-reactions 是一个数组,可添加随意添加多条联动关系。

经过这些,我们的表单完全可以使用协议来描述了!(并且是配置化的描述感觉,很清晰)

分层架构

formily如何分层架构,才能更加自洽且优雅呢?

杰尼龟.png

formily分为:

  • 内核层(formily core)
  • UI桥接层(React、Vue)
  • 扩展组件层 (AntD,Element)
  • 配置应用层(表单配置器)
  • JSON-schmea独立存在

内核层与UI无关,保证了用户管理的逻辑和状态不耦合任何一个框架

JSON-schema 独立存在,给UI桥接层消费,保证了协议驱动在不同UI框架下的绝对一致性。

扩展组件层,保证用户开箱即用,无需使用者花时间做二次开发。

核心优势

总结下formily的核心优势:

  • 高性能(proxy 响应式更新)
  • 开箱即用
  • 联动逻辑实现高效
  • 跨端能力,跨框架能力,复用性强
  • 动态渲染能力(根据条件、依赖、数据变化和用户输入等动态渲染表单,formik、antD Form等插件想实现动态渲染都是靠外部js逻辑处理的)。